Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 5 – Collections and iterables

Sets and maps in Dart: uniqueness and lookup

Build Dart sets and maps, count and group with update and putIfAbsent, and pick between LinkedHashMap, HashMap, SplayTreeMap and ListQueue.

  • Beginner
  • 15 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build sets and maps with the right literal, including the empty-braces gotcha
  • Use set algebra, union, intersection, difference and containsAll, on real inputs
  • Count and group items with update, putIfAbsent and the null-aware ??= operator
  • Pick a key type whose == and hashCode behave by value, not by identity
  • Choose between LinkedHashMap, HashMap, SplayTreeMap and ListQueue

Before you start

What you’ll learn · Before you start

What you will learn

  • Build sets and maps with the right literal, including the empty-braces gotcha
  • Use set algebra, union, intersection, difference and containsAll, on real inputs
  • Count and group items with update, putIfAbsent and the null-aware ??= operator
  • Pick a key type whose == and hashCode behave by value, not by identity
  • Choose between LinkedHashMap, HashMap, SplayTreeMap and ListQueue

Before you start

On this page

Write var x = {} and ask whether x is a Set: Dart answers false. An empty pair of braces is a Map, so a program meant to hold three tags starts with the wrong type and prints no warning. Maps and sets look alike on the page, and the small rules that keep them straight are easy to miss: the key that compares by identity when it should compare by value, the iteration order that four different classes under one word keep in four different ways.

This lesson counts a mixed Hindi-English sentence with update, runs set algebra on two teams’ skills, keys a seat map by a record so that (row: 'B', seat: 12) is one key, and measures the key order of three Map flavours in one run. Every number comes from a program you can run on the Dart SDK 3.13.5.

An empty pair of braces is a Map, not a Set

Both Set and Map use braces in their literals, and three rules decide which type a literal gives you. A literal with key: value pairs is a Map. A literal of plain values is a Set. An empty literal, with nothing to tell them apart, is a Map: Map has had {} since Dart’s first release, and keeping that meant that an empty literal could not quietly change its type when Set literals arrived in 2.2. Run this program and read each line against the three rules:

empty_braces.dart: which braces make which type Dart · empty_braces.dart
void main() {
  // An empty pair of braces is a Map, not a Set.
  final x = {};
  print('var x = {} is Map: ${x is Map}');
  print('var x = {} is Set: ${x is Set}');

  // To make an empty set, say what the elements are.
  final tags = <String>{};
  tags.add('offer');
  tags.add('offer');
  tags.add('festive');
  print('<String>{} after three adds: $tags');
  print('tags.length: ${tags.length}');

  // A literal with pairs is a Map; a literal of plain values is a Set.
  final prices = {'chai': 20, 'poha': 60, 'thali': 180};
  final cities = {'Pune', 'Mumbai', 'Pune', 'Delhi'};
  print('prices.runtimeType is Map: ${prices is Map}');
  print('cities.runtimeType is Set: ${cities is Set}');
  print('cities: $cities');
  print('cities.length: ${cities.length}');
}

Output

var x = {} is Map: true
var x = {} is Set: false
<String>{} after three adds: {offer, festive}
tags.length: 2
prices.runtimeType is Map: true
cities.runtimeType is Set: true
cities: {Pune, Mumbai, Delhi}
cities.length: 3

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run empty_braces.dart

var x = {} is a Map, so x is Set printed false. To make an empty set, name the element type: <String>{} is a set, and three adds of offer, offer and festive left a set of length 2, because a set drops duplicates silently. The non-empty literals went the other way: {'chai': 20, 'poha': 60, 'thali': 180} has pairs so it is a Map, and {'Pune', 'Mumbai', 'Pune', 'Delhi'} has plain values so it is a Set, which collapses the second Pune and prints {Pune, Mumbai, Delhi}.

Two slightly different ways say the type without a prefix: a context type, so final Set<String> tags = {} makes a set because the variable’s type asks for one, and a type argument on a constructor, as in Set<String>() or <String>{}. The analyzer picks the type from whichever one tells it the most.

An empty literal with no context

var items = {}; items.add('milk'); fails with “The method ‘add’ isn’t defined for the type ‘Map<dynamic, dynamic>’”, because Map has no add. The fix is a context (Set&lt;String&gt; items = {}) or a type argument (<String>{}).

The same two tools, a type argument on the literal and a context type on the variable, rescue a non-empty literal whose element type the compiler cannot work out, such as a heterogeneous <Object>{...} of mixed strings and integers. When the elements are of one type that infers cleanly, as in {20, 60, 180} of three ints, neither is needed; the inferred Set<int> is what you expect.

Key idea

Braces with pairs make a Map, braces with plain values make a Set, and an empty pair of braces is a Map; use <String>{} or a Set<String> variable to say you meant a set.

Set algebra: union, intersection and difference

A Set is the right type for a yes-or-no question about values, with no counts and no order to defend. Three operations run a set algebra on two sets. union returns a new set with every value of either side. intersection returns a new set with the values that are in both sides. difference returns a new set with the values of the first side that are not in the second. Each one gives you a new set, so neither input is touched. containsAll asks whether every value of a given set lies in another. contains on a single value runs in O(1) on average, which is why reaching for a Set is often a speed-up over a List.contains loop.

