skip to content

Equality & Operators

Dart equality is identity until you override operator == and hashCode together, which value objects need to work as map keys. Interviewers also ask why two equal-looking lists are not ==.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart, why does [1, 2] == [1, 2] evaluate to false, and how do you compare two lists by their contents?

level: juniorimportance: must knowfreq 58%

answer

  1. inherited from Object
  2. two literals, two new objects
  3. identity, not elements
  4. foundation listEquals, collection ListEquality
  5. const lists are the exception

basics

~20 s

Dart's List keeps identity equality: == is true only for the same list object, and every non-const literal creates a new one. Compare contents with Flutter's listEquals, package:collection's ListEquality, or a length check plus element loop.

solid answer

~40 s

`List` declares `operator ==`, but its contract says lists are only equal to themselves: the comparison never looks at the elements. Each evaluation of a non-const literal such as `[1, 2]` allocates a new object, so two literals are two distinct objects and `==` returns `false`. `Set` and `Map` behave the same way. To compare contents, use `listEquals(a, b)` from Flutter's `foundation` library, `ListEquality().equals(a, b)` or `DeepCollectionEquality` from `package:collection` for nested data, or a length check plus an element-by-element loop. The case people find confusing is `const [1, 2] == const [1, 2]`, which is `true` because constant canonicalization makes both expressions the same object, not because any elements were compared.

code

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

void main() {
  final a = [1, 2];
  final b = [1, 2];

  print(a == b); // false: two separate list objects
  print(identical(a, a)); // true: same object
  print(listEquals(a, b)); // true: same length, equal elements
  print(<List<int>>{a, b}.length); // 2: identity hash codes differ
  print(const [1, 2] == const [1, 2]); // true: one canonical instance
}

go deeper

for a junior

Recall that == on lists, sets and maps checks for the same object, and name one helper that compares contents, such as listEquals from Flutter's foundation library.

for a middle

Explain why the const case is true through canonicalization rather than element comparison, and show how a list field changes a class's == and hashCode.

for a senior

Point out the production traps: lists as map keys, change detection that compares freshly built lists, and nested collections that need DeepCollectionEquality.

for a principal

Weigh modelling collections as immutable value types with content equality against leaving identity semantics and paying the cost of deep comparison at chosen boundaries.

