05 · Collections (List, Map, Set)¶
Lists¶
A List is an ordered, indexable collection — Dart's equivalent of an array,
but growable by default.
void main() {
var fruits = ['apple', 'banana', 'cherry'];
print(fruits[0]); // apple
print(fruits.length); // 3
fruits.add('date');
fruits.insert(1, 'apricot');
print(fruits);
// [apple, apricot, banana, cherry, date]
fruits.remove('banana');
fruits.removeAt(0);
print(fruits);
// [apricot, cherry, date]
print(fruits.contains('cherry')); // true
print(fruits.indexOf('date')); // 2
}
Fixed-length lists (can't add/remove, but elements are still mutable) and truly immutable lists are both available:
void main() {
var fixed = List.filled(3, 0); // [0, 0, 0], fixed-length
fixed[0] = 99; // OK -- elements are mutable
// fixed.add(1); // runtime error -- can't grow a fixed-length list
var immutable = const [1, 2, 3];
// immutable.add(4); // compile error -- can't modify a const list
print(immutable); // [1, 2, 3]
}
Maps¶
A Map stores key-value pairs, similar to a dictionary or hash map.
void main() {
var ages = {'Ada': 36, 'Grace': 85, 'Linus': 55};
print(ages['Ada']); // 36
print(ages['Unknown']); // null -- missing keys return null, not an error
ages['Ada'] = 37; // update
ages['Katherine'] = 101; // insert
print(ages);
// {Ada: 37, Grace: 85, Linus: 55, Katherine: 101}
ages.remove('Linus');
print(ages.containsKey('Grace')); // true
print(ages.keys); // (Ada, Grace, Katherine)
print(ages.values); // (37, 85, 101)
ages.forEach((name, age) {
print('$name is $age');
});
}
Map.entries and destructuring give a convenient way to iterate both key and
value together:
void main() {
var ages = {'Ada': 36, 'Grace': 85};
for (var entry in ages.entries) {
print('${entry.key}: ${entry.value}');
}
// Ada: 36
// Grace: 85
}
Sets¶
A Set is an unordered collection of unique elements — duplicates are
automatically discarded.
void main() {
var uniqueNumbers = {1, 2, 2, 3, 3, 3};
print(uniqueNumbers); // {1, 2, 3}
uniqueNumbers.add(4);
uniqueNumbers.add(2); // no-op, already present
print(uniqueNumbers); // {1, 2, 3, 4}
var a = {1, 2, 3};
var b = {2, 3, 4};
print(a.union(b)); // {1, 2, 3, 4}
print(a.intersection(b)); // {2, 3}
print(a.difference(b)); // {1}
}
Note: {} alone creates an empty Map, not a Set — for an empty set, use
<int>{} or Set<int>().
Spread operator and collection-if / collection-for¶
void main() {
var veggies = ['carrot', 'potato'];
var fruits = ['apple', 'banana'];
// Spread -- inline one collection's elements into another
var groceries = [...fruits, ...veggies, 'bread'];
print(groceries);
// [apple, banana, carrot, potato, bread]
bool includeDessert = true;
var menu = [
'soup',
'salad',
if (includeDessert) 'cake', // collection-if -- conditionally include an element
];
print(menu); // [soup, salad, cake]
var doubled = [for (var v in [1, 2, 3]) v * 2]; // collection-for
print(doubled); // [2, 4, 6]
}
Iteration and common methods¶
void main() {
var numbers = [5, 3, 8, 1, 9, 2];
numbers.sort();
print(numbers); // [1, 2, 3, 5, 8, 9]
print(numbers.first); // 1
print(numbers.last); // 9
print(numbers.reversed.toList()); // [9, 8, 5, 3, 2, 1]
var evens = numbers.where((n) => n.isEven).toList();
print(evens); // [2, 8]
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 4, 6, 10, 16, 18]
int sum = numbers.fold(0, (total, n) => total + n);
print(sum); // 28
}
Cheat sheet¶
| Type | Literal | Ordered? | Duplicates? | Access |
|---|---|---|---|---|
List<T> |
[1, 2, 3] |
Yes | Yes | By index: list[0] |
Map<K, V> |
{'a': 1} |
Insertion order | Keys unique | By key: map['a'] |
Set<T> |
{1, 2, 3} |
Insertion order | No | By value: set.contains(x) |
How It Actually Works¶
List, Map, and Set literals aren't three unrelated syntaxes — the
compiler decides which concrete class to instantiate based on the literal's
static context, then wires up growable storage underneath. A growable
List in Dart is backed by an array that's over-allocated and doubled in
capacity when it fills up (the classic amortized-O(1)-append strategy) —
.add() is fast on average but occasionally triggers a full reallocation
and copy, which is why building a very large list with repeated .add()
calls, while still fine in practice, does more total copying than
constructing it with a known-size constructor.
Map and Set are hash-based: both hash the key (or element) via its
hashCode and bucket it accordingly, which is exactly why overriding ==
without overriding hashCode consistently (equal objects must produce equal
hash codes) silently breaks lookups — two "equal" keys can land in different
buckets and the map will report containsKey as false even though a ==
check on the same two objects returns true.
The spread operator (...) and collection-if/collection-for are resolved
entirely at compile time into equivalent imperative code — [...a, ...b]
desugars to something like "create a new growable list, iterate a and
b, appending each element," and if (cond) x inside a literal desugars to
a conditional append. There's no separate "spread" runtime representation;
by the time your code reaches the VM, it's ordinary loop and append
operations, which is why spreading a null collection without ...?
throws immediately — the desugared code calls .iterator on null.
🔀 See this in another language¶
Exercise¶
Given a List<Map<String, dynamic>> where each map represents a person with
'name' (String) and 'age' (int) keys, write code that: filters to only
people 18 or older, extracts just their names into a new List<String>,
sorts that list alphabetically, and prints the result. Then build a Set<int>
from two overlapping lists of numbers and print their union, intersection, and
difference.