hashCode 与 ==操作符

Set 和 Map 通常依托哈希表实现。 逻辑上,哈希表可以抽象为一个两列的表格:一列是由 Object.hashCode生成的哈希码(即散列码),另一列则是对应的 entry(key-value pair)。物理存储上,程序先将 key 映射为哈希码,再将哈希码转换为数组索引,该索引决定了 Entry 在哈希表中的物理位置。通过这种方式,Set 和 Map 实现了极其高效的元素存取。有趣的是,在很多底层实现中,Set 其实就是一个“值为空”的 Map;例如 Go 语言并未提供内置的 Set 类型,而是建议开发者直接使用 Map。

flowchart LR
   A(( entry )) -->|key.hashCode| B(( 哈希码 ))
   B --> C(( 索引 ))
   C -->|构建| D[( 哈希表 )]
   D -->|查找| A

Set或Map通常基于Object.== 进行相等性测试。Object.hashCodeObject.== (equals)的默认实现仅体现了对象的标识符。为了保持Set或Map语义正确:

  • 两个相等的对象,其哈希值也必须相等;
  • 重写了Object.==也必须重写Object.hashCode,反之亦然;
  • 通常只对不可变类重写 Object.==

示例

下例中Point2类 重写了 Object.hashCodeObject.== ,而 Point类使用了其默认实现。

// ex661.dart
void main() {
  var a = Point(1, 2);
  var b = Point(1, 2);

  var c = Point2(1, 2);
  var d = Point2(1, 2);

  print('a.hashCode: ${a.hashCode}'); // 1
  print('b.hashCode: ${b.hashCode}'); // 1a

  print('c.hashCode: ${c.hashCode}'); // 2
  print('d.hashCode: ${d.hashCode}'); // 2a

  assert(a != b); // 3
  assert(c == d); // 3a

  var set1 = {a, b, c, d}; // 4
  print(set1); // Output: {Point(1, 2), Point(1, 2), Point2(1, 2)}

  var map1 = {a: 'a', b: 'b', c: 'c', d: 'd'}; // 5
  print(map1); // Output: {Point(1, 2): a, Point(1, 2): b, Point2(1, 2): d}
}

class Point {
  final int x, y;
  Point(this.x, this.y);

  @override
  String toString() => 'Point($x, $y)';
}

class Point2 {
  final int x, y;
  Point2(this.x, this.y);

  @override
  String toString() => 'Point2($x, $y)';

  @override
  int get hashCode {
    const prime = 31;
    var result = 1;
    result = prime * result + x.hashCode;
    result = prime * result + y.hashCode;
    return result;
  }

  @override
  bool operator ==(Object other) {
    if (other is Point2) {
      return x == other.x && y == other.y;
    }
    return false;
  }
}
  1. 变量a,b的类型为Point,虽然它们的成员变量(xy)的值都一样,但它们的哈希值不同(原因是它们的对象标识不同);
  2. 变量c,d的类型为Point2,它们的哈希值相同(因为我们重写了Object.hashCodeObject.==,只要它们的成员变量具有相同的值,它们的哈希值就一定相同);
  3. a != bc == d,显然成立;
  4. 由于 c==d 成立, set1 只有3个元素;
  5. 同理 map1只有3个entries,map1内部将cd视为同样的key(即Point2(1, 2)),与其对应的value是最新的那个值'd'