ZetCode

Dart HashMap

last modified April 4, 2025

In Dart, HashMap is an unordered collection of key-value pairs. It provides fast lookup, addition, and deletion of entries based on keys.

HashMap implements the Map interface and uses hash tables for storage. Keys must have consistent Object.== and Object.hashCode implementations.

Creating a HashMap

The simplest way to create a HashMap is using the constructor.

main.dart
import 'dart:collection';

void main() {
  var capitals = HashMap<String, String>();
  capitals['USA'] = 'Washington';
  capitals['Japan'] = 'Tokyo';
  capitals['France'] = 'Paris';

  print(capitals);
}

We create a HashMap of country-capital pairs. The generic types specify that both keys and values are Strings. We add entries using the [] operator.

$ dart main.dart
{USA: Washington, France: Paris, Japan: Tokyo}

HashMap vs Other Map Types

Dart's dart:collection library provides three concrete Map implementations. All share the same Map<K,V> interface; the difference lies in key ordering and performance trade-offs.

Type Key order Lookup / insert Best for
HashMap None (unordered) O(1) amortized Fast lookups when order is irrelevant
LinkedHashMap Insertion order O(1) amortized Default {} literal; predictable iteration
SplayTreeMap Sort order O(log n) Sorted keys, range queries
main.dart
import 'dart:collection';

void main() {
  var data = {'c': 3, 'a': 1, 'b': 2};

  // Default {} literal is a LinkedHashMap — insertion order preserved
  var linked = LinkedHashMap.of(data);
  print('LinkedHashMap: $linked'); // {c: 3, a: 1, b: 2}

  // HashMap — no ordering guarantee
  var hash = HashMap.of(data);
  print('HashMap: $hash'); // order may vary

  // SplayTreeMap — keys sorted alphabetically
  var splay = SplayTreeMap.of(data);
  print('SplayTreeMap: $splay'); // {a: 1, b: 2, c: 3}
}

Use HashMap when you never need to iterate keys in a predictable order and raw speed is the priority. If insertion order matters, the default LinkedHashMap is the right choice. For sorted keys or range-based queries, reach for SplayTreeMap.

$ dart main.dart
LinkedHashMap: {c: 3, a: 1, b: 2}
HashMap: {b: 2, c: 3, a: 1}
SplayTreeMap: {a: 1, b: 2, c: 3}

HashMap from Iterables

HashMap.fromIterables builds a map from two parallel iterables — one supplying the keys and the other the values. Both iterables must have the same length.

main.dart
import 'dart:collection';

void main() {
  var countries = ['Slovakia', 'Germany', 'France'];
  var capitals = ['Bratislava', 'Berlin', 'Paris'];

  var capitalMap = HashMap.fromIterables(countries, capitals);
  print(capitalMap);
}

Each element of countries is paired with the element at the same position in capitals. Because HashMap is unordered, the printed order may differ from the insertion order.

$ dart main.dart
{France: Paris, Germany: Berlin, Slovakia: Bratislava}

Building Maps Idiomatically

Dart offers several concise patterns for constructing a HashMap in a single expression. Use collection-for to transform a list inline, or addEntries to populate a map from an iterable of MapEntry objects.

main.dart
import 'dart:collection';

void main() {
  var fruits = ['apple', 'banana', 'cherry'];

  // Collection-for produces a LinkedHashMap literal, then wrap in HashMap.of
  var lengths = HashMap.of({for (var f in fruits) f: f.length});
  print(lengths); // {cherry: 6, banana: 6, apple: 5}

  // addEntries: build from a lazily-transformed iterable
  var upper = HashMap<String, int>()
    ..addEntries(lengths.entries.map(
        (e) => MapEntry(e.key.toUpperCase(), e.value)));
  print(upper); // {CHERRY: 6, BANANA: 6, APPLE: 5}
}

The collection-for syntax ({for (var x in list) key: value}) is the most readable option when key and value are both derived from the same element. addEntries is useful when the source is an existing Iterable<MapEntry> such as the result of .entries.map(...).

