ZetCode

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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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 ==.

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;
}

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.

main.dart
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

Source

Dart HashSet documentation

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

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.