本文共 1830 字,大约阅读时间需要 6 分钟。
什么时候需要覆盖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
- 一致性。多次调用x.equals(y)返回一致的结果
- 非空性。对于任何非null的引用值x,x.equals(null)必须返回false
不要忽略这些约定,程序很有可能会运行不正常,直至崩溃。因为没有一个类是孤立的,类之间是相互传递调用的。例如集合类,依赖于传递给它的对象是否遵循equals约定。HashMap的get(key)方法,先是利用key的hash值找到数组的下标,如果发生hash冲突就是再利用equals进一步判断的。违反这些约定会导致,刚放入的一个键值对却发现无法通过键获取到值。
违反通用约定的结果
- 自反性。你把一个对象放到集合中,之后去取的时候告诉你不存在该对象
- 对称性。x.equals(y)为true,不同类的contain(x)和contain(y)返回的结果无法确定
- 传递性。子类添加属性会影响到equals的比较结果,我们无法在扩展可实例化的类的同时,及增加类的值组件,同时保留equals约定,除非愿意放弃面向对象所带来的优势
- 一致性。不要使equals方法依赖于不可靠的资源
- 非空性。
实现高质量equals方法的诀窍
- 使用==判断参数是否为这个对象的引用
- 使用instanceof判断参数是否为正确的类型
- 把参数转换为正确的类型
- 对该类中的每一个关键域,判断是否和参数中对应的域相匹配(引用类型递归调用equals方法,除float和double的基本类型使用==)
- 完成equals方法之后,确定是否满足5个通用约定
注意事项
- 覆盖equals方法总要覆盖hashCode方法
- 不要让equals方法过于智能
- equals方法入参参数保持为Object
覆盖equals方法时总是要覆盖hashCode
很常见的错误来源就是没有覆盖hashCode方法。例如hashMap中会使用到散列码,put方法将key存储在一个散列桶中,两个相等的实例具有不同的散列码会导致get方法返回null。
覆盖hashCode方法,必须遵守的通用约定
- 在应用程序执行期间,equals所用到的信息没有被修改的情况下,调用多次hashCode方法都返回同一个整数
- 如果两个对象equals,那么这两个对象调用hashCode返回一样的整数结果。相等的对象必须具有相等的散列码
- 如果两个对象不equals,那么这两个对象调用hashCode不要求返回一样的整数结果
为不相等的对象产生不同的散列码
- 把一个非零值,比如17,保存在一个int类型的result变量中。
- 对对象的每一个关键域,循环完成以下操作:
- 为该域计算int类型的散列码c
- 如果该域类型为boolean,则计算(f?1:0)
- byte、char、short、int,计算(int)f
- long,计算(int)(f^(f>>>32))
- float,计算Float.floatToIntBits(f)
- double,计算Double.doubleToLongBits(f),然后再计算(int)(f^(f>>>32))
- 引用类型,递归调用equals方法
- 数组,需要把每一个元素当作独立的域来处理,可以使用Arrays.hashCode方法
- 计算 result = 31*result + c
- 返回result
- 确定是否满足“相等的对象具有相等的散列码”
为何选择31
- 31是一个素数。如果乘数是偶数,乘法溢出的话信息会丢失。31*i 虚拟机可以优化为 (i<<5)-i,利用位移运算代替乘法可以提高性能。
只覆盖hashCode,不覆盖equals有什么影响?
哈希码的作用是为了提高哈希表的性能,让数据在哈希表中分布的更均匀。不覆盖equals方法不影响其他功能的使用,但是覆盖了equals一定要覆盖hashCode。
各位看官觉得呢?有问题请指出。
转载地址:http://amrai.baihongyu.com/