欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

HashMap源码解析

程序员文章站 2022-03-29 10:21:07
一、HashMap概述 HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 值得注意 ......

HashMap源码解析:

本文除了自己看了些源码,也看了很多博客,还有慕课网的专栏。本文的用途主要是为了自己复习。

1.概览:

HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。

1.1底层结构

在JDK1.7时,HashMap是数组+链表的结构。在JDK1.8,HashMap是数组+链表+红黑树结构。当链表长度大于等于8时,会变成红黑树结构,红黑树节点数小于等于6时,会退化成链表。

HashMap源码解析

(图片来自慕课网专栏:面试官精讲Java源码及大厂真题)

1.2常见属性:

  private static final long serialVersionUID = 362498820763181265L;
 
    /**
     * 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 
    /**
     * 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
 
    /**
     * 默认装填因子0.75,如果当前键值对个数 >= HashMap最大容量*装填因子,进行rehash操作
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
    /**
     * JDK1.8 新加,Entry链表最大长度,当桶中节点数目大于该长度时,将链表转成红黑树存储;
     */
    static final int TREEIFY_THRESHOLD = 8;
 
    /**
     * JDK1.8 新加,当桶中节点数小于该长度,将红黑树转为链表存储;
     */
    static final int UNTREEIFY_THRESHOLD = 6;