set_algebra.dart: two teams' skills, three set operations Dart · set_algebra.dart
void main() {
  final backend = {'Dart', 'Postgres', 'Docker', 'HTTP', 'Linux'};
  final mobile = {'Dart', 'Flutter', 'HTTP', 'iOS', 'Android'};

  print('backend: $backend');
  print('mobile:  $mobile');
  print('union: ${backend.union(mobile)}');
  print('intersection: ${backend.intersection(mobile)}');
  print('only backend knows: ${backend.difference(mobile)}');
  print('only mobile knows: ${mobile.difference(backend)}');

  print('backend has every skill of [Dart, HTTP]: '
      '${backend.containsAll({'Dart', 'HTTP'})}');
  print('backend has every skill of [Dart, Flutter]: '
      '${backend.containsAll({'Dart', 'Flutter'})}');

  // Iteration keeps insertion order, as the LinkedHashSet documentation says.
  final order = <String>{};
  for (final name in ['Riya', 'Asha', 'Riya', 'Dev', 'Asha']) {
    order.add(name);
  }
  print('insertion order after five adds: ${order.toList()}');
}

Output

backend: {Dart, Postgres, Docker, HTTP, Linux}
mobile:  {Dart, Flutter, HTTP, iOS, Android}
union: {Dart, Postgres, Docker, HTTP, Linux, Flutter, iOS, Android}
intersection: {Dart, HTTP}
only backend knows: {Postgres, Docker, Linux}
only mobile knows: {Flutter, iOS, Android}
backend has every skill of [Dart, HTTP]: true
backend has every skill of [Dart, Flutter]: false
insertion order after five adds: [Riya, Asha, Dev]

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run set_algebra.dart

The intersection of a backend team’s skills and a mobile team’s skills gave {Dart, HTTP}, the two skills they share. backend.difference(mobile) kept {Postgres, Docker, Linux}, the skills only the backend team has, while mobile.difference(backend) gave {Flutter, iOS, Android}, the skills only the mobile team has. containsAll on {Dart, HTTP} answered true and on {Dart, Flutter} answered false, which is the question “could the backend team cover this list of required skills on paper?”

The last line matters for anyone who iterates a set. The default Set constructor and the set literal both give you a LinkedHashSet, which the API page describes as a set in which “iteration order is the insertion order of the first add of each element”. Five adds of Riya, Asha, Riya, Dev, Asha built the set [Riya, Asha, Dev] and kept that order; a HashSet from dart:collection has no such promise, so iteration there is in no order you can rely on.

A set for membership questions, a list for order and repeats

Reach for a Set when a program asks “is this value here?” or “which values do these two have in common?”; keep a List when a value may appear more than once or when the order you added items in is part of what you want to show. [tags.toSet()] turns a list into a set of its distinct values in one line.

Checkpoint

Check: What braces make which type · Set algebra and iteration order

  1. Question 1 of 2 What does this program print?

    Choose one answer.

    final x = {};
    print(x is Set);
    print(x is Map);
    Show the answer: Question 1 of 2

    Answer: false, then true

    The brace literal keeps its historical meaning: with nothing to tell them apart, braces mean a Map. For an empty set, write <String>{} or declare the variable's type as Set<String>.

  2. Question 2 of 2 'backend' is {Dart, Postgres, Docker, HTTP, Linux} and 'mobile' is {Dart, Flutter, HTTP, iOS, Android}. Which calls build the set {Dart, HTTP}? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • backend.intersection(mobile)
    • mobile.intersection(backend)

    intersection reads "the values in both"; difference reads "the values in the first but not the second". The run of set_algebra.dart prints both to confirm.

Build counts and groups without the missing-key branch

Reading a key that is not in a Map returns null. That one rule turns every “look it up, bump it or make one” loop into a branch: a null has no +, and counts[word] + 1 would fail. Map has three operations that remove that branch and read better. update(key, (c) => c + 1, ifAbsent: () => 1) tries to update an existing entry and, when the key is new, runs the A callback that `Map.update` runs when a key is missing: it returns the value to store under that key. once to make a value. putIfAbsent(key, () => <String>[]) returns the existing value or writes a new one from the builder; it returns that value, so a chain like .add(word) works on either case. cache[key] ??= value writes only when the slot is null.

