skip to content

In Dart, how do you override operator == and hashCode so a map Coordinate class deduplicates in a Set, and what breaks with only ==?

level: middleimportance: must knowfreq 68%

answer

  1. two members, one contract
  2. Object parameter, never nullable
  3. type test, then field compare
  4. Object.hash over the same fields
  5. hash_and_equals lint

basics

~20 s

Override both: operator ==(Object other) checks that other is a Coordinate and compares the fields, and hashCode returns Object.hash over those same fields. Override only == and equal coordinates usually land in different hash buckets, so a Set keeps duplicates.

solid answer

~40 s

Declare `bool operator ==(Object other)` with a non-nullable parameter, because the language answers `x == null` itself and never passes `null` in. Inside, optionally short-circuit on `identical(this, other)`, then return `other is Coordinate && other.lat == lat && other.lng == lng`. Pair it with `int get hashCode => Object.hash(lat, lng)` over exactly the fields `==` compares. Dart's default `Set` and `Map` are hash-based: they find the bucket from `hashCode` first and only then call `==`. If you override only `==`, the inherited identity hash puts two equal coordinates in different buckets, `==` is usually never consulted, and the set keeps both. The `hash_and_equals` lint, in the core, recommended and flutter lint sets, flags the missing half. Keep the fields `final` so the hash cannot change after insertion.

code

dart · 26 lines
dart
import 'package:flutter/foundation.dart';

@immutable
class Coordinate {
  const Coordinate(this.lat, this.lng);

  final double lat;
  final double lng;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Coordinate && other.lat == lat && other.lng == lng;

  @override
  int get hashCode => Object.hash(lat, lng);
}

void main() {
  final visited = <Coordinate>{
    Coordinate(52.52, 13.405),
    Coordinate(52.52, 13.405), // equal to the first: not added
  };
  print(visited.length); // 1
  print(visited.contains(Coordinate(52.52, 13.405))); // true
}

go deeper

for a junior

Recall that == and hashCode are overridden together, with the Object parameter type and an is-check before comparing fields.

for a middle

Explain how a hash set picks the bucket from hashCode before calling ==, and why that makes a missing hashCode override lose deduplication.

for a senior

Discuss subclass symmetry, keeping value fields final, and choosing between hand-written equality, records, equatable and code generation for a codebase.

for a principal

Set a team rule for value types, such as immutable by default with generated or record-based equality, and enforce it with the hash_and_equals and mutable-class lints.

