我在使用findbugs的时候,发现了自己程序中的一个bug:
EQ_SELF_USE_OBJECT
Bug: defines equals method and uses Object.equals(Object)
这说的是在写.equals函数的时候,传递的参数需要用object类,否则就不是重写,不能覆盖父类的equals。
于是我想到我对equals和hashcode这点当时就没怎么学明白。想要梳理一下。
阅读:《Java编程思想》17.9散列与散列码
MIT6.031讲义 Reading 15: Equality : http://web.mit.edu/6.031/www/sp17/classes/15-equality/
1.equals
在一个自定义的ADT里面,我们需要判断相等,现在写一个简单的程序
package test1; public class bag { String label; bag(String label) { this.label = label; } public boolean equals(bag that) { return this.label.equals(that.label); } public static void main(String[] args) { bag bag1 = new bag("CHANEL"); bag bag2 = new bag("CHANEL"); Object bag3 = bag2; bag bag4 = new bag("PRADA"); System.out.println(bag1.equals(bag2)); //true System.out.println(bag1.equals(bag3)); //false } }
事实上, bag 只是重载(overloaded)了 equals() ⽅法,因为它的⽅法标识和 Object 中的不⼀样,也就是说,这是 bag 中有两个 equals() ⽅法:⼀个是从 Object 隐式继承下来 的 equals(Object) ,还有⼀个就是我们写的 equals(bag).而正确的做法是重写(override),这也就是为什么我们重写的时候最好在开头加上@标记,这样就可以自动检查我们是否真的是override了。
package test1; public class bag { String label; bag(String label) { this.label = label; } @Override public boolean equals(Object that) { return (that instanceof bag) && this.sameValue((bag) that); } boolean sameValue(bag that1) { return this.label == that1.label; } public static void main(String[] args) { bag bag1 = new bag("CHANEL"); bag bag2 = new bag("CHANEL"); Object bag3 = bag2; bag bag4 = new bag("PRADA"); System.out.println(bag1.equals(bag2));//true System.out.println(bag1.equals(bag3));//true } }
这就是我用findbugs找出的bug
另外,equals需满足四个条件,不然就会有潜在的bug:
1.equals 必须定义⼀个等价关系。即(⾃反性、对称性和传递性)。
2.equals 必须是确定的。x.equals(y)无论调用多少次都应该是相同的结果。
3.对于不是null的索引 x , x.equals(null) 一定返回false。
4.如果两个对象使⽤ equals 操作后结果为真,那么它们各⾃的 hashCode 操作的结果也应该相同。
前三条我们可以值观理解的,而为什么它们的hashcode需要相同,这就与ADT的查询有关了。
2.hashcode
bag bag1 = new bag("CHANEL"); bag bag2 = new bag("CHANEL"); Object bag3 = bag2; bag bag4 = new bag("PRADA"); System.out.println(bag1.hashCode()); //118352462 System.out.println(bag2.hashCode()); //1550089733 System.out.println(bag3.hashCode()); //1550089733 Set<bag> set = new HashSet<bag>(); set.add(bag1); System.out.println(set.contains(bag1)); //true System.out.println(set.contains(bag2)); //false
这就很奇怪,按照我们之前写到equals,bag1是和bag2相等的,所以set.contains(bag2)应该也为true才是。这就涉及到了hash类(hashSet,hashMap)的查询原理了。
查询一个值的过程首先是根据hashcode计算散列值,然后使用散列值查询数组。(数组中保存list,使用equals进行线性查询),所以hashcode可以相同,但是最好的状态是均匀分布这样更快。
这就是hashMap查询很快的原因了。
改正上面的程序:
@Override public boolean equals(Object that) { return (that instanceof bag) && this.sameValue((bag) that); } boolean sameValue(bag that1) { return this.label == that1.label; } public int hashCode() { return this.label.hashCode(); } public static void main(String[] args) { bag bag1 = new bag("CHANEL"); bag bag2 = new bag("CHANEL"); Object bag3 = bag2; bag bag4 = new bag("PRADA"); System.out.println(bag1.hashCode()); //1986660217 System.out.println(bag2.hashCode()); //1986660217 System.out.println(bag3.hashCode()); //1986660217 Set<bag> set = new HashSet<bag>(); set.add(bag1); System.out.println(set.contains(bag1)); //true System.out.println(set.contains(bag2)); //true }
其实这些细节什么的,不注意的话,也许没什么问题,但也许作为程序潜在的bug,也许会产生很严重的后果。
但是还有一个问题就是,如果这个变量是mutable的话(我添加了一个改label的mutator),那么我修改了里面的内容的话,那么hashSet里面就找不到了
public void changelabel(String string) { this.label =string; } public static void main(String[] args) { bag bag1 = new bag("CHANEL"); bag bag2 = new bag("CHANEL"); Object bag3 = bag2; bag bag4 = new bag("PRADA"); System.out.println(bag1.hashCode()); //1986660217 System.out.println(bag2.hashCode()); //1986660217 System.out.println(bag3.hashCode()); //1986660217 Set<bag> set = new HashSet<bag>(); set.add(bag1); System.out.println(set.contains(bag1)); //true bag1.changelabel("DIOR"); System.out.println(set.contains(bag1)); //false }
所以对于可变数据类型,我们就不要改了,让equals() 应该⽐较索引,就像 == ⼀样即⽐较⾏为相等性就可以了。