word_count.dart: counting and grouping a mixed Hindi-English sentence Dart · word_count.dart
void main() {
  final sentence = 'namaste doston chai lao chai aur poha garam chai hai';

  // Build counts with update(..., ifAbsent: ...): one line per word, no missing-key branch.
  final counts = <String, int>{};
  for (final word in sentence.split(' ')) {
    counts.update(word, (c) => c + 1, ifAbsent: () => 1);
  }
  print('sentence: $sentence');
  print('counts: $counts');
  print('distinct words: ${counts.length}');
  print('chai appears ${counts['chai']} times');

  // A missing key returns null, so counts never throws when a word is absent.
  print('missing key counts["english"]: ${counts['english']}');

  // Another pattern that reads well: putIfAbsent plus a mutation.
  final byLetter = <String, List<String>>{};
  for (final word in sentence.split(' ')) {
    byLetter.putIfAbsent(word[0], () => <String>[]).add(word);
  }
  print('grouped by first letter: $byLetter');

  // The ??= operator writes a value only when the slot is null.
  final cache = <String, String>{};
  cache['chai'] ??= 'uploaded 2 s ago';
  cache['chai'] ??= 'uploaded now'; // skipped, chai is set
  cache['poha'] ??= 'uploaded now';
  print('cache after three ??= writes: $cache');
}

Output

sentence: namaste doston chai lao chai aur poha garam chai hai
counts: {namaste: 1, doston: 1, chai: 3, lao: 1, aur: 1, poha: 1, garam: 1, hai: 1}
distinct words: 8
chai appears 3 times
missing key counts["english"]: null
grouped by first letter: {n: [namaste], d: [doston], c: [chai, chai, chai], l: [lao], a: [aur], p: [poha], g: [garam], h: [hai]}
cache after three ??= writes: {chai: uploaded 2 s ago, poha: uploaded now}

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run word_count.dart

The sentence has chai three times and seven other words once each. update collected it into {namaste: 1, doston: 1, chai: 3, lao: 1, aur: 1, poha: 1, garam: 1, hai: 1}, with the keys in the order they first appeared. counts['english'] returned null without an exception, so the next line could compare it. The grouping step used putIfAbsent to make the list once and .add to extend it, so the chai group grew to three. The cache with ??= printed {chai: uploaded 2 s ago, poha: uploaded now}: the second write to chai did nothing, because the slot was no longer null. Step through the first words to see what update writes on each one:

Interactive · step by step One update call per word, from an empty map to chai 3 Step through the first words of the chai sentence as the map takes each one.
Counting words with update, one word at a time, step 6 of 6. Showing input word (done), counts map (chai 3, rest 1), slot read (2), what was written (update gives 3). In focus: counts map.updatelook upput backinput worddonecounts mapchai 3, rest 1slot read2what was writtenupdate gives 3

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Empty map. The map is empty. The loop takes its first word, namaste, and calls update('namaste', (c) => c + 1, ifAbsent: () => 1).
  2. Slot missing: run ifAbsent. update finds no entry for namaste, so it runs the ifAbsent builder once and stores 1 under namaste.
  3. Next word, same path. doston is also new. ifAbsent runs again and the counts map now holds two entries, in insertion order.
  4. chai arrives first time. The map still has no chai, so ifAbsent runs for it too. The counts map now holds three entries.
  5. chai again: slot is 1. The second chai finds a slot that holds 1. update calls the first callback, (c) => c + 1, and writes back 2.
  6. Loop ends. After every word, each new word ran ifAbsent once and only chai ran the update callback twice. chai holds 3; every other word holds 1.

Three slips are worth naming. counts[word]! + 1 writes “I promise this is not null” when the key is missing; Dart’s null check then throws, and the program ends. Reading counts[word] ?? 0 and writing back fixes it, but update with an ifAbsent reads better and runs the builder only when it has to. The second slip is a loop over counts.keys that reads counts[k] for every key: counts.entries gives you both at once, with pattern destructuring in Dart 3 that writes as for (final MapEntry(:key, :value) in counts.entries) and names what each piece is for.

A grouping function on grapheme cluster: What a reader sees as one character, as Unicode's UAX #29 defines it: one or more code points, such as स्ते or a family emoji. package:characters splits text into grapheme clusters. More in “Dart strings, interpolation and Unicode text”, so that अमित and Asha both land under one cluster each, lives in the strings and Unicode lesson. The exercise at the end of this lesson uses that reader-aware rule.

Trusting the result of a lookup

final n = counts[word]!; return n + 1; throws “Null check operator used on a null value” the first time any new word arrives. Prefer update(word, (c) => c + 1, ifAbsent: () => 1), which makes both branches part of one call.

A record makes a map key by value, not by identity

Every Map looks up a key in two steps. It asks the key for its hash code: The integer every Dart object returns from its hashCode property, used by hash-based sets and maps to pick a bucket for the value. Two objects that compare equal with == must return the same hash code., which chooses a bucket, and then compares the keys in that bucket with == to find a match. Both come from the key’s class. String, int and other value types override both, so two separate String objects with the same characters are one map key. List overrides neither, so two separate List objects, even with the same elements, are two map keys. A List makes a poor key, as the list keys cache answer in the operators module shows.

A record is the easy fix in Dart 3: a record: An anonymous, immutable value that bundles a fixed set of positional and named fields, each with its own type. Records need no declaration and compare equal when their shape and fields are equal. More in “Records in Dart: several values without a class” defines == and hashCode from its shape and its field values, so (row: 'B', seat: 12) built again elsewhere is the same key as the one you stored under. A seat map keyed by (row: String, seat: int) carries two columns of a booking table in the type system; no | character to split a composite string key, no accidental collision between row B seat 12 and row B1 seat 2.

