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.hashCode 与 Object.== (equals)的默认实现仅体现了对象的标识符。为了保持Set或Map语义正确:
- 两个相等的对象,其哈希值也必须相等;
- 重写了
Object.==也必须重写Object.hashCode,反之亦然; - 通常只对不可变类重写
Object.==。
示例
下例中Point2类 重写了 Object.hashCode 与 Object.== ,而 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;
}
}
- 变量
a,b的类型为Point,虽然它们的成员变量(x和y)的值都一样,但它们的哈希值不同(原因是它们的对象标识不同); - 变量
c,d的类型为Point2,它们的哈希值相同(因为我们重写了Object.hashCode与Object.==,只要它们的成员变量具有相同的值,它们的哈希值就一定相同); a != b且c == d,显然成立;- 由于
c==d成立,set1只有3个元素; - 同理
map1只有3个entries,map1内部将c和d视为同样的key(即Point2(1, 2)),与其对应的value是最新的那个值'd'。