skip to content

When counting word frequencies in a Dart Map<String, int>, how do update, putIfAbsent and the [] operator differ?

level: middleimportance: must knowfreq 50%

answer

  1. [] returns a nullable value
  2. counts[word]++ does not compile
  3. update throws without ifAbsent
  4. putIfAbsent returns the stored value
  5. get-or-create for grouping

basics

~20 s

In a Dart map, [] returns a nullable value, so counting needs counts[w] = (counts[w] ?? 0) + 1. update(w, (n) => n + 1, ifAbsent: () => 1) does it in one call; putIfAbsent returns the stored value and suits get-or-create grouping.

solid answer

~40 s

`counts[word]` is typed `int?` because the key may be missing, so `counts[word]++` does not compile and the index-operator version needs `counts[word] = (counts[word] ?? 0) + 1`, with two lookups. `update(word, (n) => n + 1, ifAbsent: () => 1)` expresses the increment in one call and returns the new value; without `ifAbsent` it throws `ArgumentError` for a missing key, which is useful when absence is a bug. `putIfAbsent(key, () => value)` leaves an existing value alone and returns whatever is stored after the call, which makes it the idiom for grouping into mutable containers: `byInitial.putIfAbsent(w[0], () => []).add(w)`. To read the result, iterate `entries` as `MapEntry` pairs, and never add or remove keys while iterating — that throws `ConcurrentModificationError`.

code

dart · 19 lines
dart
Map<String, int> wordFrequencies(String text) {
  final counts = <String, int>{};
  for (final raw in text.split(RegExp(r'\s+'))) {
    final word = raw.toLowerCase();
    if (word.isEmpty) continue;
    counts.update(word, (n) => n + 1, ifAbsent: () => 1);
  }
  return counts;
}

void main() {
  final counts = wordFrequencies('To be or not to be');
  print(counts); // {to: 2, be: 2, or: 1, not: 1}

  final repeated = Map.fromEntries(
    counts.entries.where((e) => e.value > 1),
  );
  print(repeated); // {to: 2, be: 2}
}

go deeper

for a junior

Know that map[key] can return null and write a correct increment with ?? or update plus ifAbsent.

for a middle

Explain what update and putIfAbsent return and throw, when each fits, and how entries support filtering and rebuilding a map.

for a senior

Spot double lookups, key normalisation bugs and modification during iteration in review, and choose throwing update where a missing key signals bad data.

for a principal

Encourage small, named helpers for recurring aggregation patterns so the map-update idiom is consistent across a codebase.