## Equality in Dart starts as identity Every Dart class inherits `operator ==` and `hashCode` from `Object`. The inherited `==` returns `true` **only when both operands are the same object** (identity), and the inherited `hashCode` represents that identity. A class gets value-style equality only if it overrides both members. `int`, `double`, `String`, `DateTime` and records compare by value because their implementations define it that way; ordinary classes you write do not. ## What `List.==` actually promises The `List` interface in `dart:core` does declare `bool operator ==(Object other)`, which surprises people into thinking it compares elements. Its documentation says the opposite: **lists are, by default, only equal to themselves**, and even when the other operand is a list the comparison does not look at the elements. So in `[1, 2] == [1, 2]`: 1. The first literal is evaluated and a new growable list is allocated. 2. The second literal is evaluated and **another** new list is allocated. 3. `==` runs the list's identity comparison, sees two different objects, and returns `false`. The same applies to `Set` and `Map` values: `{1} == {1}` and `{'a': 1} == {'a': 1}` are both `false` for non-const literals. Their `hashCode` is identity-based too, which is why a `List<int>` works poorly as a `Map` key or a `Set` element: two lists with the same contents are two different keys. ## Comparing contents | Tool | Where it lives | Compares | |---|---|---| | `listEquals(a, b)` | Flutter's `package:flutter/foundation.dart` | length, then each element with `==`; also `setEquals`, `mapEquals` | | `ListEquality().equals(a, b)` | `package:collection` | length and elements with `==`, one level deep | | `DeepCollectionEquality().equals(a, b)` | `package:collection` | nested lists, sets and maps recursively | | a hand-written loop | your code | whatever you decide, for example tolerance on doubles | `listEquals` takes nullable lists, so `listEquals(null, null)` is `true`. Note that these helpers still use each **element's** `==`: comparing two lists of your own objects by content only works if those objects override `==` themselves. ## Where the identity rule bites - **A value class with a list field.** If `Route` holds `List<Coordinate> points` and its `==` writes `points == other.points`, two routes with the same points are unequal. Compare with `listEquals` and hash with `Object.hashAll(points)`, which combines element hashes in order. - **Lists as set elements or map keys.** `<List<int>>{[1, 2], [1, 2]}` has two elements. Convert to a record or a value class with proper equality, or key the map by a derived string only when that string is canonical. - **`contains` and `indexOf` on a list of lists.** `[[1, 2]].contains([1, 2])` is `false` for the same reason. - **Change detection.** Comparing a freshly built list with the previous one by `==` always reports a change, even when nothing changed; comparing a list to itself after mutating it in place always reports no change. ## Why collections keep identity equality Dart's collections are **mutable by default**. If `List.==` compared contents, a list's `hashCode` would have to be computed from its elements too, and adding an element to a list stored in a `Set` would silently move it to a different bucket. Content comparison is also `O(n)`, and nested collections make it recursive, so the cost of an innocent-looking `==` would depend on data size. Keeping identity makes `==` and `hashCode` cheap and stable, and leaves content comparison as an explicit choice with an explicit helper. ## The `const` exception `const [1, 2] == const [1, 2]` is `true`. Nothing about `List.==` changed: **constant canonicalization** makes two constant expressions with the same state evaluate to one canonical instance, so the identity comparison succeeds. `identical(const [1, 2], const [1, 2])` is also `true`. The moment either side is not constant, for example a list built at runtime or `[1, 2]` without `const`, the result is `false` again. Relying on this for content comparison is a trap: it works in a toy example and fails on the first list that comes from JSON or user input. ## Takeaways - `==` on `List`, `Set` and `Map` means **same object**, not same contents. - Use a content-comparison helper for collections, and `Object.hashAll` when a collection field takes part in a class's `hashCode`. - Element comparison in those helpers is only as good as the elements' own `==`.

  • Your value class has a List<String> tags field. How do you write its == and hashCode?
    Compare the field element by element, for example `listEquals(tags, other.tags)` or `ListEquality().equals(...)`, and hash it with `Object.hashAll(tags)`, which combines the element hashes in iteration order. Using `tags == other.tags` and `tags.hashCode` silently falls back to identity, so two equal-looking instances would differ. Also make the list unmodifiable, since mutating it would change equality after the object is stored in a set.
  • When do you need DeepCollectionEquality instead of listEquals?
    When elements are themselves collections. `listEquals` compares elements with their own `==`, so a `List<List<int>>` still compares the inner lists by identity. `DeepCollectionEquality` from `package:collection` recurses into nested lists, sets and maps, treating sets and maps as unordered.

saying these in an interview costs you the question

  • Dart's List.== compares elements in order, like a value type.
  • Two list literals with the same elements are the same object.
  • const lists compare by contents because they are unmodifiable.
  • list.hashCode is computed from the list's elements.
  • listEquals deep-compares nested lists automatically.
open as a page

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%

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.

open as a page

In Dart, how does the identical() function differ from ==, and when do you need identical() or an identity-based Set or Map?

level: middleimportance: should knowfreq 40%

basics

~20 s

== calls the left operand's overridable operator ==; identical(a, b) is a top-level function that only tests whether both references are the same object. Use identity for fast paths, visited-sets and Set.identity or Map.identity collections.

open as a page

A Dart Set of map Coordinate objects stops finding elements and shows duplicates after coordinates are edited in place; what is wrong, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The Set stored each Coordinate under the hashCode it had when added; mutating a field that feeds hashCode changes the hash, so lookups probe the wrong bucket. Make the class immutable and replace elements with remove then add.

open as a page

In Dart, which operators can a class define, and how would you declare + and [] on a coordinate value type?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

Arithmetic, bitwise, relational, ==, [] and []= are declared as methods with the operator keyword, such as Coordinate operator +(Coordinate other). !=, &&, ||, ?? and ! are built in, and += reuses +.

open as a page