文章目录
1. Object equals方法与hashCode
Java Object类中equals方法为:
public boolean equals(Object obj) {
return (this == obj);
}
所以如果没有重写Object类的equals方法,那么equals与==计算结果是相同的。
Object类的hashCode方法是一个native方法,看不到具体实现,hashCode返回的是由对象存储地址转化得到的值
public native int hashCode();
2. 重写equals方法
首先需要说明的是重写equals方法看似很简单,但是可能会产生非常多的问题,大多数情况下我们并不需要重写equals方法,如果实在需要重写equals方法,推荐采用Google开源的Auto Value 框架,它会自动生成equals和hashCode方法,或者IDE也有工具生成这些方法(更不推荐这个方法),但是都比自己重写会少很多问题
重写的equals方法需要满足五个性质
-
自反性:对象必须等于其自身
-
非空性:所有对象都不能等于null
-
对称性:如果a.equals(b)为true,那么b.equals(a)必须也为true
重写equals方法是很有可能出现这个问题的,例如你写了一个类CaseInsensitiveString,重写equals要 求字符串不区分大小写,那么a是CaseInsensitiveString类型,值为"Pow",b为String类型,值为“pow”,那么a.equals(b)为true,而b.equals(a)调用的是String的equals方法,区分大小写,则为false,违反了对称性。
如何处理这种情况呢,可以禁止与String互操作,代码如下
@Oveerride
public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ...
}
对象o 如果是String类型,那么o instanceof CaseInsensitiveString则为false,对称性得到保证。同时还保证了非空性。
还有个问题需要思考,例如一个类A有属性x,y,A重写了equals方法,类B继承了类A,同时增加了z属性,但是沿用了A的equals方法,那么B的实例k和m在做equals比较的时候z属性会被直接忽略掉,虽然不违反equals约定,但是明显这种比较是不正确的。
如果我们重写子类的equals方法呢,那么A类实例a与B类实例b,a.equals(b)的结果为true,因为z属性将不会拿来比较,会被忽略,b.equals(a)结果为false,因为z属性会被拿来比较,但是a中没有z属性,这又违反了对称性。处理这个问题有一个权宜之计,就是“复合优先于继承”,不采用继承的方法。当然,如果继承的是抽象类的话,因为抽象类无法实例化对象,也就不会产生这些问题。
-
传递性:如果a.equals(b)为true,b.equals©为true,则a.equals©也为true
-
一致性:相等的对象永远相等,不相等的对象永远不相等
对了,还要考虑一个问题,不要将equals声明重的Object对象替换成其它类型。
public boolean equals(MyClass o)
如果你写成这样,就是重载,而不是覆盖,可能到最后你会花上数小时都不知道为什么得不到自己想要的结果。重写的时候带上注解@Override可以避免这种问题
重写equals方法必须重写hashCode方法
看很多文章都提到了这一点,但是只有一句因为equals相等时hashCode必须相等,所以要重写equals方法,第一次看的时候特别懵,琢磨了挺久才明白是怎么一回事。
首先要明白HashCode与equals的关系
- 应用程序执行期间,对象的equals方法的比较操作所用到的信息没有修改,则同一个对象的hashCode方法都必须返回同一个值。
- 如果equals结果为true,则两个对象的hashCode必须相等(重点)
- 如果equals结果为false,则hashCode值不一定不同,例如“重”,“地”的hash值就相同,这个现象叫做哈希碰撞
所以,map在查找key的时候,先用哈希函数找到相同的key(hashCode相同值不一定相同),然后再用equals判断是否相同。如果hash值都不相同,那么就没必要进行equals操作了,大大提高了效率。
所以当你想要重写equals方法时,一定要想到我重写后的equals方法为true的两个对象,如果我不重写hashCode方法,会不会导致两个对象的hash值不同,违反了上文的重点要求
可以自己写个例子试验一下,创建一个Mychar类,有个char类型数组chars,重写了equals方法但是没用重写hashCode方法(随便写的,只是为了说明问题,可能没有保证五条性质)
@Data
@AllArgsConstructor
public class MyChar {
private char[] chars;
@Override
public boolean equals(Object obj) {
if(!(obj instanceof MyChar)){
return false;
}else{
MyChar anotherObj = (MyChar)obj;
for (int i = 0; i < chars.length; i++) {
if (this.chars[i] != anotherObj.chars[i]){
return false;
}
}
return true;
}
}
}
编写测试类Test,测试以下项目
public class Test {
public static void main(String[] args) {
char[] Char = new char[]{'a','b'};
MyChar myChar1 = new MyChar(Char);
MyChar myChar2 = new MyChar(Char);
System.out.println(myChar1 == myChar2); //是否时同一对象
System.out.println(myChar1.equals(myChar2)); //逻辑上是否相等
System.out.println(myChar1.hashCode()); // hash值
System.out.println(myChar2.hashCode());
}
}
false
true
1878246837
929338653
进程已结束,退出代码为 0
可以看到两个不是同一对象equals的结果为true,但是hash值完全不相同,这就是不重写hashCode方法的后果。
我们都知道String类重写了equals和hashCode方法,可以学习一下它的方法,具体代码如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
3. 结语
总之尽量不要自己重写equals方法,除非迫不得已,如果要重写推荐采用Google的开源框架Auto Value实现,通常优于手工实现它们。
4. 参考文献
本文主要参考《Effective Java》第三版,其它参考资料略。