## The task A text-analysis tool counts how often each word appears. The natural container is a `Map<String, int>`, and the interesting part is the single line that increments a count. Dart offers several ways to write it, and they differ in how many lookups they do, what they return and how they fail. ## The four idioms ```dart final counts = <String, int>{}; // 1. update with ifAbsent: one call, handles both cases. counts.update(word, (n) => n + 1, ifAbsent: () => 1); // 2. Index operators with a null fallback. counts[word] = (counts[word] ?? 0) + 1; // 3. putIfAbsent, then increment. counts.putIfAbsent(word, () => 0); counts[word] = counts[word]! + 1; // 4. Does not compile: counts[word] is int?, so ++ is not allowed. // counts[word]++; ``` | Method | Key present | Key absent | Returns | |---|---|---|---| | `map[key]` | the value | `null` | `V?` | | `map[key] = v` | replaces the value | adds the entry | `v`, as any assignment expression | | `update(key, f)` | stores `f(old)` | **throws `ArgumentError`** | the new value | | `update(key, f, ifAbsent: g)` | stores `f(old)` | stores `g()` | the new value | | `putIfAbsent(key, g)` | leaves it alone | stores `g()` | the value now stored | ## Why the [] operator is nullable `Map.operator []` is declared as returning `V?`. With sound null safety, even a `Map<String, int>` returns `int?` from `counts[word]`, because the key might be missing. That is why `counts[word]++` and `counts[word] += 1` fail to compile, and why idiom 2 needs `?? 0`. The same nullability means `[]` cannot tell "missing" from "present with a null value" in a map whose value type is nullable; use `containsKey` when that distinction matters. ## update: transform a value `update` is the right tool when the stored value is **replaced** by a new value computed from the old one — counts, sums, maximums. Without `ifAbsent` it assumes the key exists and throws `ArgumentError` (*Key not in map.*) when it does not, which is useful when a missing key really is a bug. With `ifAbsent` it becomes the one-call increment in idiom 1. ## putIfAbsent: get or create `putIfAbsent` returns the value that is **in the map after the call** — the existing one, or the one `ifAbsent` just created. That makes it the idiom for **grouping**, where the value is a mutable container you add to in place: ```dart final byInitial = <String, List<String>>{}; for (final word in words) { byInitial.putIfAbsent(word[0], () => []).add(word); } ``` For counters, `putIfAbsent` is clumsy (idiom 3 needs a second lookup and a `!`), because an `int` cannot be modified in place. ## Reading the result with entries `map.entries` is an `Iterable<MapEntry<K, V>>`, visiting keys in insertion order for the default map. It is the way to process key and value together and the input for sorting or rebuilding: - `counts.entries.where((e) => e.value > 1)` — words seen more than once; - `Map.fromEntries(...)` / `addEntries(...)` — rebuild a map from filtered or sorted entries; - `for (final MapEntry(:key, :value) in counts.entries)` — destructure each entry. ## Pitfalls worth naming 1. **Mutating keys during iteration.** Calling `update` with a new key or `remove` while looping over `counts.keys` or `counts.entries` changes the key set and throws `ConcurrentModificationError`; updating an existing key's value is fine. 2. **Side effects in `ifAbsent`.** It runs only when the key is missing, so do not put logging or I/O there expecting it on every call. 3. **Normalising keys.** `'The'` and `'the'` are different `String` keys; lowercase and trim before counting, or the map silently splits one word into several. 4. **Double lookups.** Idioms 2 and 3 look the key up twice; `update` with `ifAbsent` expresses the intent in one call. ## Interview summary - `[]` returns `V?`, so increments need `?? 0` or `update`; `counts[word]++` does not compile. - `update` **replaces** a value computed from the old one and throws `ArgumentError` for a missing key unless `ifAbsent` is given. - `putIfAbsent` **gets or creates** and returns the stored value, which is ideal for grouping into lists or sets. - `entries`, `Map.fromEntries` and `addEntries` process and rebuild maps as key/value pairs. - Never add or remove keys while iterating; collect first or use `removeWhere`.

  • What happens in Dart when you call counts.update('zebra', (n) => n + 1) and 'zebra' is not a key?
    It throws an `ArgumentError` (the message says the key is not in the map), because `update` without `ifAbsent` has no value to start from. Pass `ifAbsent: () => 1` to insert, or keep the throw deliberately when a missing key means corrupted input.
  • Why is putIfAbsent a better fit than update for grouping words by first letter in Dart?
    The group is a mutable `List`, so you want the existing list back to add to it. `putIfAbsent` returns the stored value, creating an empty list only the first time: `byInitial.putIfAbsent(w[0], () => []).add(w)`. `update` is for replacing a value, like an `int` count.
  • How do you tell a missing key from a key mapped to null in a Dart Map<String, int?>?
    `map[key]` returns `null` in both cases, so it cannot distinguish them. Use `containsKey(key)`, or `putIfAbsent` when you want to insert only if the key is absent.

update is a tally clerk who crosses out the old number and writes the next one, while putIfAbsent is a clerk who hands you the existing folder, or a new empty one, so you can drop papers in it yourself.

saying these in an interview costs you the question

  • counts[word]++ works on a Map<String, int>.
  • update inserts the key automatically when it is missing.
  • putIfAbsent overwrites the existing value.
  • map[key] throws when the key is missing.
  • Adding keys inside a loop over entries is safe.