Skip to content
AgileLogics
5 min read

Equality, hashCode, and toString

Prerequisites: Classes and objects

By default, two objects are equal only if they are the same object, and printing one shows Instance of 'ClassName'. For classes that hold data, you usually want to change both.

Overriding ==, hashCode, and toString

class Money {
  final int cents;
  final String currency;

  const Money(this.cents, this.currency);

  @override
  bool operator ==(Object other) =>
      other is Money && other.cents == cents && other.currency == currency;

  @override
  int get hashCode => Object.hash(cents, currency);

  @override
  String toString() => '${(cents / 100).toStringAsFixed(2)} $currency';

  Money operator +(Money other) {
    if (other.currency != currency) throw ArgumentError('currency mismatch');
    return Money(cents + other.cents, currency);
  }
}

void main() {
  final a = Money(1050, 'EUR');
  final b = Money(1050, 'EUR');

  print(a == b);
  print({a, b}.length);
  print(a);
  print(a + Money(250, 'EUR'));
}

Expected output

true
1
10.50 EUR
13.00 EUR
  • operator == defines when two objects count as equal.
  • hashCode must agree with ==: equal objects must have equal hash codes, or sets and maps behave incorrectly. Object.hash(...) combines fields correctly.
  • toString controls what print and string interpolation show.
  • Dart also lets you define operators such as +, <, and [].

Less boilerplate

Writing these by hand for every data class is tedious and easy to get wrong. Common alternatives:

  • Records — already compared by value, for simple groupings.
  • The equatable package — list the fields once in a props getter.
  • Code generators such as freezed — generate ==, hashCode, toString, copyWith, and JSON methods from a short declaration.

Common mistakes

  • Overriding == without hashCode. Objects that are "equal" then appear twice in a set.
  • Making objects with a custom == mutable. If a field changes while the object is in a set, the set can no longer find it.