ZetCode

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:

Why the contract matters:

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.

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

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

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

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

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

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

Jan Bodnar is a passionate programmer with over a decade of teaching and writing experience. Since 2007, he has authored over 1,400 articles and 8 e-books across various programming languages.

Explore all Dart tutorials.