$ dart main.dart
{cherry: 6, banana: 6, apple: 5}
{CHERRY: 6, BANANA: 6, APPLE: 5}

Checking Contents

HashMap provides methods to look up keys and values, and properties to inspect its size.

main.dart
import 'dart:collection';

void main() {
  var scores = HashMap<String, int>();
  scores['Alice'] = 90;
  scores['Bob'] = 85;
  scores['Charlie'] = 95;

  print(scores.containsKey('Alice')); // true
  print(scores.containsKey('Eve'));   // false
  print(scores.containsValue(85));    // true
  print(scores.isEmpty);              // false
  print(scores.isNotEmpty);           // true
  print(scores.length);               // 3
}

containsKey runs in O(1) time because it hashes the key directly. containsValue is O(n) — it scans all values — so prefer key-based lookups when possible.

$ dart main.dart
true
false
true
false
true
3

Updating Values

HashMap offers several ways to update values: update for modifying existing entries, update with ifAbsent for an upsert-style operation, and putIfAbsent to insert only when a key is missing.

main.dart
import 'dart:collection';

void main() {
  var inventory = HashMap<String, int>();
  inventory['apples'] = 5;
  inventory['oranges'] = 3;

  // Increment an existing value
  inventory.update('apples', (value) => value + 3);

  // Upsert: increment if present, insert with 5 if absent
  inventory.update('bananas', (value) => value + 5, ifAbsent: () => 5);

  // Insert only if the key does not yet exist
  inventory.putIfAbsent('grapes', () => 8);

  print(inventory);
}

update throws a StateError if the key is absent and no ifAbsent callback is provided. putIfAbsent is a read-then-write shorthand; it returns the existing value when the key is already present.

$ dart main.dart
{bananas: 5, grapes: 8, oranges: 3, apples: 8}

Iterating Over HashMap

A HashMap can be traversed via its keys, values, or entries collections.

main.dart
import 'dart:collection';

void main() {
  var colors = HashMap<String, String>();
  colors['red'] = '#FF0000';
  colors['green'] = '#00FF00';
  colors['blue'] = '#0000FF';

  print('Keys:');
  for (var key in colors.keys) {
    print(key);
  }

  print('\nValues:');
  for (var value in colors.values) {
    print(value);
  }

  print('\nEntries:');
  for (var entry in colors.entries) {
    print('${entry.key}: ${entry.value}');
  }
}

colors.entries yields MapEntry objects, giving structured access to both the key and the value in one step. Because HashMap does not preserve insertion order, the iteration order shown below may differ between runs.

$ dart main.dart
Keys:
red
green
blue

Values:
#FF0000
#00FF00
#0000FF

Entries:
red: #FF0000
green: #00FF00
blue: #0000FF

Concurrent Modification

Modifying a HashMap's structure (adding or removing keys) while iterating over it throws a ConcurrentModificationError. The two safe patterns are to snapshot the keys first, or to use removeWhere.

main.dart
import 'dart:collection';

void main() {
  var stock = HashMap.of({
    'apples': 5,
    'bananas': 0,
    'oranges': 3,
    'grapes': 0,
  });

  // WRONG — throws ConcurrentModificationError:
  // for (var key in stock.keys) {
  //   if (stock[key] == 0) stock.remove(key);
  // }

  // Safe: snapshot the keys first, then remove
  var toRemove = stock.keys.where((k) => stock[k] == 0).toList();
  toRemove.forEach(stock.remove);
  print('Snapshot removal: $stock');

  // Preferred: removeWhere handles the iteration safely
  var stock2 = HashMap.of(
      {'apples': 5, 'limes': 0, 'pears': 2, 'kiwis': 0});
  stock2.removeWhere((_, v) => v == 0);
  print('removeWhere: $stock2');
}

removeWhere is the idiomatic choice for predicate-based bulk removal. Updating a value in-place via map[key] = newValue is safe during iteration because it does not alter the map's structure.