seat_map.dart: records as map keys, and the identical trap Dart · seat_map.dart
void main() {
  // A seat is a record of a row letter and a number. Records compare by shape and value,
  // so two separately built keys with the same row and seat are the same map key.
  final booked = <({String row, int seat}), String>{};
  booked[(row: 'B', seat: 12)] = 'Asha';
  booked[(row: 'B', seat: 13)] = 'Ravi';
  booked[(row: 'C', seat: 1)] = 'Meera';

  print('seat B12 holds: ${booked[(row: 'B', seat: 12)]}');
  print('seat C7 holds: ${booked[(row: 'C', seat: 7)]}');

  final k1 = (row: 'B', seat: 12);
  final k2 = (row: 'B', seat: 12);
  print('two keys built separately, k1 == k2: ${k1 == k2}');
  print('hashCodes equal: ${k1.hashCode == k2.hashCode}');
  print('identical(k1, k2): ${identical(k1, k2)}');

  // Entries destructure with the pattern MapEntry(:key, :value).
  print('every booked seat:');
  for (final MapEntry(:key, :value) in booked.entries) {
    print('  ${key.row}${key.seat} -> $value');
  }

  // A List is a poor key, because list == is identity, not contents.
  final listKeyed = <List<int>, String>{};
  listKeyed[[2, 12]] = 'Asha';
  print('listKeyed[[2, 12]] on a fresh list: ${listKeyed[[2, 12]]}');
}

Output

seat B12 holds: Asha
seat C7 holds: null
two keys built separately, k1 == k2: true
hashCodes equal: true
identical(k1, k2): false
every booked seat:
  B12 -> Asha
  B13 -> Ravi
  C1 -> Meera
listKeyed[[2, 12]] on a fresh list: null

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run seat_map.dart

The lookup for (row: 'B', seat: 12) found Asha, and the lookup for an unbooked seat returned null. Two keys built in separate expressions were == with equal hash codes, and identical on them was still false: the records are two separate objects, and the map doesn’t care, because it reads only hashCode and ==. Iteration printed each MapEntry: The pair of a key and its value that a Map iterates with map.entries. Dart 3's pattern MapEntry(:key, :value) destructures each entry into two named variables in one line. in insertion order, and the destructuring pattern MapEntry(:key, :value) unpacked each entry’s two sides in one step.

The last line is the identity trap. listKeyed[[2, 12]] = 'Asha' wrote under one list, and listKeyed[[2, 12]] read from another list of the same contents. The second list has a different hashCode, lands in a different bucket, and the lookup returns null. A record, or a small class with final fields and overridden == and hashCode, prevents the whole class of bugs the identity default hides.

Build hash codes with Object.hash

A small class with two or more fields can define its hash as Object.hash(a, b, c), which xors field hashes without the common pitfalls of writing one by hand; override == next to it, with the same fields in the same order.

Checkpoint

Check: Building counts and groups · Keys compare with == and hashCode

  1. Question 1 of 2 A loop counts a sentence into counts, a Map<String, int>, with 'counts.update(word, (c) => c + 1, ifAbsent: () => 1);'. For the word 'chai', seen for the third time, which callback runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The first one, (c) => c + 1, which reads 2 and writes back 3

    update tries to update first; it falls through to ifAbsent only on a missing key. That is how word_count.dart ran its first callback twice for chai and its ifAbsent builder once per new word.

  2. Question 2 of 2 A booked seats map is 'Map<(String row, int seat), String>'. The code 'booked[(row: "B", seat: 12)] = "Asha"; print(booked[(row: "B", seat: 12)]);' is run. What is printed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Asha

    The lookup in seat_map.dart prints Asha for exactly this reason. Compare with a List key, which is identity-based and would miss the second lookup.

Three map flavours, with the order each one keeps

The default Map the brace literal gives you is a hash-based map: A map that finds a key in two steps: it reads the key's hashCode to choose a bucket and compares the keys in that bucket with ==. LinkedHashMap and HashMap in dart:collection are both hash-based. that happens to keep insertion order on the side: a LinkedHashMap, in dart:collection. Three concrete classes sit under the Map interface, and the choice is about iteration order and about what the key type is:

  • LinkedHashMap tracks a linked list of entries in the order they were first written, so iteration follows that order. The default {} literal and the Map constructor both give you one.
  • HashMap keeps no such list, so iteration goes in whatever order the hash buckets happen to lie in.
  • SplayTreeMap is a self-balancing binary search tree keyed by compareTo, so iteration gives the keys in sorted order and firstKey and lastKey return the smallest and the largest in O(log n).

Run the same five inserts through each of them:

flavours.dart: three map flavours over the same five inserts Dart · flavours.dart
import 'dart:collection';

