In Dart, why does [1, 2] == [1, 2] evaluate to false, and how do you compare two lists by their contents?
answer
- inherited from Object
- two literals, two new objects
- identity, not elements
- foundation listEquals, collection ListEquality
- const lists are the exception
basics
~20 sDart'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 linesimport '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
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.
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.
Point out the production traps: lists as map keys, change detection that compares freshly built lists, and nested collections that need DeepCollectionEquality.
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.