java-集合中的去重原理及源码分析

set集合和map集合中为什么要重写equals和hashcode 方法

Object中的原生方法:

Object中

  • hashcode():返回的是对象的地址值,所以这种情况下不同对象的hashcode肯定不同
  • equals():比较的是对象的地址

知识拓展(及其重要)

  • 1.hashmap和hashset中都是不允许元素重复的,那么我们每次加元素进去的时候,都要进行比较,是否元素重复了
  • 2.那么我们是否可以用Object里的equals方法来判断元素是否相同呢?答案是不行的,因为Object中的equals方法是判断对象地址的,如果是两个不同的对象,但是里面的内容相同,通过object中的equals方法同样返回的是不等,那么还会造成重复添加元素的问题,所以这里我们就要有一个结论,必须重写equals方法!(如何重写下面会讲到,无非就是判断内容是否相等)
  • 3.在我们重写equals方法后,我们又会发现一个问题。当集合中的数据量过大的时候,我们每次添加元素都要调用成千上万次的equals方法,那么这就使我们的代码效率非常低,所以这就是为什么我们要改写hashcode的原因。
    4.我们刚刚讲到Object中的hashcode方法是直接返回对象地址的,也就是说这种hashcode方法也无法根据对象的内容生成散列值(也就是方法返回的值),所以我们要改写Object中的hashcode方法,使其根据对象的内容生成一个散列值,每次插入一个新的对象,都要生成一个散列值,将其插入一个table中,那么这个table有什么用呢?
  • 5.实际上hashmap,hashset中判断元素是否重复是有两个过程的,首先生成插入新对象的hashcode,到我们刚刚寻找的table中去寻找(这种查询方式很快,用了哈希表的原理),当我们发现没有不同的散列值的时候,就可以判断这是一个不重复的元素,就可以直接插入了。当我们发现table已经有相同的散列值的时候,!!!并不是可以直接判断此对象就是重复元素了,还要调用equals方法,再判断一次,如果判断的结果还是真,那么就真的是重复元素了,就不存了,如果equals判断为假,那么就重新散列其他的值,再进行插入判断。

问题:为什么hashcode方法判断完为重复了,还要让equals再判断一次

  • 答:实际上hashcode方法(重写之后)的原理仅仅是根据对象内容去返回一串定长的数字,但是当数据量很大的时候,总会出现不同内容生成同样散列值的情况,因为这串数字是定长的!所以这就是关键了。此时就发生了哈希碰撞

总结:

  • hashcode相同,元素对象不一定重复
  • hashcode不同,元素对象一定不重复
  • euqals判断是最精确的,用hashcode做第一步的初步判断是为了提高程序效率

当引用类型为 String 的时候(String;类中重写了equals方法)

  • String类中的hashcode:根据字符串内容生成的一串数字,也就是说,一般情况下(这里就是指不出现不同元素相同散列值的情况)只要字符串的内容相等,那么这两个String对象的散列值就相同。
  • String类的equals:是去判断两个字符串每一个字符是否相等,最精准的判断

当引用类型为自定义类型的时候,如果想要达到去重的效果,就必须进行重写里面的hashcode()和equals()方法

public interface Set<E> extends Collection<E> {
    boolean add(E e);
}

public class HashSet<E> extends AbstractSet<E> implements Set<E>{
    private static final Object PRESENT = new Object();
    private transient HashMap<E,Object> map;

    public boolean add(E e) {
        //e -- "hello"
        //PRESENT -- new Object()
        return map.put(e, PRESENT)==null;
    }
}


public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    //key -- "hello"
    //value -- new Object()
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    //这个方法可以简单理解为调用元素类中的hashCode()方法计算哈希值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {

        //简单理解为哈希表中存储的是一个一个的节点数组
        Node<K,V>[] tab;
        Node<K,V> p;
        int n, i;

        //判断哈希表是否已经做完初始化,如果发现没有做初始化,就在这里给它做初始化
        if ((tab = table) == null || (n = tab.length) == 0){
            n = (tab = resize()).length;
        }

        //根据元素对象计算好的哈希值再进行一次与运算,计算出来的是该元素在哈希表中存储的位置
        //如果这个位置上是null,说明该位置上暂且是没有元素的,可以进行存储
        //创建新的节点,在该位置上存储元素
        //分析到这一步,发现存储的过程是与元素类中的hashCode()结果相关的。
        if ((p = tab[i = (n - 1) & hash]) == null){
            tab[i] = newNode(hash, key, value, null);
        }else {
            Node<K,V> e;
            K k;
            //看到这里我们得出判断元素的哈希值是否一样并且判断元素的内容是否一样
            //根据元素类中的equals()方法进行判断
            //如果哈希值一样说明存储的位置一样
            //如果equals()比较的结果一样,说明内容一样(前提是元素类中重写了equals方法,否则调用的是父类Object的equals方法)
            //这样的话,比较的是地址值
            //如果都一样,不搭理他,从现象上来看,就是没有赋值
            //看到这里,我们其实没有必要再往后看了
            //已经知道了代码中add()方法,其实是和元素类中的hashCode()和equals()方法有关
            //会不会去重取决于该元素类中有没有重写hashCode()和equals()方法
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

}

————————————————
版权声明:本文为CSDN博主「我是方小磊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44844089/article/details/103681519

posted @   a-tao必须奥利给  阅读(120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示