$ dart main.dart
Snapshot removal: {oranges: 3, apples: 5}
removeWhere: {pears: 2, apples: 5}

Removing Elements

HashMap provides remove to delete a single entry by key, removeWhere for predicate-based bulk removal, and clear to discard all entries.

main.dart
import 'dart:collection';

void main() {
  var users = HashMap<int, String>();
  users[1] = 'Alice';
  users[2] = 'Bob';
  users[3] = 'Charlie';
  users[4] = 'David';

  print('Original: $users');

  // Remove by key
  users.remove(2);
  print('After remove: $users');

  // Remove where
  users.removeWhere((key, value) => key.isEven);
  print('After removeWhere: $users');

  // Clear all
  users.clear();
  print('After clear: $users');
}

remove returns the value that was removed, or null if the key was absent. removeWhere visits every entry, so it runs in O(n) time.

$ dart main.dart
Original: {1: Alice, 2: Bob, 3: Charlie, 4: David}
After remove: {1: Alice, 3: Charlie, 4: David}
After removeWhere: {1: Alice, 3: Charlie}
After clear: {}

HashMap with Custom Objects

Using a custom class as a HashMap key requires overriding both operator == and hashCode. Two objects that compare equal must produce the same hash code; otherwise the HashMap will treat them as different keys.

main.dart
import 'dart:collection';

class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Person && name == other.name && age == other.age;

  @override
  int get hashCode => Object.hash(name, age);

  @override
  String toString() => 'Person($name, $age)';
}

void main() {
  var people = HashMap<Person, String>();
  var p1 = Person('Alice', 30);
  var p2 = Person('Bob', 25);
  var p3 = Person('Alice', 30); // logically equal to p1

  people[p1] = 'Engineer';
  people[p2] = 'Doctor';
  people[p3] = 'Manager'; // overwrites p1's entry

  print(people);
  print('p1 and p3 are the same key: ${people[p1] == people[p3]}');
}

p3 is a distinct object from p1, but they compare equal and share the same hash code, so people[p3] = 'Manager' overwrites the earlier 'Engineer' entry. The map ends up with only two entries. Object.hash is the idiomatic way to combine multiple fields into a single hash code.

$ dart main.dart
{Person(Bob, 25): Doctor, Person(Alice, 30): Manager}
p1 and p3 are the same key: true

Custom Equality and Hashing

The HashMap constructor accepts optional equals and hashCode callbacks, letting you control how keys are compared without modifying the key class itself. A common use case is case-insensitive string keys such as HTTP header names.

main.dart
import 'dart:collection';

void main() {
  var headers = HashMap<String, String>(
    equals: (a, b) => a.toLowerCase() == b.toLowerCase(),
    hashCode: (s) => s.toLowerCase().hashCode,
  );

  headers['Content-Type'] = 'application/json';
  headers['Authorization'] = 'Bearer token123';

  print(headers['content-type']);          // application/json
  print(headers['CONTENT-TYPE']);          // application/json
  print(headers.containsKey('authorization')); // true
}

The equals and hashCode callbacks must be consistent: any two keys considered equal by equals must return the same value from hashCode. Violating this contract causes silent data loss — entries may become permanently unreachable inside the map.

$ dart main.dart
application/json
application/json
true

Identity HashMap

HashMap.identity() compares keys by object identity (identical()) instead of ==. Two objects that are logically equal but are distinct instances are stored as separate keys. This is useful when you need to associate data with a specific object reference — for example, caching per-instance metadata.

main.dart
import 'dart:collection';

class Tag {
  final String label;
  Tag(this.label);

  @override
  bool operator ==(Object other) => other is Tag && other.label == label;

  @override
  int get hashCode => label.hashCode;

  @override
  String toString() => 'Tag($label)';
}

