从一个小问题出发来理解Set中的hashCode与equals

本文深入解析了集合类中的hashCode和equals接口的使用,通过具体案例展示了如何正确实现这两个接口以满足特定需求,强调了它们在对象比较、哈希表操作中的作用。通过实际代码示例,阐述了它们在Set集合使用中的重要性和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集合类中的hashCode和equals是两个比较容易混淆的接口。我在准备面试的时候就经常被绕晕,后来把区别索性死记硬背了下来。最近又用到了这一知识,才下决心搞明白它。

先说一下我用到的场景。有一个自定义的类Event,其中包含了若干字段:

public class Event {
    String systemId;
    String logType;
    
    /* 其他接口 */
    ...
}

我需要用个Set来收集Event对象:

Set<Event> set = new HashSet<Event>();

这里有以下几个要求:

  1. 当Event中systemId和logType两个字段(String类型)相同时,判断Event对象相同

  2. 当set中已包含相同Event对象的时候,Set.contains接口返回true

  3. 向set添加Event时,当set中包含有相同Event时,不再重复添加


第一次尝试:修改equals

既然判断Event对象是依靠systemId和logType两个字段,那我直接改写equals接口不就好啦!

equals接口改写如下:

@Override
public boolean equals(Object o) {
    if (!(o instanceof Event)) {
        return false;
    }
    return ((Event)o).systemId.equals(systemId)
            && ((Event)o).logType.equals(logType);
}

然而测试发现,当set内有字段相同的Event对象时,contains并不能返回true;而向set添加具有相同字段的Event时,并不能避免重复添加;第一次尝试失败。


错误探究

为了探究错误原因,翻出HashSet源码了解一下。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
    ...
}

HashSet里面使用一个HashMap来实现其功能,加入HashSet的值都作为key被添加到HashMap中去。

再看HashMap源码:

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
        return e;
    }
    return null;
}

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

在HashMap中寻找一个key,共分为两个步骤:

  1. 根据key的哈希值找到key所在的Entry

  2. Entry其实是一个链表,遍历次链表,通过key的equals接口寻找相同的对象

  3. 若找到相同对象,则返回此Entry,否则返回null


向HashMap中添加一个key时,也是安装相似的步骤:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
     }

     modCount++;
     addEntry(hash, key, value, i);
     return null;
}

不同的是步骤3,当找到相同key的时候,用新key替换;若没有相同key,则把此key添加上。

这里我们可以看到若想在HashMap中找到相同的对象,不仅要保证二者能通过equals验证,还要有相同的哈希值,这个哈希值就是通过hashCode接口来定义的。


第二次尝试:修改hashCode

在第一次尝试工作的基础上,再次重构hashCode:

@Override
public int hashCode() {
    return this.toString().hashCode();
}

@Override
public String toString() {
    return "System ID: " + systemId + ", Log type: " + logType;
}

测试结果符合最终要求。

总结

通过上述探讨我们可以看到,equals和hashCode其实是两个截然不同的概念,及Comparision和Hashing。前者是单纯的对象比较,后者则是在做哈希寻址的时候为确定对象的地址提供参数。因为相同的对象一定要保证在同一个哈希slot中,故引出了以下原则:equals相同的时候,一定要保证hashCode相同;反之,hashCode相同的时候,equals不一定相同。这是因为哈希值毕竟是根据对象的属性计算出的一个整形结果,所以不同对象有可能哈希值相同,这种情况下两个不同的对象就会寻到同一个slot,即哈希冲突,在哈希冲突情况下,就要靠equals来判断冲突的对象是否相同了。而当修改equals的时候,一定要同时修改hashCode,否则Set根本不会将两个“相同”对象放到同一个slot里,更没机会调用equals啦!我之所以走了这么多弯路,就是因为改写equals的时候没有同时修改hashCode。不过话说回来,不犯这么愚蠢的错误,也不会了解这么多啦。愚蠢的错误中往往发现真理呀!





转载于:https://my.oschina.net/dokia/blog/551271

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值