hashCode 和 equals 用来标识对象,两个方法协同工作可以用来判断两个对象是否相等。根据生成的哈希码将数据离散开来,可以使存取数据更快。对象通过调用 Object.equals() 生成哈希值;由于不可避免地会存在哈希值冲突的情况,因此当 hashCode 相同时,还需要再调用 equal 进行一次值的比较;但是 hashCode 不同,直接判定 Objects 不同,跳过 equals ,这加快了冲突处理效率。 Object 类定义中对 hashCode 和 equals 要求如下:
如果两个对象的 equals 的结果是相等的,则两个对象的 hashCode 的返回结果也必须是相同的。
任何时候重写 equals ,都必须同时重写 hashCode。
在 Map 和 Set 类集合中,用到这两个方法时,首先判断 hashCode 的值,如果 hash 相等,则再判断 equals 的结果,HashMap 的 get 判断代码如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 而其中 getNode 方法中有一个判断如下
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
if 条件表达式中的 e.hash == hash 是先觉条件,只有相等才会执行后面的判断逻辑。如果不相等,后面的判断逻辑也就不会执行。equals 不相等时并不强制要求 hashCode 也不相等,但是哈希算法的初衷是尽可能让元素均匀分布,降低哈希冲突的概率,即在 equals 不相等时尽量使 hashCode 也不相等,这样 && 或 || 短路操作一旦生效,会极大地提高程序的执行效率。如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。此外,因为 Set 存储的是也是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的自定义对象也必须重写这两个方法。而具体重写了 equals方法,但是没有重写 hashCode 方法,会有什么影响。下面通过实例来讲解下
public class EqualsObject {
private int id;
private String name;
public EqualsObject(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1
if (o == null || getClass() != o.getClass()) return false; // 2
EqualsObject that = (EqualsObject) o;
return id == that.id && Objects.equals(name, that.name);
}
}
1 处先判断是否是统一对象,是的话直接返回 true
2 处说明,首先判断两个对象的类型是否相同,如果不匹配,则直接返回 false。另外此处使用了 getClass 的方式,就是严格限制了只有 Eqauls 对象本身才可以执行 equals 操作。
注意,上例代码中并没有重写 hashCode 方法,然后我们将上例对象加入到 HashSet 中测试下:
最终输出的结果是3。但是这些 EqualsObject 对象明显是相同的,但是在 HashSet 中,应该只会存在一个。之所以结果为 3 ,是因为如果不重写 hashCode() ,即使 equals() 相等也毫无意义,Object.hashCode 的实现是默认为每一个对象生成一个不同的 int 数值,他本身是 native 方法,一般与对象的内存地址有关。
在没有重写 equals 方法时,我们直接调用的是 Object类里面的equals方法,这个方法比较的是两个对象的内存地址,如果通过new 关键字新建的两个对象,
User user1 = new User("小明", "20", "湖南");
User user2 = new User("小明", "20", "湖南");
对应的属性都完全相同,但是它们分配的内存地址是不一样的,也就是属于不同的对象,因此调用这个方法时就是为false,但是明显不符合的我们想要的结果,在我们看来,这两个对象就是同一个对象,它们应该返回的是true。因此,想要让这两个对象相等,我们就需要重写 equals 方法。但是 hashCode 方法是根据内存来计算的,在保证这两个小明都是同一个人(同一对象)的前提下,它们对应的hashCode理应也应该是一样的,但是通过new关键字新建的对象对应的hashCode也明显不一样,这时候也需要我们重写hashCode方法,人为地返回true。
例如用到map的时候,map是根据key的hashCode和key.equals是否都相等判断是否存在重复的entry的。所以如果我们从map中put进两个值(key都为 new User(“小明”, “20”, “湖南”) ),而没有重写hashCode,得到的结果(size = 2)和我们期望的结果(size=1)将不一样
在hashMap中,当存入一个key为username,值为null的数据时,调用了map.get(“username”),get() 方法的返回值时null。假设当前我们并不知道hashMap是否存在key为username的键值对,这时候我们需要有个方法来判断这个null到底是我们存进去的,还是这个key不存在而返回的null。在单线程的情况下下,我们通过contains方法来验证下
if (map.contains("username")){
return map.get("username");
}
但是在ConcurrentHashMap或者HashTable中,如果允许键值都能为null,并且在多线程的情况下,当一个线程执行完 map.contains("username")
时,其他一个线程已经把这个username的键值对给删掉了,这个时候我们通过get("username")
得到其实是键值对不存在的null,而非我们存进去的key为username,值为null的null。
如果键值不允许为null?假设当前有个键为username,值为xiaoming,执行到map.contains(“username”)时返回true,此时第二个线程进来把该键值对删除了,map.get(“username”)就会返回为null,而此时这个null就能很明确的表明该键不存在,而不是模棱两可(1.这个值就是null2.这个键不存在)