Equals and HashCode in Dart
Last modified June 5, 2025
This tutorial explores the equals/hashCode contract in Dart, a critical concept
for object comparison and hash-based collections. It includes modern hash
functions like Object.hash
, Object.hashAll
, and
Object.hashAllUnordered
introduced in Dart 2.14.
The equals/hashCode contract ensures objects are compared
correctly and stored reliably in hash-based collections like
HashSet
and HashMap
. Properly implementing this
contract prevents errors in collection operations.
Key rules of the contract:
-
If two objects are equal via the
==
operator, theirhashCode
values must be identical. -
Objects with the same
hashCode
may not be equal due to possible hash collisions. - Violating the contract causes unpredictable behavior in hash-based collections, such as failed lookups or missing elements.
Why the contract matters:
-
Efficient Lookups: Hash-based collections use
hashCode
for fast indexing. Inconsistent hash codes disrupt retrieval. -
Collision Handling: Collections use
==
to resolve hash collisions. Inconsistencies lead to data integrity issues. -
Stable Hashing: A changing
hashCode
after insertion in a collection can make objects irretrievable.
The contract ensures reliable behavior in collections. Dart's modern hash
functions (Object.hash
, Object.hashAll
,
Object.hashAllUnordered
) simplify hash code generation while
maintaining contract compliance.
Concept | Description | Importance |
---|---|---|
== Operator | Compares objects for equality | Must be reflexive, symmetric, transitive |
hashCode | Generates an integer hash value | Must align with == |
Object.hash | Combines multiple hash codes | Simplifies hash code generation |
Identity | Default comparison in Dart | Uses identical |
By default, Dart's ==
operator uses identical
for
identity comparison. To implement value-based equality, override both
==
and hashCode
. The Object
class
provides default identity-based implementations.
Basic Equals/HashCode with Object.hash
This example uses Object.hash
to implement the equals/hashCode
contract for a Person
class, comparing name and age.
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: $name, age: $age)'; } void main() { var p1 = Person('Alice', 30); var p2 = Person('Alice', 30); var p3 = Person('Bob', 25); print('p1 == p2: ${p1 == p2}'); print('p1 == p3: ${p1 == p3}'); var people = {p1, p2, p3}; print('People in set: $people'); }
The ==
operator checks identity, type, and field equality.
Object.hash
combines the hash codes of name
and age
, replacing the older XOR approach for better
distribution.
The HashSet
recognizes p1
and p2
as equal, storing only one. Object.hash
ensures consistent
hash codes for equal objects.
$ dart run basic_equals.dart p1 == p2: true p1 == p3: false People in set: {Person(name: Alice, age: 30), Person(name: Bob, age: 25)}
Equality with Collections
This example shows how the contract affects Set
and
List
behavior using a Point
class.
class Point { final int x; final int y; Point(this.x, this.y); @override bool operator ==(Object other) => identical(this, other) || other is Point && x == other.x && y == other.y; @override int get hashCode => Object.hash(x, y); @override String toString() => 'Point($x, $y)'; } void main() { var p1 = Point(1, 2); var p2 = Point(1, 2); var p3 = Point(3, 4); var pointsList = [p1, p2, p3]; print('List length: ${pointsList.length}'); var pointsSet = {p1, p2, p3}; print('Set size: ${pointsSet.length}'); print('Contains p2: ${pointsSet.contains(p2)}'); }
The Point
class uses value equality, with Object.hash
for hash code generation. The List
allows duplicates, while the
Set
eliminates them based on equality.
The contains
method confirms that p2
is recognized in
the Set
, demonstrating proper contract adherence.
$ dart run collection_equality.dart List length: 3 Set size: 2 Contains p2: true
Inheritance and Equality
This example implements equals/hashCode in a class hierarchy, ensuring contract compliance across subclasses.
class Shape { final String color; Shape(this.color); @override bool operator ==(Object other) => identical(this, other) || other is Shape && runtimeType == other.runtimeType && color == other.color; @override int get hashCode => Object.hash(color, runtimeType); } class Circle extends Shape { final double radius; Circle(String color, this.radius) : super(color); @override bool operator ==(Object other) => identical(this, other) || other is Circle && super == other && radius == other.radius; @override int get hashCode => Object.hash(super.hashCode, radius); @override String toString() => 'Circle(color: $color, radius: $radius)'; } void main() { var shape = Shape('red'); var circle1 = Circle('blue', 5.0); var circle2 = Circle('blue', 5.0); var circle3 = Circle('red', 10.0); print('circle1 == circle2: ${circle1 == circle2}'); print('circle1 == circle3: ${circle1 == circle3}'); print('shape == circle1: ${shape == circle1}'); var shapes = {shape, circle1, circle2, circle3}; print('Unique shapes: $shapes'); }
The Shape
class includes runtimeType
to distinguish
subclasses. Circle
checks superclass equality first, using
Object.hash
to combine hash codes.
This ensures symmetry and consistency across the hierarchy, with
circle1
and circle2
being equal.
$ dart run inheritance_equality.dart circle1 == circle2: true circle1 == circle3: false shape == circle1: false Unique shapes: {Shape(color: red), Circle(color: blue, radius: 5.0), Circle(color: red, radius: 10.0)}
Equality with Nullable Fields
This example handles nullable fields in equals/hashCode implementations using
Object.hash
.
class Product { final String id; final String? name; final double? price; Product(this.id, this.name, this.price); @override bool operator ==(Object other) => identical(this, other) || other is Product && id == other.id && name == other.name && price == other.price; @override int get hashCode => Object.hash(id, name, price); @override String toString() => 'Product(id: $id, name: $name, price: $price)'; } void main() { var p1 = Product('123', 'Widget', 9.99); var p2 = Product('123', 'Widget', 9.99); var p3 = Product('123', null, null); var p4 = Product('123', null, null); print('p1 == p2: ${p1 == p2}'); print('p3 == p4: ${p3 == p4}'); print('p1 == p3: ${p1 == p3}'); var products = {p1, p2, p3, p4}; print('Unique products: $products'); }
The ==
operator safely compares nullable fields.
Object.hash
handles null values automatically, ensuring
robust hash code generation.
The example confirms that products with null fields are correctly handled, maintaining the contract.
$ dart run nullable_equality.dart p1 == p2: true p3 == p4: true p1 == p3: false Unique products: {Product(id: 123, name: Widget, price: 9.99), Product(id: 123, name: null, price: null)}
Equality with Collections Using Object.hashAll()
This example uses Object.hashAll
to handle collections
as fields in a Team
class.
class Team { final String name; final List<String> members; Team(this.name, this.members); @override bool operator ==(Object other) => identical(this, other) || other is Team && name == other.name && other.members.length == members.length && other.members.asMap().entries.every( (e) => members[e.key] == e.value); @override int get hashCode => Object.hash(name, Object.hashAll(members)); @override String toString() => 'Team(name: $name, members: $members)'; } void main() { var team1 = Team('A', ['Alice', 'Bob']); var team2 = Team('A', ['Alice', 'Bob']); var team3 = Team('A', ['Alice', 'Charlie']); var team4 = Team('B', ['Alice', 'Bob']); print('team1 == team2: ${team1 == team2}'); print('team1 == team3: ${team1 == team3}'); print('team1 == team4: ${team1 == team4}'); var teams = {team1, team2, team3, team4}; print('Unique teams: $teams'); }
The ==
operator checks list equality manually for simplicity.
Object.hashAll
generates a hash code for the members
list, combined with the name
hash code.
This ensures teams with identical members are equal, and the
Set
correctly identifies unique teams.
$ dart run collection_field_equality.dart team1 == team2: true team1 == team3: false team1 == team4: false Unique teams: {Team(name: A, members: [Alice, Bob]), Team(name: A, members: [Alice, Charlie]), Team(name: B, members: [Alice, Bob])}
Unordered Collection Equality with Object.hashAllUnordered
This example uses Object.hashAllUnordered
for collections where
order doesn't matter, like a set of tags in a Post
class.
class Post { final String title; final Set<String> tags; Post(this.title, this.tags); @override bool operator ==(Object other) => identical(this, other) || other is Post && title == other.title && tags.length == other.tags.length && tags.every((tag) => other.tags.contains(tag)); @override int get hashCode => Object.hash(title, Object.hashAllUnordered(tags)); @override String toString() => 'Post(title: $title, tags: $tags)'; } void main() { var post1 = Post('News', {'urgent', 'breaking'}); var post2 = Post('News', {'breaking', 'urgent'}); var post3 = Post('News', {'local', 'urgent'}); var post4 = Post('Update', {'urgent', 'breaking'}); print('post1 == post2: ${post1 == post2}'); print('post1 == post3: ${post1 == post3}'); print('post1 == post4: ${post1 == post4}'); var posts = {post1, post2, post3, post4}; print('Unique posts: $posts'); }
The ==
operator checks if the tags
sets
contain the same elements, regardless of order.
Object.hashAllUnordered
generates a hash code
insensitive to element order.
This ensures posts with the same tags in any order are equal, and the
Set
handles them correctly.
Source
Dart Equality Guidelines
Dart == operator
Mastering the equals/hashCode contract, enhanced by
Object.hash
, Object.hashAll
, and
Object.hashAllUnordered
, ensures robust object comparison and
collection behavior in Dart.
Author
Explore all Dart tutorials.