equals() 的三条契约(自反、对称、传递)是文档里的一句话,这篇真实动手违反它们,看看集合类到底会因此出多大的问题——结果比想象中更直接:同一个语义查询,调换一下参数顺序,答案真实就变了。
上一篇:数组 clone() 真实验证
构造一个"能识别普通String,但反过来String不认识它"的equals实现——这是《Effective Java》里的经典反例。
EqualsContractDemo.java class CaseInsensitiveString { public boolean equals(Object o) { if (o instanceof CaseInsensitiveString) return ...; if (o instanceof String) return s.equalsIgnoreCase((String) o); // 问题根源 return false; } } CaseInsensitiveString cis = new CaseInsensitiveString("Hello"); String s = "hello";
cis.equals(s) 真实结果: true s.equals(cis) 真实结果: false(对称性真实被违反:一个说相等,另一个说不相等) list.contains(s) 真实结果: false list2.contains(cis) 真实结果: true(同样的语义查询,因为参数顺序不同,真实给出不同答案)
真实结果证明了对称性契约被打破后的直接后果:cis.equals(s) 说两者相等,但 s.equals(cis)(标准 String.equals(),只认识另一个 String)真实说不相等。这个不一致真实传导进了集合操作——List<CaseInsensitiveString>.contains(s) 内部调用的是 s.equals(元素),结果是 false;而反过来 List<Object>.contains(cis)(存的是 s)内部调用的是 cis.equals(元素),结果真实是 true。同一对"逻辑上相等"的值,查询顺序一换,答案就变了——这是对称性契约最直接、也最容易在真实项目里踩中的坑。
另一个经典场景:给 Point 加一个 color 字段变成 ColorPoint,equals() 里"忽略颜色向下兼容"的写法会真实打破传递性。
EqualsContractDemo.java class ColorPoint extends Point { public boolean equals(Object o) { if (!(o instanceof Point)) return false; if (!(o instanceof ColorPoint)) return o.equals(this); // 退化成父类比较 return super.equals(o) && ((ColorPoint) o).color.equals(color); } }
p1.equals(cp1) 真实结果(忽略颜色比较): true cp1.equals(p1) 真实结果: true p1.equals(cp2) 真实结果: true cp1.equals(cp2) 真实结果(颜色不同): false(p1=cp1, p1=cp2, 但 cp1≠cp2 —— 传递性真实被打破)
每一行都是真实运行这段代码得到的布尔值。