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, theirhashCodevalues must be identical. -
Objects with the same
hashCodemay 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
hashCodefor 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
hashCodeafter 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.