bigywater.tech
Java 内部原理 · 81

equals() 契约真实验证:对称性被打破后,List.contains() 真实给出矛盾答案

equals() 的三条契约(自反、对称、传递)是文档里的一句话,这篇真实动手违反它们,看看集合类到底会因此出多大的问题——结果比想象中更直接:同一个语义查询,调换一下参数顺序,答案真实就变了。

对称性打破 → contains()矛盾
真实验证:list.contains(s) 和 list2.contains(cis) 描述同一个语义查询,因为equals实现不对称,真实给出了相反的答案
p1=cp1,p1=cp2,但cp1≠cp2
真实复现经典传递性陷阱:Point和ColorPoint的equals真实让"相等"关系不再传递

上一篇:数组 clone() 真实验证

真实复现:对称性被打破,contains() 结果真实矛盾

构造一个"能识别普通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。同一对"逻辑上相等"的值,查询顺序一换,答案就变了——这是对称性契约最直接、也最容易在真实项目里踩中的坑。

真实复现:传递性被打破,p1=cp1、p1=cp2,但cp1≠cp2

另一个经典场景:给 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 —— 传递性真实被打破)
真实矛盾是怎么产生的。p1 是普通 Point,只比较坐标,所以真实认为和坐标相同的 cp1(红色)、cp2(蓝色)都相等——从 p1 的视角看,颜色信息不存在。但 cp1.equals(cp2) 真实要比较颜色,红色和蓝色不同,结果是 false。这样就真实构造出了 p1.equals(cp1)=true、p1.equals(cp2)=true,却 cp1.equals(cp2)=false 的矛盾链条——按照"相等"应该具有的传递性,前两个都成立理应推出第三个也成立,真实结果却违反了这个逻辑。《Effective Java》对这类问题的建议是:如果要在 equals 里加入新的字段,更安全的做法是用组合代替继承(把 Point 作为 ColorPoint 的字段,而不是父类),彻底避免"部分比较、部分忽略"的场景出现。

分步演示:矛盾是在哪一步真实出现的

每一行都是真实运行这段代码得到的布尔值。

对称性是怎么被打破、又是怎么传导进 List.contains() 的;传递性又是怎么在 p1/cp1/cp2 之间断掉的
这一步新验证的比较

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,两组契约违反场景重复运行结果完全一致。
    • 和Comparator 契约那篇一起看,能更完整理解 Java 里"相等性""排序性"这些看似简单的接口,背后都有严格的数学性质要求,违反了不会立刻报错,但会在使用集合类时真实产生难以排查的诡异行为。
    • 没有涉及:自反性(x.equals(x)必须为true)被打破的真实场景(比较少见,但在涉及浮点数NaN字段时可能发生)、Objects.equals()对null的安全处理——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号