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.
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 |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
- O(1) amortized — lookup, insertion, and removal each compute a hash code, locate a bucket, and perform a constant number of equality checks in the typical case.
- Worst-case O(n) — a poor hash function that maps all keys to the same bucket degrades every operation to a linear scan. Dart's built-in types (strings, integers, etc.) have well-distributed hash codes, so degenerate behaviour is rare in practice.
- Automatic resizing — when the entry count exceeds the
internal load threshold, Dart doubles the bucket array and rehashes all
entries. This O(n) rehash is amortised across many insertions so the
per-insert cost stays O(1). Pre-populate the map from a collection in one
call (e.g.,
HashMap.of) rather than inserting one entry at a time to avoid intermediate rehashes. - vs LinkedHashMap —
HashMapuses slightly less memory and is marginally faster thanLinkedHashMap, which maintains a doubly-linked list to preserve insertion order. - vs SplayTreeMap —
HashMapwins on random lookups (O(1) vs O(log n)), butSplayTreeMapis the right choice when you need sorted iteration or range-based queries.
Best Practices
- Immutable keys: Use immutable objects (or
finalfields) as keys. Mutating a key after insertion corrupts the map because its hash code may change. - Consistent hashCode and ==: Always override both together. If two objects are equal, they must return the same hash code. Use
Object.hash(orObject.hashAll) to combine fields. - Prefer key lookups:
containsKeyand[]are O(1). AvoidcontainsValuein hot paths — it scans the entire map. - Ordered iteration: If you need consistent iteration order, use
LinkedHashMap(preserves insertion order) or a sortedMapfromSplayTreeMap. - Null safety: Declare value types as non-nullable (e.g.,
HashMap<String, int>) and useputIfAbsentorupdate(..., ifAbsent: ...)to avoid null-related surprises.
Source
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
List all Dart tutorials.