void main() {
  // Five store codes added in a fixed order, so the three flavours can be compared.
  const added = ['pune', 'mumbai', 'bengaluru', 'delhi', 'chennai'];
  const sales = {
    'pune': 120,
    'mumbai': 400,
    'bengaluru': 250,
    'delhi': 980,
    'chennai': 180,
  };

  // The default Map literal is a LinkedHashMap: iteration keeps insertion order.
  final linked = <String, int>{};
  for (final city in added) {
    linked[city] = sales[city]!;
  }
  print('LinkedHashMap keys: ${linked.keys.toList()}');

  // HashMap gives no order guarantee.
  final hashed = HashMap<String, int>();
  for (final city in added) {
    hashed[city] = sales[city]!;
  }
  print('HashMap keys: ${hashed.keys.toList()}');

  // SplayTreeMap keeps the keys sorted, by the natural order of String.
  final sorted = SplayTreeMap<String, int>();
  for (final city in added) {
    sorted[city] = sales[city]!;
  }
  print('SplayTreeMap keys: ${sorted.keys.toList()}');

  // The same five entries, three orderings.
  print('added order:   $added');
  print('linked first:  ${linked.keys.first}');
  print('sorted first:  ${sorted.keys.first}');
  print('sorted last:   ${sorted.keys.last}');
}

Output

LinkedHashMap keys: [pune, mumbai, bengaluru, delhi, chennai]
HashMap keys: [chennai, mumbai, delhi, pune, bengaluru]
SplayTreeMap keys: [bengaluru, chennai, delhi, mumbai, pune]
added order:   [pune, mumbai, bengaluru, delhi, chennai]
linked first:  pune
sorted first:  bengaluru
sorted last:   pune

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run flavours.dart

The LinkedHashMap’s keys came out [pune, mumbai, bengaluru, delhi, chennai], the order the loop added them in. The HashMap’s keys came out [chennai, mumbai, delhi, pune, bengaluru], which looks like no order on this page; nothing in the API promises any particular order, and the class documentation adds that iteration order can change between SDK versions, so code that relies on this run’s output will quietly go wrong one day. The SplayTreeMap’s keys came out [bengaluru, chennai, delhi, mumbai, pune], which is String.compareTo order, so it starts with bengaluru and ends with pune in every run.

A fourth flavour serves a different question. Queue and its default class FIFO queue: A collection that serves items first-in, first-out: additions go at the back and removes come from the front. dart:collection's Queue and its ListQueue class work that way, with amortised O(1) at both ends. from dart:collection are not maps at all; they serve items in first-in, first-out order, as the next section shows. Decide between the four by answering two questions about iteration order and the main operation. The table rules out the three that do not fit:

Interactive · answer and compare Which collection fits the task? Answer two questions about what you need. The table marks what fits and says why the others do not.
What order do you need when you iterate?
What is the main operation?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four types from dart:collection against iteration order, lookup, extremes and FIFO work
Type Lookup by key Iteration order Smallest or largest key FIFO work
LinkedHashMap Iteration order: insertion order Good O(1) average, like any hash-based map Good insertion order, documented Poor needs a full scan with reduce Poor no removeFirst on a Map
HashMap Iteration order: no guaranteed order Good O(1) average Poor no promise on iteration order Poor needs a full scan with reduce Poor no removeFirst on a Map
SplayTreeMap Iteration order: keys sorted by compareTo Mixed O(log n) average on a balanced tree Good keys sorted by compareTo Good firstKey and lastKey O(log n) Poor no removeFirst on a Map
ListQueue Iteration order: insertion order, both ends Poor no key lookup; items only Good insertion order, add at either end Poor no key compared on insert Good removeFirst O(1), as a ListQueue promises
How the answers decide
  • “What order do you need when you iterate?” The order I added keys in and “What is the main operation?” Lookup by key: rules out HashMap, SplayTreeMap and ListQueue. A LinkedHashMap is the default map literal: insertion order plus hash-based lookup, which the other three cannot all give.
  • “What order do you need when you iterate?” Sorted by the key value: rules out LinkedHashMap, HashMap and ListQueue. Only a SplayTreeMap keeps keys sorted, so sorted iteration and firstKey or lastKey are O(log n), not a scan.
  • “What is the main operation?” Read the smallest or largest key: rules out LinkedHashMap, HashMap and ListQueue. A SplayTreeMap tracks the smallest and largest key as it inserts, so those reads are O(log n); the other three need a scan.
  • “What is the main operation?” Take items in first-in, first-out order: rules out LinkedHashMap, HashMap and SplayTreeMap. A ListQueue serves items in the order they arrived in amortised O(1); maps give lookup by key, not FIFO work.
  • “What order do you need when you iterate?” Any order is fine and “What is the main operation?” Lookup by key: adds a caveat to HashMap. A HashMap drops the insertion-order memory and saves a little memory and work on insert; keep it when iteration order never matters.

Sets have the same three flavours, under the same names with Set where Map sits: LinkedHashSet is the default and keeps insertion order; HashSet keeps no order; a splay tree: A self-balancing binary search tree that moves a looked-up node to the root, so a key read often becomes cheap. SplayTreeMap and SplayTreeSet in dart:collection use one, keyed by compareTo. keeps the elements sorted. The choice is the same question read against values rather than keys.

Key idea