## The contract Dart expects `Object` gives every class identity equality: `==` is `true` only for the same object, and `hashCode` represents that identity. When a type should compare **by value**, such as a map coordinate, a money amount or an ID, you override two members that must agree: - `operator ==` defines when two instances are equal. `Object`'s documentation requires an **equivalence relation**: reflexive, symmetric, transitive, and it should never throw. - `hashCode` must return **the same integer for any two objects that are equal** under your `==`. Unequal objects may share a hash; that only costs performance. Effective Dart states both rules as "DO override `hashCode` if you override `==`" and "DO make your `==` operator obey the mathematical rules of equality". ## Writing the override ```dart @override bool operator ==(Object other) => identical(this, other) || other is Coordinate && other.lat == lat && other.lng == lng; @override int get hashCode => Object.hash(lat, lng); ``` Points an interviewer listens for: 1. **The parameter is `Object`, not `Object?`.** For `a == b`, Dart first handles `null` itself: if either side is `null`, the result is `true` only when both are. Your method is called only with a non-null argument, and a nullable parameter triggers the analyzer's `non_nullable_equals_parameter` warning. Narrowing the parameter to `Coordinate` is an invalid override, a compile-time error, because an override's parameter type must be the same as or a supertype of the original. 2. **Type-test first.** `other is Coordinate` also promotes `other`, so `other.lat` compiles without a cast. 3. **`identical(this, other)`** is an optional fast path; it matters when comparing many fields. 4. **Hash exactly the fields you compare.** Hashing a field that `==` ignores breaks the contract; skipping one that `==` uses merely causes collisions. ## Choosing the hashing helper | Helper | Use it for | |---|---| | `Object.hash(a, b, ...)` | a fixed set of fields; takes 2 to 20 values, `null` allowed | | `Object.hashAll(iterable)` | a list-like field whose order matters | | `Object.hashAllUnordered(iterable)` | a set-like field whose order does not matter | All three arrived in Dart 2.14 and replace hand-rolled `a.hashCode ^ b.hashCode` code, which collides for swapped fields: `(1, 2)` and `(2, 1)` give the same XOR. Their results are only guaranteed stable within one run of one isolate, so never persist them. ## What breaks with only `==` overridden Dart's default `Set` is a `LinkedHashSet` and the default `Map` a `LinkedHashMap`; both are hash tables. Adding an element goes: 1. compute `element.hashCode` and pick a bucket; 2. compare the element with `==` only against entries in that bucket; 3. insert if nothing there was equal. With the inherited identity hash, two field-equal coordinates almost always get different hash codes, so step 2 usually never compares them and the set keeps both. `contains` and `Map` lookups fail the same way: a coordinate you build from the same latitude and longitude is not found. The failure is **silent and probabilistic**, which is why the `hash_and_equals` lint exists and why it sits in the `core`, `recommended` and `flutter` rule sets. ## Subclasses and symmetry `other is Coordinate` accepts subclasses. If a subclass `LabeledCoordinate` adds a `label` and overrides `==` to compare it, then `coordinate == labeled` can be `true` while `labeled == coordinate` is `false`, which breaks symmetry. Common fixes: - prevent subclassing of value types; - compare `other.runtimeType == runtimeType` instead of an `is` test, at the price of never treating a subclass instance as equal; - use composition instead of inheritance for the extra data. ## Checking an override 1. Assert that `a == a` holds and that `a == b` agrees with `b == a` for a pair of equal instances. 2. Assert that equal instances report the same `hashCode`. 3. Add two equal instances to a `Set` and check its `length` is `1`; look up a freshly built equal instance with `contains`. 4. Keep the `hash_and_equals` lint enabled so a later edit cannot drop one half. A test like this takes a minute and catches the half-overridden class before it reaches a deduplication path. ## Alternatives to writing it by hand - **Records** such as `(double, double)` already compare and hash by field values. - **`package:equatable`** implements both members from a `props` list. - **Code generation** such as `freezed` writes them for you. Each is a tradeoff between boilerplate and explicit control; for a two-field value class, the hand-written version above is short and clear.

  • Why is `bool operator ==(Coordinate other)` rejected by the compiler?
    An override's parameter types must be the same as or a supertype of the overridden member's. `Object.==` takes `Object`, and `Coordinate` is narrower, so it is an invalid override. Keep `Object` and type-test with `is`, which also promotes `other` so its fields are accessible.
  • Why prefer Object.hash(lat, lng) over lat.hashCode ^ lng.hashCode?
    XOR is symmetric, so swapped values such as `(1, 2)` and `(2, 1)` collide, and identical fields cancel to zero. `Object.hash` mixes the values in order with a seed, giving better distribution. Both are valid under the contract; the difference is collision rate and therefore lookup speed.
  • Coordinate has a subclass that adds a label and compares it in ==. What goes wrong?
    Symmetry breaks: the base class's `is Coordinate` test accepts the subclass and ignores the label, while the subclass's test rejects the base instance. So `a == b` and `b == a` disagree, and hash collections behave inconsistently. Prevent subclassing, compare `runtimeType`, or use composition.

saying these in an interview costs you the question

  • Overriding == alone is enough because a Set only calls == to find duplicates.
  • The == parameter should be Object? so the method can return false for null.
  • Dart refuses to compile a class that overrides == without hashCode.
  • hashCode must be unique for every unequal instance.
  • XOR of the field hashes is the recommended way to combine them.