void main() {
  var a = Tag('dart');
  var b = Tag('dart'); // logically equal, different instance

  // Default HashMap uses == — a and b collapse to one entry
  var byEquality = HashMap<Tag, int>();
  byEquality[a] = 1;
  byEquality[b] = 2; // overwrites a's entry
  print('Equality map entries: ${byEquality.length}'); // 1

  // Identity HashMap uses identical() — a and b are separate keys
  var byIdentity = HashMap<Tag, int>.identity();
  byIdentity[a] = 1;
  byIdentity[b] = 2;
  print('Identity map entries: ${byIdentity.length}'); // 2
}
$ dart main.dart
Equality map entries: 1
Identity map entries: 2

Complex Keys with ListEquality

Lists are not valid HashMap keys out of the box because Dart's default list equality is identity-based — two lists with the same elements are not ==. Use ListEquality from package:collection to provide value-based equality for list keys.

Add the dependency first:

dart pub add collection
main.dart
import 'dart:collection';
import 'package:collection/collection.dart';

void main() {
  const eq = ListEquality<int>();

  var grid = HashMap<List<int>, String>(
    equals: eq.equals,
    hashCode: eq.hash,
  );

  grid[[0, 0]] = 'origin';
  grid[[1, 2]] = 'point A';
  grid[[3, 4]] = 'point B';

  print(grid[[0, 0]]); // origin
  print(grid[[1, 2]]); // point A
}

Without the custom equals and hashCode, each list literal creates a new object, so grid[[0, 0]] would always return null — the lookup key is a different instance from the stored key. The same technique applies to SetEquality and MapEquality from the same package.

$ dart main.dart
origin
point A

Real-World Examples

Word Frequency

Count how many times each word appears in a text.

main.dart
import 'dart:collection';

void main() {
  var text = 'the cat sat on the mat the cat';

  var freq = HashMap<String, int>();
  for (var word in text.split(' ')) {
    freq[word] = (freq[word] ?? 0) + 1;
  }

  var sorted = freq.entries.toList()
    ..sort((a, b) => b.value.compareTo(a.value));

  for (var e in sorted) {
    print('${e.key}: ${e.value}');
  }
}
$ dart main.dart
the: 3
cat: 2
sat: 1
on: 1
mat: 1

Grouping Items

Group a list of strings by their length.

main.dart
import 'dart:collection';

void main() {
  var animals = ['ant', 'bee', 'cat', 'deer', 'eagle', 'frog'];

  var byLength = HashMap<int, List<String>>();
  for (var a in animals) {
    byLength.putIfAbsent(a.length, () => []).add(a);
  }

  // Sort keys for readable output
  var keys = byLength.keys.toList()..sort();
  for (var k in keys) {
    print('$k letters: ${byLength[k]}');
  }
}
$ dart main.dart
3 letters: [ant, bee, cat]
4 letters: [deer, frog]
5 letters: [eagle]

Memoization

Cache results of expensive or recursive computations so each input is evaluated only once.

main.dart
import 'dart:collection';

final _cache = HashMap<int, BigInt>();

BigInt fib(int n) {
  if (n <= 1) return BigInt.from(n);
  return _cache.putIfAbsent(n, () => fib(n - 1) + fib(n - 2));
}

void main() {
  for (var i = 0; i <= 10; i++) {
    print('fib($i) = ${fib(i)}');
  }
}

putIfAbsent is the perfect primitive for memoization: it returns the cached value on a hit and calls the factory and stores the result on a miss — all in one operation.

$ dart main.dart
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55

Performance Characteristics

Understanding how HashMap stores entries helps you predict its behaviour and choose the right collection for your use case.

Best Practices

Source

Dart HashMap Documentation

In this tutorial we covered Dart's HashMap in depth: creating maps via constructors, fromIterables, and collection-for idioms; checking, updating, iterating, and removing entries; safe patterns for concurrent modification; custom equality and hashing callbacks; HashMap.identity(); list keys with ListEquality; real-world patterns such as word frequency, grouping, and memoization; and the performance trade-offs between HashMap, LinkedHashMap, and SplayTreeMap.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.