equals和hashCode你学会了么?

时间:2022-07-22
本文章向大家介绍equals和hashCode你学会了么?,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作为在Object中的equals方法和hashCode方法,或多或少我们在子类中都有重写过这两个方法,那么我们在重写这两个方法时需要注意些什么?就让我们通过这篇文章来聊一聊。

equals

什么时候覆盖equals方法我就不再说了,相信熟悉Java的读者肯定都知道,我们着重聊一下如何书写规范的equls方法。因为一个不规范的equals方法将会造成集合无法表现出预期的行为。

重写equals方法需要保证equals满足以下特性:

  • 自反性:对于任何非null的引用值x,必须满足x.equals(x)返回true
  • 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true并且y.equals(z)返回true时,那么x.equals(z)也必须返回true
  • 一致性:对于任何非null的引用值x、y,只要equals涉及的字段信息没有被修改,多次调用x.equals(y)要返回一样的结果
  • 非空性:对于任何非null的引用值x,x.equals(null)必须返回false

自反性

该要求说明对象必须等于自身,如果违背这一条你会发下集合的contains方法无法告诉你正确的结果。

对称性

对称性要求两个对象对于它们是否相等要保持一致,下面这段代码违反了对称性

public class IgnoreCaseString {
    private final String s;
    public IgnoreCaseString(String s) {        this.s = s;    }
    @Override    public boolean equals(Object obj) 
    {        if (obj instanceof IgnoreCaseString) 
            {            return s.equalsIgnoreCase(((IgnoreCaseString) obj).s);        }        
            if (obj instanceof String) {            return s.equalsIgnoreCase((String) obj);        }        return false;    }
    public static void main(String[] args) 
    {       IgnoreCaseString ignoreCaseString = new IgnoreCaseString("Phone");        
            String string = "phone";        
            System.out.println(ignoreCaseString.equals(string));        
            System.out.println(string.equals(ignoreCaseString));        
            System.out.println("------------------------------");        
            List<String> list = new ArrayList<>();        
            list.add(string);        
            System.out.println(list.contains(ignoreCaseString));    
    }
}

传递性

这种情况通常发生在具有父子关系的对象中,子类增加的信息会影响到equals的比较结果。解决这种问题通常有两种方式,一种是通过getClass()的方式(具体的大家可以通过阅读Effective Java这本书),还有一种是在我们扩展类的功能时尽量使用复合而并不是使用继承,通过复合组件里面的域的比较也可以解决。

一致性

一致性要求如果两个对象相等,那么他们就必须保持相等,除非它们中有对象被修改了。

非空性

非空性要求所有的对象不等于null

如何写好equals

  1. 如果比较操作昂贵,可以优先使用==操作符检查是否是同一个对象引用
  2. 使用instanceOf操作符检查参数是否为正确的类型(正确的类型通常是指equals方法所在的那个类,但有时也是该类实现的某个接口,比如Set、List等集合)
  3. 把参数转换为正确的类型
  4. 对于该类中的每个关键字段,检查参数中的字段是否与该对象中对应的字段相匹配,对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较,对于引用类型可以递归调用equals方法,对于float使用Float.compare方法,double使用Double.compare方法,数组域可以使用Arrays.equals方法。
  5. 最最重要的一步,当编写完equals方法一定要进行单元测试,验证equals方法是否满足上述特性。

hashCode

在每个覆盖了equals方法的类中,必须重写hashCode方法。如果不这样做,会导致该类无法与所有基于散列的集合一起正常运作。hashCode方法需要遵循以下规定:

  • 在程序执行期间,只要对象的equals方法比较操作用到的信息没有被修改,那么对于同一个对象调用多次hashCode方法必须返回同一个整数。但是在一个应用程序的多次执行中,每次执行可以返回的整数可以不一致。
  • 如果两个对象的根据equals方法比较结果是相等的,那么调用任意一个对象的hashCode方法都必须产生相同的结果
  • 如果两个对象根据equals方法比较结果不想等,那么调用这两个对象的hashCode方法可以产生相同或者不同的整数结果,但是尽量保证程序可以产出不同的整数因为这样可以提高散列表的性能

如何写好hashCode

  1. 把某个非零的常数值,比如说17保存在一个result的int类型变量中
  2. 对于对象中equals方法中涉及到的每一个域(f)计算散列码
  3. 按照result = 31 * result + c(第二步计算的散列码)合并到result中并返回
  4. 验证自己的hashCode方法

使用非0初始值的原因是让域初始值为0的那些域可以影响到散列值,如果使用0那么散列值不会再受这些域的影响,从而增加Hash冲突的可能性,从而降低了散列表的性能

31是一个奇素数,在Java中如果两个比较大的数相乘则会发生移除,31并不算一个比较大的数,其次选用31的理由是可以用移位和减法来代替乘法,31 * i = (i << 5) - i,现在的VM都可以自动完成这种优化,因此可以获得很好的性能提升

计算散列码
  • 如果f是boolean类型,计算f? 1 :0
  • 如果f是byte、char、short或者int类型,计算(int)f
  • 如果是long类型,计算(int)(f ^ (f>>>32))
  • 如果是float类型,计算Float.floatToIntBits(f)
  • 如果是double类型,计算Double.doubleToLongBits(f),然后在将得到long结果计算散列值
  • 如果是一个引用类型,递归调用hashCode
  • 如果是是一个数组,调用Arryas.hashCode方法

如果一个类是不可变的并且计算散列码的成本比较大,可以考虑把散列码缓存在对象内部,而不是每次请求时都重新计算(这一点在Kafka中也有所应用)。