The default {} map is a LinkedHashMap that keeps insertion order: The order in which values were first added to a set or keys were first written to a map, which a LinkedHashSet and a LinkedHashMap replay when the collection is iterated.; pick a HashMap only when iteration order never matters, and a SplayTreeMap when you need the keys sorted or the smallest or largest key in O(log n).

A ListQueue serves items in the order they arrived

Many programs need a sequence that keeps order and lets you take from one end while adding to the other: a worker that processes uploads in the order they arrived, a scheduler that runs each pending job once. A Map is wrong for that work, because a map’s remove takes a key, not the oldest or the newest entry. dart:collection gives you Queue, an interface, and ListQueue, its default concrete class, built on a growable ring buffer so that addLast at the back and removeFirst at the front both run in amortised O(1) time.

fifo.dart: a ListQueue serves uploads in the order they arrived Dart · fifo.dart
import 'dart:collection';

void main() {
  // A ListQueue is a FIFO queue: addLast at the back, removeFirst from the front.
  final queue = ListQueue<String>();
  queue.addLast('upload-01.jpg');
  queue.addLast('upload-02.jpg');
  queue.addLast('upload-03.jpg');

  print('queue: $queue');
  print('length: ${queue.length}');
  print('next in line: ${queue.first}');

  while (queue.isNotEmpty) {
    final next = queue.removeFirst();
    print('processing $next');
  }
  print('queue is empty: ${queue.isEmpty}');

  // Both ends are supported, so a ListQueue is also a deque.
  final deque = ListQueue<int>();
  deque.addLast(2);
  deque.addLast(3);
  deque.addFirst(1);
  deque.addLast(4);
  print('deque after addFirst(1) and three addLasts: $deque');
  print('removeFirst: ${deque.removeFirst()}');
  print('removeLast: ${deque.removeLast()}');
  print('remaining: $deque');
}

Output

queue: {upload-01.jpg, upload-02.jpg, upload-03.jpg}
length: 3
next in line: upload-01.jpg
processing upload-01.jpg
processing upload-02.jpg
processing upload-03.jpg
queue is empty: true
deque after addFirst(1) and three addLasts: {1, 2, 3, 4}
removeFirst: 1
removeLast: 4
remaining: {2, 3}

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run fifo.dart

Three addLast calls put three uploads in the queue, and the while loop processed them as upload-01.jpg, upload-02.jpg, upload-03.jpg, oldest first. A ListQueue is also a deque: it accepts addFirst, removeLast and random reads, which the deque test did by putting 1 at the front, three items at the back, and ending with {2, 3} after a removeFirst and a removeLast. The removeFirst returned 1, the oldest, and removeLast returned 4, the newest, which is why only 2, 3 were left.

A Map has one more read-only form worth knowing: Map.unmodifiableOf(source), new in Dart 3.13, copies the entries of source into a map that refuses writes, with better typing than the older Map.unmodifiable. A short API that returns a Map and does not want callers to change it hands out an unmodifiableOf copy; the caller still reads it the same way.

Try it

To take distinct values out of a list of lines without writing a single line of code, paste them into the duplicate line remover; to see which values a column of a CSV holds and how many times each one appears, paste the file into the CSV value counter.

Checkpoint

Check: Three map flavours and their orders · FIFO work with a ListQueue

  1. Question 1 of 2 A log of events keeps the order each event arrived in and reads a specific event id in O(1) on average. Which Map from dart:collection fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: LinkedHashMap

    The default {} literal is already a LinkedHashMap, so a program that needs this shape has to write nothing special. HashMap trades insertion order for a slightly lower per-entry cost, and SplayTreeMap exists for the cases where sorted iteration or extremes matter.

  2. Question 2 of 2 A ListQueue<int> is empty. After addLast(2), addLast(3), addFirst(1), addLast(4), removeFirst() and removeLast(), what is the first item of the queue?

    Type a number.

    A hint

    Draw the queue as a line and move the ends in and out one by one.

    Show the answer: Question 2 of 2

    Answer: 2

    Build it up: addLast 2 and 3 gives [2, 3]; addFirst 1 gives [1, 2, 3]; addLast 4 gives [1, 2, 3, 4]. removeFirst drops 1 for [2, 3, 4]; removeLast drops 4 for [2, 3]. The first item is 2, as the deque trace of fifo.dart prints.

Practise: group by first grapheme and find common skills

Make a console project with dart create -t console map_lab and work in bin/map_lab.dart. Add dart pub add characters so the first function can be reader-aware. Then write two functions, each one tested on your own small inputs before you move on:

  1. Map<String, List<String>> groupByInitial(List<String> names) groups names by the first grapheme cluster of each name, so ['Asha Rao', 'अमित शाह', 'ananya'] gives {A: [Asha Rao, ananya], अ: [अमित शाह]} when the match is case-insensitive for Latin letters. Use putIfAbsent to make the inner list once per group, and read the first cluster through name.characters.first so that अमित groups under अ, not under one of its code units.
  2. Set<String> commonSkills(List<Set<String>> teams) returns the skills every team has, with an empty set when the list of teams is empty. Start from the first team as the seed, fold the rest with intersection, and read the first-team-missing test as a special case before the loop.