/**
     * 桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。
     * 应该至少4*TREEIFY_THRESHOLD来避免扩容和树形结构化之间的冲突。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
 
    /**
     * JDK1.6用Entry描述键值对,JDK1.8中用Node代替Entry
     */
    static class Node<K, V> implements Map.Entry<K, V> {
        // hash存储key的hashCode
        final int hash;
        // final:一个键值对的key不可改变
        final K key;
        V value;
        //指向下个节点的引用
        Node<K, V> next;
 
        //构造函数
        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
 
        public final K getKey() {
            return key;
        }
 
        public final V getValue() {
            return value;
        }
 
        public final String toString() {
            return key + "=" + value;
        }
 
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
 
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
 
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

// 哈希桶数组,分配的时候,table的长度总是2的幂
 transient Node<K,V>[] table;

//HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能
    transient Set<Map.Entry<K,V>> entrySet;
// 实际存储的数量,则HashMap的size()方法,实际返回的就是这个值,isEmpty()也是判断该值是否为0
    transient int size;
//修改次数
    transient int modCount;
//HashMap扩容的阈值
    int threshold;
//负载因子
   final float loadFactor;

2.重要方法解析:

2.1构造函数

//自定义初始化容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

//自定义初始化容量、使用默认的负载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 //无参构造函数 ,使用默认的初始容量和负载因子
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  //以一个Map构造一个HashMap
     public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

2.2 新增 put 方法

HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  /**
     *
     * @param hash         指定参数key的哈希值
     * @param key          指定参数key
     * @param value        指定参数value
     * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
     * @param evict        如果为false,数组table在创建模式中
     * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
     */
   public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 参数解析:onlyIfAbsent表示,如果为true,那么将不会替换已有的值,否则替换
// evict:该参数用于LinkedHashMap,这里没有其他作用(空实现)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // tab是该方法中内部数组引用,p是数组中首节点,n是内部数组长度,i是key对应的索引下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次put的时候,table未初始化,也就是tab为空,需要扩容
    if ((tab = table) == null || (n = tab.length) == 0)
    	// 这里实现扩容,具体逻辑稍后分析
        n = (tab = resize()).length;
    // 获取指定key的对应下标的首节点并赋值给p,如果首节点为null,那么创建一个新的Node节点作为首节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // p此时指向的是不为null的首节点,将p赋值给e
            // 如果首节点的key和要存入的key相同,那么直接覆盖value的值(在下一个if中执行的)
            e = p;
        else if (p instanceof TreeNode)
        	// 如果首节点是红黑树的,将键值对插添加到红黑树,该方法后续分析
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 能走到这里,说明首节点和新put的的这个节点的key不一样,且该Node节点不是TreeNode类型
        	// 开始需要遍历链表,如果链表中存在该键值对,直接覆盖value。
        	// 如果不存在,则在末端插入键值对。然后判断链表长度是否大于等于8(其实就是遍历次数 + 1),
        	// 尝试转换成红黑树。注意此处使用“尝试”,因为在treeifyBin方法中还会判断
        	// 当前哈希表长度是否到达64,如果达到,转换为红黑树,否则会放弃次此转换,优先扩充数组容量。
            for (int binCount = 0; ; ++binCount) {
            	// 当binCount = 0时,也就是第一次if判断,此时p就是首节点,p.next就是第二个节点
            	// 其他情况及时链表中其他节点,当e == null的时候,也就是到达了链表的结尾
                if ((e = p.next) == null) {
                	// 新建一个Node并作为链表的最后一个节点
                    p.next = newNode(hash, key, value, null);
                    // 判断遍历次数是否>=7(首节点未遍历,直接从第二个节点开始遍历的,当次数为7时,说明链表长度为8)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	// “尝试”将链表转换为红黑树,内部还会判断哈希表长度,不一定转换成功,也许是扩容
                        treeifyBin(tab, hash);
                    break;
                }
                // 只要没走到上面那个if里面,说明链表没有遍历结束,如果在链表中间发现有key一样的,那么就直接将旧值替换成新值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 将正在遍历的节点赋值给p,方便能遍历下一个节点
                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;
}

//链表转化为红黑树的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 在这里判断是否满足扩容条件,如果满足就扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    	// 到这里开始遍历链表
        TreeNode<K,V> hd = null, tl = null;
        do {
        	// 将链表中的节点Node类型转换成为TreeNode
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // TreeNode链表转换成为红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}



HashMap源码解析

  • 小结:
  • 1.如果table[ ]哈希表没有被初始化,则调用resize()进行初始化
  • 2.根据key的hash值计算下标(n-1)&hashcode,如果索引对应的桶为null的话直接新建结点添加。
  • 3.如果下标对应的桶中的元素不为null
  • 3.1判断桶中第一个元素的key是否与要添加的键值对的key是否相同,相同就直接覆盖
  • 3.2不相同判断该元素是否为红黑树结点,是红黑树结点的话就执行红黑树的插入操作
  • 3.3不是红黑树结点,那就是链表的结点了。那么就遍历链表,并检查链表结点的key是否与要插入的键值对key是否相同,相同则覆盖,否则就插入到链表的尾部。插入后如果链表的长度大于8,就尝试转换为红黑树结构。如果哈希表的长度大于64,那么就会转换为红黑树结构。
  • 4.判断当前键值对数量是否大于扩容的阈值,是的话就进行扩容操作。

小问题:为什么需要转换为红黑树?
HashMap在JDK8之后引入了红黑树的概念,表示若哈希表的桶中链表元素超过8时(默认哈希表长度不小于64),会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。以6和8来作为平衡点是因为,中间有个差值7可以防止链表和树之间频繁的转换。假设,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。概括起来就是:链表:如果元素小于8个,查询成本高,新增成本低,红黑树:如果元素大于8个,查询成本低,新增成本高。

2.3 扩容方法resize()

  • 扩容的逻辑
  • 1.计算扩容后的容量,临界值。
  • 2.将hashMap的临界值修改为扩容后的临界值
  • 3.根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
  • 4.遍历原哈希表中的结点。
  • 4.1如果桶中的结点不为null,且只有一个结点,那么就重新计算索引存入到新数组中
  • 4.2如果桶中不止一个结点,且是红黑树类型的结点,那么就调用split方法,进行红黑树的剪枝
  • 4.3如果是链表结点,就根据(e.hash & oldCap) == 0判断,将链表中的结点挂在低位链还是高位链上。低位链的索引就是原索引位置,高位链位置是原索引+oldCap(旧容量)

与Jdk1.7不同的是:

(1)rehash方式不同:JDK1.7再哈希是将hash&newCap来重新计算索引的下标的,虽然数组大小扩大了一倍,但是同一个key在新旧哈希表中对应的下标却存在一定联系:要么一致,要么相差一个 oldCap。基于这个结论,那么我们只需要看新下标新增的高位是0或者是1即可。

而JDK1.8通过hash & oldCap来判断新增的高位是0还是1。是0就挂载原索引上,是1就挂在原索引+oldCap上。这样就省去了再哈希的时间。而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。

(2)jdk1.7rehash时,将旧链表迁移到新链表,采用的是头插法,如果在新表的数组索引位置相同,则链表元素会倒置。在高并发的情况下回形成环状链表。造成get时的死循环。

jdk1.8时则使用了尾插法,避免了这种问题。至于为什么1.7使用的是头插法,应该是设计者考虑到,后put进来的值更有可能被访问,所以用的是头插法吧。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 计算老哈希表的容量,如果老哈希表为空,那么容量为0,否则就是老哈希表的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // threshold的值有点特殊,当初始化扩容的时候,它就是哈希表需要扩容到的长度
    // 否则就是capacity * loadFactor了
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	// 能进入到这里,说明不是初始化扩容,当容量已经到达最大的时候,就不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新哈希表长度扩容到原来的两倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
    	// 能进入到这里,说明oldCap = 0,也就是初始化扩容,此时扩容的大小就应该是oldThr的值
    	// 且这个值必然是2的N次幂,前面已经通过方法tableSizeFor进行了计算
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
    	// 如果上述条件都不满足,那么直接使用默认值16作为容量,12作为扩容阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 下面的if是为了计算扩容阈值,因为上面三个条件中,第一和第二都没有计算threshold值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 到这里就完成了扩容后的容量和扩容阈值的计算工作
    threshold = newThr;
    // 创建新的哈希表,容量为newCap,扩容阈值为newThr
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将扩容后的哈希表赋值给table
    table = newTab;
    if (oldTab != null) {
    	// 能进入这里,说明不是初始化扩容
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
            	// 将桶内节点设置为null
                oldTab[j] = null;
                // 如果链表只有一个节点,那么直接重新计算索引存入新数组
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果该节点是红黑树,执行split方法来处理红黑树节点,包括升级、降级、回退到链表等操作
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                	// 说明是链表操作,接下来部分是重点
                	// "lo"前缀代表的是存储原bucket的节点,"hi"前缀代表的是存储在新bucket的节点
                	// loHead是头节点,loTail是尾节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表
                    do {
                        next = e.next;
                        // 从if-else可以看出,对一条链表进行遍历,将链表中的元素通过
                        // 条件(e.hash & oldCap) == 0来进行分类,满足条件的存放在loHead链上
                        // 不满足条件的存放在hiHead链上,且都是尾部插入方式,这和JDK7的头部插入有区别
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 将loHead链放在数组的原位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 将hiHead链放在数组的(j + oldCap)位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}



2.4 get()方法

HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    // tab:内部数组  first: 索引位首节点 n: 数组长度 k: 索引位首节点的key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 数组不为null 数组长度大于0 索引位首节点不为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果索引位首节点的hash==key的hash 或者 key和索引位首节点的k相同(逻辑相等)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            // 返回索引位首节点(值对象)
            return first;
        if ((e = first.next) != null) {
            // 如果是红黑色则到红黑树中查找对应的节点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 遍历链表,查询key相同的节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

把上述代码总结起来就是以下几个步骤:

1.检查哈希表数组是否为null和索引位首节点(bucket的第一个节点)是否为null
2.如果索引节点的hash==key的hash或 key和索引节点的k相同则直接返回(bucket的第一个节点)
3.如果首节点是红黑色则到红黑树查找key相同的节点
4.不是首节点,也不是红黑树,那么就开始遍历链表,获取与key相同键的节点
5.如果都没找到就返回null

红黑树的引入巧妙的将原本O(n)的时间复杂度降低到了O(logn)。

3.线程安全性

首先HashMap是线程不安全的,其主要体现:

#1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

#2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

Jdk1.7时, 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

因此HashMap是非线程安全的,可以使用Collections.SynchronizedMap()来包装HashMap,使其具备线程安全的特性。多线程环境下,可以使用ConcurrentHashMap来代替HashMap,ConcurrentHashMap的用法和HashMap基本一致,

4.红黑树相关知识

**红黑树:**红黑树是一种自平衡的二叉查找树。

**特性:**红黑树的每个节点只能是红色或者黑色、根节点是黑色的、每个叶子节点都是黑色的、如果一个叶子节点是红色的,它的子结点必须是黑色的、从一个节点到该节点的叶子节点的所有路径都包含相同数目的黑色节点。

**左旋:**对节点进行左旋,相当于把节点的右节点作为其父节点,即将节点变成一个左节点。

**右旋:**对节点进行右旋,相当于把节点的左节点作为其父节点,即将节点变成一个右节点。

**插入:**① 被插入的节点是根节点,直接将其涂为黑色。② 被插入节点的父节点是黑色的,不做处理,节点插入后仍是红黑树。③ 被插入节点的父节点是红色的,一定存在非空祖父节点,根据叔叔节点的颜色分类处理。

**删除:**① 被删除的节点没有子节点,直接将其删除。② 被删除节点只有一个子节点,直接删除该节点,并用其唯一子节点替换其位置。③ 被插入节点有两个子节点,先找出该节点的替换节点,然后把替换节点的数值复制给该节点,删除替换节点。

**调整平衡:**在插入和删除节点后,通过左旋、右旋或变色使其重新成为红黑树。① 如果当前节点的子节点是一红一黑,直接将该节点设为黑色。② 如果当前节点的子结点都是黑色,且当前节点是根节点,则不做处理。③ 如果当前节点的子节点都是黑色且当前节点不是根节点,根据兄弟节点的颜色分类处理。

5.相关面试题

(1)为什么初始化容量是16?

主要是为了服务key映射到索引的算法 index = HashCode(Key) & (Length- 1)。hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。

而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。

为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。

首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。

另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16

(2)负载因子为什么是0.75?

这其实是出于容量和性能之间平衡的结果:

当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;

而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。

所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。

(3)为什么重写equals方法时要重写hashcode方法。

​ 在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样

  • 对于值对象,==比较的是两个对象的值
  • 对于引用对象,比较的是两个对象的地址

大家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,也就是说”帅丙“和”丙帅“的index都可能是2,在一个链表上的。

我们去get的时候,他就是根据key去hash然后计算出index,找到了2,那我怎么找到具体的”帅丙“还是”丙帅“呢?

equals!是的,所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。

不然一个链表的对象,你哪里知道你要找的是哪个,到时候发现hashCode都一样,这不是完犊子嘛。

参考博客和文章的链接:

https://itlemon.blog.csdn.net/article/details/104271481

https://mp.weixin.qq.com/s/0Gf2DzuzgEx0i3mHVvhKNQ

本文地址:https://blog.csdn.net/Iloveyshsh/article/details/107363556