Dart HashSet
last modified October 18, 2026
In this article, we show how to work with HashSet in Dart. A
HashSet is an unordered collection of unique elements, defined in
the dart:collection library.
HashSet implements the Set interface and stores its
elements in a hash table. Adding, removing and looking up an element takes O(1)
time on average. Elements are compared with == and their
hashCode, so these two members must be consistent: objects that are
equal must have the same hash code.
Dart has three standard sets. The default {} set literal and the
toSet method create a LinkedHashSet, which remembers
the insertion order. A HashSet gives up that order for a little
less memory and speed. A SplayTreeSet keeps its elements sorted.
The order in which a HashSet returns elements is unspecified. It
depends on the hash codes and the internal table, it need not match the
insertion order, and it may differ between Dart versions or platforms. The
outputs shown below are from Dart 3.13 on Linux; never rely on them.
Creating a HashSet
We create a set with the constructor and add elements with add.
import 'dart:collection';
void main() {
var colors = HashSet<String>();
colors.add('red');
colors.add('green');
colors.add('blue');
var added = colors.add('red');
print('Added a duplicate: $added');
print(colors);
print(colors.length);
}
The add method returns true if the element was added
and false if the set already contained it, so a duplicate is
silently ignored. The elements are not printed in the order of insertion.
$ dart main.dart
Added a duplicate: false
{green, blue, red}
3
HashSet from an Iterable
The HashSet.of and HashSet.from constructors create a
set from an existing iterable and remove the duplicates.
import 'dart:collection';
void main() {
var numbers = [1, 2, 3, 2, 4, 1, 5];
var unique = HashSet.of(numbers);
print('Original: $numbers');
print('Unique: $unique');
print('HashSet<int>: ${unique is HashSet<int>}');
var fromList = HashSet.from(numbers);
print('from() gives HashSet<int>: ${fromList is HashSet<int>}');
var large = HashSet<int>.of(numbers.where((n) => n > 2));
print('Large: $large');
var plain = numbers.toSet();
print('toSet() is a LinkedHashSet: ${plain is LinkedHashSet}');
print('toSet() is a HashSet: ${plain is HashSet}');
}
The list contains duplicates, which are gone in the resulting set. There is a
subtle difference between the two constructors. HashSet.of keeps
the element type of the iterable, so we get a HashSet<int>.
HashSet.from accepts any iterable of dynamic elements
and checks the type at runtime; when the type argument is not given, as here, it
is inferred as dynamic. Prefer of unless you really
need from. Note also that toSet returns a
LinkedHashSet, not a HashSet.
$ dart main.dart
Original: [1, 2, 3, 2, 4, 1, 5]
Unique: {1, 2, 3, 4, 5}
HashSet<int>: true
from() gives HashSet<int>: false
Large: {3, 4, 5}
toSet() is a LinkedHashSet: true
toSet() is a HashSet: false
Checking the contents
The set provides methods to test the presence of elements.
import 'dart:collection';
void main() {
var primes = HashSet.of([2, 3, 5, 7, 11]);
print(primes.contains(5));
print(primes.contains(4));
print(primes.isEmpty);
print(primes.isNotEmpty);
print(primes.length);
print(primes.containsAll([3, 7]));
print(primes.lookup(7));
print(primes.lookup(8));
}
The contains method is the main strength of a hash set; it does not
scan the elements. The containsAll method checks whether all
elements of another iterable are present. The lookup method returns
the element of the set that is equal to the argument, or null. This
is useful when equal objects are not identical, for instance to get the
canonical instance.
$ dart main.dart true false false true 5 true 7 null
Set operations
The union, intersection and difference
methods implement the standard set operations.
import 'dart:collection';
void main() {
var a = HashSet.of([1, 2, 3, 4]);
var b = HashSet.of([3, 4, 5, 6]);
print('Union: ${a.union(b)}');
print('Intersection: ${a.intersection(b)}');
print('Difference: ${a.difference(b)}');
print('a unchanged: $a');
print('Subset: ${HashSet.of([3, 4]).difference(a).isEmpty}');
}
Each method returns a new set and leaves the originals unchanged. The
union contains the elements of both sets, the
intersection only those present in both, and the
difference the elements of the first set that are not in the second.
An empty difference means that the first set is a subset of the second. The
containsAll method is the shorter way to test it.
$ dart main.dart
Union: {1, 2, 3, 4, 5, 6}
Intersection: {3, 4}
Difference: {1, 2}
a unchanged: {1, 2, 3, 4}
Subset: true
Iterating over a HashSet
A HashSet is an Iterable, so we can use a
for loop, forEach or the iterator directly.
import 'dart:collection';
void main() {
var languages = HashSet.of(['Dart', 'Java', 'Python', 'Go']);
print('for loop:');
for (var lang in languages) {
print(lang);
}
print('forEach:');
languages.forEach(print);
print('iterator:');
var it = languages.iterator;
while (it.moveNext()) {
print(it.current);
}
print('sorted:');
print(languages.toList()..sort());
}
All three approaches visit the elements in the same order as long as the set is not modified, but the order is arbitrary. If we need a predictable order, we copy the elements to a list and sort it, as in the last statement.
$ dart main.dart for loop: Go Python Dart Java forEach: Go Python Dart Java iterator: Go Python Dart Java sorted: [Dart, Go, Java, Python]
Removing elements
Elements can be removed one by one, by a condition, or all at once.
import 'dart:collection';
void main() {
var fruits = HashSet.of(['apple', 'banana', 'orange', 'kiwi']);
print('Original: ${fruits.toList()..sort()}');
var removed = fruits.remove('banana');
print('Removed banana: $removed');
print('Removed melon: ${fruits.remove('melon')}');
fruits.removeWhere((fruit) => fruit.length > 5);
print('After removeWhere: ${fruits.toList()..sort()}');
fruits.retainWhere((fruit) => fruit.startsWith('a'));
print('After retainWhere: $fruits');
fruits.clear();
print('After clear: $fruits');
}
The remove method returns true if the element was in
the set. The removeWhere method removes the elements that satisfy a
predicate (here the names longer than five characters), and
retainWhere does the opposite and keeps only the matching ones. The
clear method empties the set. To get a stable output we sort the
elements before printing the first two lists.
$ dart main.dart
Original: [apple, banana, kiwi, orange]
Removed banana: true
Removed melon: false
After removeWhere: [apple, kiwi]
After retainWhere: {apple}
After clear: {}
HashSet with custom objects
When we store our own objects, we have to override == and
hashCode.
import 'dart:collection';
class Book {
final String title;
final String author;
Book(this.title, this.author);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Book && title == other.title && author == other.author;
@override
int get hashCode => Object.hash(title, author);
@override
String toString() => 'Book($title, $author)';
}
void main() {
var library = HashSet<Book>();
var book1 = Book('Dart in Depth', 'John Doe');
var book2 = Book('Flutter Basics', 'Jane Smith');
var book3 = Book('Dart in Depth', 'John Doe');
print(library.add(book1));
print(library.add(book2));
print(library.add(book3));
print('Library size: ${library.length}');
print('Contains a copy: ${library.contains(Book('Flutter Basics', 'Jane Smith'))}');
}
The Book class compares two books by title and author, and it
combines the same fields into a hash code with Object.hash. The
first add returns true, the second also, but the third
book equals the first one, so add returns false and the
set has two elements. A newly created, equal book is found by
contains. Without the overrides, the set would use object identity
and keep all three books.
$ dart main.dart true true false Library size: 2 Contains a copy: true
Custom equality
The constructor accepts the optional equals and hashCode
functions. They let us change how elements are compared without touching the
element class.
import 'dart:collection';
void main() {
var names = HashSet<String>(
equals: (a, b) => a.toLowerCase() == b.toLowerCase(),
hashCode: (s) => s.toLowerCase().hashCode,
);
print(names.add('Alice'));
print(names.add('ALICE'));
print(names.add('Bob'));
print(names);
print(names.contains('bob'));
print(names.lookup('alice'));
}
Here the strings are compared case-insensitively. The second add
is rejected because ALICE equals Alice, and the set
keeps the first spelling. The lookup method returns that stored
element. The two functions must agree: if equals says that two
elements are equal, hashCode must return the same number for both.
$ dart main.dart
true
false
true
{Alice, Bob}
true
Alice
Identity sets
The HashSet.identity constructor compares elements with
identical instead of ==.
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;
}
void main() {
var a = Tag('dart');
var b = Tag('dart');
var byEquality = HashSet<Tag>()..addAll([a, b]);
print('Equality set: ${byEquality.length}');
var byIdentity = HashSet<Tag>.identity()..addAll([a, b]);
print('Identity set: ${byIdentity.length}');
print('Contains a: ${byIdentity.contains(a)}');
print('Contains new Tag: ${byIdentity.contains(Tag('dart'))}');
}
The two Tag objects are equal according to ==, so the
default set stores only one of them. The identity set treats them as different
objects and stores both. Likewise, a third, new Tag is not found,
because it is a different instance. An identity set is useful for tracking
specific objects, for example the nodes already visited in a graph traversal.
$ dart main.dart Equality set: 1 Identity set: 2 Contains a: true Contains new Tag: false
Finding duplicates
The fact that add reports whether the element was new makes it easy
to detect duplicates in one pass.
import 'dart:collection';
void main() {
var words = 'the cat and the dog and the bird'.split(' ');
var seen = HashSet<String>();
var duplicates = <String>[];
for (var word in words) {
if (!seen.add(word)) {
duplicates.add(word);
}
}
print('Distinct words: ${seen.length}');
print('Repeated: $duplicates');
}
We go through the words once and add each to the set. When add
returns false, we have seen the word before. The whole operation is
O(n), while comparing every pair of words would be O(n²).
$ dart main.dart Distinct words: 5 Repeated: [the, and, the]
Best practices
- Do not rely on the order: use
LinkedHashSet(the default{}literal) for insertion order orSplayTreeSetfor sorted order. - Override both: when you override
==, overridehashCodetoo, and combine fields withObject.hash. - Immutable elements: do not change fields that take part in
==andhashCodewhile the object is in the set; otherwise it can no longer be found. - Use
of: preferHashSet.oftoHashSet.fromto keep the static element type. - Membership tests: use a set rather than
List.containswhen you test membership repeatedly; a list needs a linear scan. - Do not modify while iterating: adding or removing elements
inside a loop over the set throws a
ConcurrentModificationError.
Source
We have covered creating a HashSet, the set operations, iteration,
removal, custom objects, custom equality, identity sets and a practical use for
finding duplicates.
Author
List all Dart tutorials.