Make a console project with dart create -t console map_lab, add dart pub add characters, and check each function against the examples above, then against an empty list of names and an empty list of teams. When both pass, work through the self-check. The idea you are least sure of comes first, and every choice explains itself.

Practice that adapts to you

Practise this lesson

14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Which literal gives you a Set<String> you can add to?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: <String>{}

    Either a type argument on the literal, as in <String>{}, or a context type, as in final Set<String> tags = {}, is enough; the second works because the variable's declared type tells the compiler what the empty literal is.

  2. Question 2 of 14 How fast is a Set<String>.contains(value) on average?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: O(1)

    The hash-based set in dart:collection, which the default literal gives you, runs contains and add in O(1) on average per call. For membership tests that run inside a loop, this is often why reaching for a Set beats a List.

  3. Question 3 of 14 What does this program print?

    Read the code, then choose one answer.

    final backend = {'Dart', 'Postgres', 'Docker'};
    print(backend.containsAll({'Dart', 'HTTP'}));
    Show the answer: Question 3 of 14

    Answer: false

    containsAll reads as "does this set hold every value of that one?" and gives a bool. For the two values that are shared, use intersection, which returns the set {Dart, HTTP} on the lesson's teams.

  4. Question 4 of 14 counts is an empty Map<String, int>. What does 'counts.update("new", (c) => c + 1, ifAbsent: () => 1);' leave counts as?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: {new: 1}

    update's rule for a missing key is "run the ifAbsent builder, store what it returns, and skip the first callback". The run of word_count.dart does this once for each new word.

  5. Question 5 of 14 'byLetter.putIfAbsent("c", () => <String>[]).add("chai");' is called twice in a row on an empty map. What is in byLetter afterwards?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: {c: [chai, chai]}

    putIfAbsent returns the stored value, so a chained method call works on both branches. The grouping line in word_count.dart runs on this shape, which is why chai, chai, chai land in one group of three.

  6. Question 6 of 14 What does this program print?

    Read the code, then choose one answer.

    final m = {'a': 1};
    print(m['b']);
    Show the answer: Question 6 of 14

    Answer: null

    Every Map lookup returns a nullable value, so counts["english"] printed null in word_count.dart without an exception. Pair it with ?? 0 or ??= when you need a default.

  7. Question 7 of 14 'cache[[storeId, itemId]] = price;' uses a List as a map key. A later request builds a fresh [storeId, itemId] and reads 'cache[[storeId, itemId]]'. Why does the lookup return null, although the ids are equal?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: List does not override == or hashCode, so the two lists are two different keys

    Switch the key to a record, '(storeId, itemId)', or a small class with final fields that overrides == and hashCode. That is why the lists lesson's "list keys cache" question maps onto this concept.

  8. Question 8 of 14 What does the HashMap documentation promise about iteration order?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: Nothing: iteration order can be any order, and may change between SDK versions

    The run of flavours.dart printed [chennai, mumbai, delhi, pune, bengaluru], which looks like no order a reader would expect. Nothing promises that run reproduces either; a HashMap trades iteration order for the lower per-entry cost of not maintaining the linked list.

  9. Question 9 of 14 What does this program print?

    Read the code, then choose one answer.

    final m = SplayTreeMap<String, int>();
    m['pune'] = 1;
    m['delhi'] = 2;
    m['chennai'] = 3;
    print(m.firstKey());
    Show the answer: Question 9 of 14

    Answer: chennai

    A SplayTreeMap's keys came out as [bengaluru, chennai, delhi, mumbai, pune] in flavours.dart, so firstKey and lastKey are O(log n) reads of the extremes.

  10. Question 10 of 14 A ListQueue<String> q is empty. q.addLast("a"); q.addLast("b"); q.addLast("c"); What does q.removeFirst() return?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: a

    fifo.dart ran this exact pattern on upload-01, upload-02 and upload-03 and processed them in that order.

  11. Question 11 of 14 A background worker queues 10,000 small jobs and processes them in order. Which collection fits, and why not a plain List?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: A ListQueue, because its removeFirst is amortised O(1); a List's removeAt(0) is O(n)

    A ListQueue is backed by a growable ring buffer, as the API page says, so removing from the front is amortised O(1); a plain List''s removeAt(0) is O(n), which turns a 10,000-job loop into about 50 million moves.

  12. Question 12 of 14 You need an empty Set<String> as a field with no initialiser value in sight: which declaration gives one?

    Choose every answer that is right.

    Show the answer: Question 12 of 14

    Answer:

    • Set<String> tags = {};
    • final tags = <String>{};

    Braces alone mean Map. Either write the type argument on the literal (<String>{}) or give the declaration a Set<String> target; both appear in the lesson's table.

  13. Question 13 of 14 seat_map.dart looked up (row: 'B', seat: 12) with a record built separately from the one it stored. What came back?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: Asha, because equal records have equal hashCodes

    Records have structural equality, so a freshly built (row: 'B', seat: 12) hashes to the same bucket and compares equal to the stored key. A List key, built the same way twice, returned null in the same run.

  14. Question 14 of 14 Which map did flavours.dart print in the order pune, mumbai, bengaluru, delhi, chennai, the order its keys were added?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: LinkedHashMap, the default a map literal gives

    A map literal and Map() both give a LinkedHashMap, whose iteration follows insertion order. HashMap makes no promise, and SplayTreeMap keeps its keys sorted.

Interview questions

Warm-up (fresher): Why does var x = {} print Map for runtimeType, and how do you write an empty set?

Dart decides which type a literal gives in three rules: a brace literal with key: value pairs is a Map, a brace literal of plain values is a Set, and an empty brace literal is a Map. The empty case kept its meaning from Dart 1, so that older code that wrote {} for an empty map did not quietly change type when set literals arrived in Dart 2.2. For an empty set, say what the elements are, either with a type argument on the literal, <String>{}, or with a context that names the type, as in final Set<String> tags = {}. The compiler then infers a set and the analyzer accepts tags.add('offer'). The same two tools fix a non-empty literal whose type cannot be inferred, such as a heterogenous <Object>{...}.

Two deeper questions in the module’s bank build on the measurements of this lesson. Think about what you would try first on each, then open the discussion for the model answer:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A background job counts how many times each error code appears in a day's logs and groups the lines by code. The first version crashes with "Null check operator used on a null value". Rewrite the counting and the grouping so no branch of the loop reads a null, and say what tests you would add.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A dashboard reads events out of a Map by event id and shows them in a timeline. On the next version a product manager asks that the oldest event appear at the top. The code works fine locally. In staging the order is random on every reload. What is going on, and how would you fix it?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What type does var x = {} have, and how do you write an empty set?

    An empty pair of braces is a Map. Write <String>{} or give a context type, as in final Set<String> tags = {}; braces with plain values make a Set and braces with key: value pairs a Map.

  2. What do Dart sets offer for membership questions, and in what order do they iterate?

    union, intersection, difference and containsAll, at O(1) on average per element. The default LinkedHashSet iterates in the order each value was first added.

  3. What does reading a missing key from a map return, and how do you count without a missing-key branch?

    null. update(key, (c) => c + 1, ifAbsent: () => 1), putIfAbsent or cache[key] ??= v keep "look up, bump or make one" in one call; counts[word]! throws on the first new word.

  4. Why do records make good composite map keys while a List key does not?

    A map finds a key by hashCode and ==. Records have value-based == and hashCode, so (row: 'B', seat: 12) built later finds the same entry; two look-alike lists are two different keys.

  5. How should a small class with several fields define its hashCode?

    With Object.hash over the fields, and an == override next to it that uses the same fields in the same order.

  6. How do LinkedHashMap, HashMap and SplayTreeMap order their keys?

    The default {} map is a LinkedHashMap and keeps insertion order; a HashMap promises no order; a SplayTreeMap keeps the keys sorted by compareTo and finds the smallest or largest in O(log n).

  7. What is a ListQueue from dart:collection?

    A FIFO queue and a deque: addLast and removeFirst run in amortised O(1), so items are served in the order they arrived.

Key takeaways

  • An empty {} is a Map, a brace literal with pairs is a Map and a brace literal of plain values is a Set; write <String>{} or a context type to make a set.
  • A Set runs union, intersection, difference and containsAll in O(1) on average per element, and the default LinkedHashSet keeps iteration in the order each value was first added.
  • Reading a missing key returns null; use update(key, ..., ifAbsent: ...), putIfAbsent or cache[key] ??= v to keep the “lookup, bump or make one” shape in one call.
  • A map looks up a key by hashCode and ==, so records, with their value-based == and hashCode, make good composite keys, while a List key makes two look-alike lists two different keys.
  • The default {} literal is a LinkedHashMap and keeps insertion order; a HashMap promises no order and a SplayTreeMap keeps the keys sorted by compareTo.
  • dart:collection’s ListQueue is a FIFO queue and a deque: addLast and removeFirst run in amortised O(1), and the three map flavours have the same sibling sets under LinkedHashSet, HashSet and SplayTreeSet.

Terms in this lesson

FIFO queue
A collection that serves items first-in, first-out: additions go at the back and removes come from the front. dart:collection's Queue and its ListQueue class work that way, with amortised O(1) at both ends.
hash code
The integer every Dart object returns from its hashCode property, used by hash-based sets and maps to pick a bucket for the value. Two objects that compare equal with == must return the same hash code.
hash-based map
A map that finds a key in two steps: it reads the key's hashCode to choose a bucket and compares the keys in that bucket with ==. LinkedHashMap and HashMap in dart:collection are both hash-based.
insertion order
The order in which values were first added to a set or keys were first written to a map, which a LinkedHashSet and a LinkedHashMap replay when the collection is iterated.
MapEntry
The pair of a key and its value that a Map iterates with map.entries. Dart 3's pattern MapEntry(:key, :value) destructures each entry into two named variables in one line.
splay tree
A self-balancing binary search tree that moves a looked-up node to the root, so a key read often becomes cheap. SplayTreeMap and SplayTreeSet in dart:collection use one, keyed by compareTo.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.