基于JDK1.8源码分析HashMap容器

前言

基于JDK1.8源码解析Map集合类下的HashMap,从源码的角度出发,分析HashMap的运行原理,总结HashMap的知识点,以及HashMap与HashTable的对比。

本来想把TreeMap和LinkedHashMap放到一起写的,但是考虑到篇幅原因,这里先主要分析一下HashMap的源码,再说说JDK1.7到JDK1.8这中间HashMap的改动以及HashMap和HashTable的区别;至于TreeMap和LinkedHashMap,放到其它地方在开一篇总结。

1、HashMap介绍

先从整体角度先分析一波HashMap,对HashMap有一个大概的把握。

1.1 HashMap顶部注解

在阅读HashMap的源码之前,先看HashMap的顶部注释。

从上面那段注释中,我们可以得到以下这些信息:

  • HashMap实现了Map接口,key和value都可以添加任意元素,包括null
  • HashMap与HashTable大致相等,除了HashMap是没有加锁,线程不安全的,HashTable的key不允许存储null值;
  • HashMap不保证元素的有序性;
  • HashMap的默认装载因子是0.75,当已经有数据的槽位与总的槽位的比例超过装载因子时,会扩容并且再哈希
  • 迭代遍历消耗的时间与HashMap实例的实际容量和键值对成正比,因此,在迭代器遍历使用较多的情况下,不应该将初始容量设置的过大,或者负载因子设置的过小;
  • 迭代器采用快速失败机制;
  • 在存储大量数据时可以在初始化时指定好容量,减少扩容和在哈希带来的时间消耗。

 1.2 HashMap的类结构

Hashmap的类结构相比于实现了Collection接口的List集合类的结构图要简单的多,继承的是AbstractMap类。

2、HashMap源码解读

经过上面从整体分析过一波HashMap,已经对HashMap有了基本的认识。下面可以从源码开始入手,深入去分析HashMap。

HashMap源码的代码量还是比较大的,全部阅读完我觉得太耗时间了,但是只要掌握好平时常用的几个方法就可以适应日常的开发工作了。

2.1 HashMap的成员变量

/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

结合注解,可以知道HashMap中几个主要的成员变量的意义:

  • DEFAULT_INITIAL_CAPACITY:默认的初始容量是16,且是2的幂;
  • MAXIMUM_CAPACITY:HashMap的最大容量;
  • DELAULT_LOAD_FACTOR:如果初始化时使用了没有指定装载因子的构造器,装载因子就使用这个变量。参数值为0.75,即哈希表中3/4的位置已经被放置了元素时扩容;
  • TREEIFY_THRESHOLD:从链表转换为红黑树的阈值;
  • UNTREEIFY_THRESHOLD:从二叉树转换为链表的阈值;
  • MIN_TREEIFY_CAPACITY:HashMap的最小树形化阈值,当哈希表的容量大于这个值时才允许树形化,低于这个阈值直接扩容;
    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;
  • table:HashMap中用来存储数据的键值对数组,不在初始化时分配内存而是在第一次put键值对时初始化到对应的容量,并且总是以两倍扩容;
  • entrySet:保存缓存的entrySet();
  • size:HashMap中的槽位个数;
  • modCount:配合快速失败机制的计数器,快速失败机制:https://blog.csdn.net/weixin_39738307/article/details/106100118
  • threshold:决定是否扩容的阈值,也就是容量*装载因子;
  • loadFactor:装载因子。

2.2 hash方法

之所以单独给hash开一个标题,是因为hash方法是HashMap的核心方法之一,几乎所有的常用方法,例如get,set等都是围绕着hash方法展开的。

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

根据注释和代码,可以知道用Object类自带的hashCode方法得到哈希值,再对哈希值本身和哈希值逻辑右移16位后的数字做异或(Java中int变量是32位的,16刚好是32的一半),也就是说hashCode方法得到的非null的值的前16位与0做异或,后16位与前16位做异或组成一个新的hash值。目的是使哈希值的分散更加均匀(注意一下Java的移位>>和>>>还是有区别的,>>是算术右移,>>>是逻辑右移)。简单来说是通过高位和低位的异或操作,让高位的哈希值扩散影响到低位的哈希值,来让后面插入的时候对数组大小做与运算之后的结果能分配的更均匀从而减少哈希冲突。具体为什么这么做会让哈希的分布更加均匀,在下面的put方法分析中会提到。

HashMap允许key为null,当key为null的时候,hash值就为0。

hashCode方法具体运行原理:https://blog.csdn.net/weixin_39738307/article/details/106079696

2.3 HashMap的构造方法

HashMap的构造方法有4个:

重点说说第一个,其实这几个都差不多,第一个复杂点,第一个看弄明白了其他几个构造器也就都迎刃而解了。

指定了初始容量和装载因子的构造器:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

这里调用了tableSizeFor函数,看一下这个函数的实现:

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

结合代码和注解可以知道, 这个函数的功能是返回一个离输入参数最近并且不小于输入参数的2的整数次幂。这里用到的是位运算的算法,关于移位算法,可以参考:https://www.cnblogs.com/wang-meng/p/9b6c35c4b2ef7e5b398db9211733292d.html

这里是把一个离初始容量最近且不小于初始容量的2的整数次幂赋值给threshold变量,这里可以抛出一个问题:为什么要选择这样一个数赋值为threshold变量?

在上面的分析中,threshold是决定是否扩容并且再散列的阈值,也就是容量*装载因子;那么这么一来,就和上面的threshold变量的赋值操作矛盾了。其实这么做问题不大,因为这时候只是在初始化的时候赋值,还没有真正的分配内存给HashMap,并且在真正分配了内存之后会再一次赋新的值给这个变量。等到第一次put数据的时候,会触发扩容机制,这个时候不仅容量会发送变化,threshold变量也会被重新赋值,这时候赋的新值就是装载因子*容量。具体的过程可以参考下面resize()方法的解析。

综上,可以知道这个构造函数主要做了这么几件事情:

  • 判断传入的容量和装载因子是否合法;
  • 如果传入的容量超过了限定的最大值,就用最大值代替;
  • 初始化装载因子,将装载因子成员变量设置为传入的装载因子;
  • 将阈值设置为一个不小于初始容量且离初始容量最近的2的幂次方;

顺便说一下为什么HashMap的容量要设置成2的幂,这个在面试中也常常会问到,可以参考https://www.jianshu.com/p/7d59251d28f3

根据hashCode把要存储的键值对映射到哈希表上的一个位置,可能第一反应想到的是取余。但是HashMap的作者没有那么做,而是用了位运算。位运算的速度要比取余来得快,可能这也是使用位运算而舍弃取余的一个原因。但是为什么必须是把容量设置成2的幂次呢?

2的幂次写成二进制数的形式是100..(n个0),所以2的幂次-1就是011..(n个1)。因为是key的哈希值对散列表的长度做与运算,所以最后得到的数值一定是在0到哈希表的长度之间,与取余操作的效果相同,但是效率上有很大提升。

另外的,HashMap的哈希表的长度取二的幂次的另外一个重要原因,是保证哈希表长度-1的二进制数的数值最后一位一定要为1!如果为哈希表长度-1的二进制数的数值的最后一位为0的话,0与0或者1做与运算最后的结果都是0,那么key的哈希码&哈希表长度-1的结果的二进制数值的最后一位一定是0。那也就是说下标的二进制值的最后一位为1的位置将永远没有办法被映射到,也就是说这些位置是存不了值的。所以,如果最后得到的结果二进制数值的最后一位一定为0的话,那么,那将会造成空间浪费,并且增加哈希碰撞,拖慢查询效率,最严重的的话会导致无法扩容(因为那些无法存放键值对的位置的存在,将永远达不到扩容阈值)!!!

再看看剩下的几个构造函数。

指定了初始容量的构造器,装载因子为默认的0.75:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

HashMap的无参构造器,也是日常开发中最常用的一种构造器,初始容量默认为16,装载因子默认为0.75:

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

创建指定map的构造器:

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

2.4 put(K, V)方法

添加方法和查看方法是HashMap中最常用的两个方法,先来看一下HashMap是怎么添加一个元素的。

先看看put方法:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hash方法刚刚已经分析过了,现在再去看看putVal方法:

经过总结提炼可以得到以下的流程图,

  • HashMap不是在初始化时分配好内存,而是在第一次插入键值对时分配初始容量大小的内存!!!
  • 通过哈希算法映射到hash表中的一个位置,如果这个位置没有放过value的话,那就直接放进去了,但是如果放过value的话,也就是说发生哈希冲突了(按照数据结构课本上面的概念,这个时候一般可以采用两种方法:开放地址法和拉链法,这里是在桶的下面接上一个红黑树/链表,属于拉链法),先看看要存放的value的key以及key的hash值是否相等,如果相等说明要插入的和原来就有的是同一个映射,就直接把值给替换了。但是如果不是相等的,就要根据那个位置下面接的是链表/红黑树来做相应的处理了;
  • 链表:遍历链表找一找是不是有相同的映射,如果找到的话就把值给替换掉,没有的话就在尾部加个新的结点存放value,也就是尾插法(在JDK1.8是尾插法,在JDK1.7以及更早的版本是头插法)。如果插入了结点,在插入之后在检查一下桶下面接的结点数量是否超过树形化阈值了,也就是TREEIFY_THRESHOLD,如果超过了的话在执行树形化操作;
  • 红黑树:按照红黑树的方法插入新的结点;
  • 当然了,所有put操作最后都要检查一下是否达到了扩容阈值,达到了就扩容并且再哈希。

这里还要注意一个点,

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

 决定键值对存放的桶的位置,是对哈希表的长度进行与运算之后的值,也就是p = tab[i = (n - 1) & hash]。之前提到过的哈希操作会通过异或运算把高位的哈希值扩散影响到低位的哈希值,如果实例化对象时没有指定初始化容量的话,那么第一次插入之后容量就默认为16,由于是对哈希表的长度做与运算,以16为例子,这个时候做与运算仅仅是hash值的后4位有效。也就是说,当哈希表的容量不够大时,这个时候hash值的高位对于插入操作来说失去作用,如果碰到高位变化特别大而低位的变化特别小时,这个时候很容易发生哈希碰撞在位置上变得集中的情况。那么,JDK1.8中对哈希值在做一步处理,通过位运算把hash值的高位和低位都再次打乱,增加了随机性,就可以用较小的代价解决可能发生的哈希冲突集中出现的情况。

2.5 get(Object key)方法

看完了HashMap添加一个元素的方法,再来看一下HashMap是如何通过key来获取一个value的。

结合注解,可以知道通过调用HashMap的hash方法计算key的hash值,调用getNode方法获取对应的value。

再来看看getNode方法的实现,

可以简单的概括,查找的时候,如果哈希映射到的桶的位置上没有元素 ,就返回null;如果有存放元素并且桶上的第一个元素就符合,那么直接返回,否则,就按照链表/红黑树的方法查找下去~

2.6 remove方法

HashMap中的remove方法有两个,分别是remove(Object key)和remove(Object key, Object value)。先看一下最常用的remove(Object key)方法的实现:

结合注解可以知道remove方法会删除指定key的键值对,并且返回删除的value。remove方法的内容只有一行调用removeNode函数的代码,看一下removeNode函数的实现:

再来看一下另外一个remove(Object key, Object value)方法,在平时的开发中,用到的次数比较少。先看一下是怎么实现的:

同样是没有返回删除的value,用一个boolean类型的返回值代表删除成功或者失败。虽然没有注释,但是根据上面一个remove的分析可以看出,这里的remove方法是删除key与value都匹配的键值对。

2.7 resize方法

之前在put方法中有提到过扩容方法,这里做一个补充。先看注解给出的信息:

可以知道,扩容之后的容量为原来容量的2倍,以及扩容会导致再哈希。

再来看一下源码是如何实现的:

总结一下扩容的步骤:

3、HashMap和HashTable的区别

因为HashTable在现在的开发中基本不怎么使用,并且有更好的替代方案,所以就不再去分析HashTable的源码了。这里主要再讲一下HashMap和HashTable的区别。关于HashTable的源码分析可以参考:https://blog.csdn.net/panweiwei1994/article/details/77427010

总结一下HashMap和HashTable的区别(HashMap在JDK1.7和1.8的实现差别还是挺大的,这里的HashMap以1.8的为例):

比较点 HashMap HashTable
底层数据结构 数组+链表/红黑树 数组+链表
线程安全性 线程不安全 线程安全
扩容方式 扩容后为当前容量的2倍 扩容后为当前容量的2倍+1
性能
默认的初始容量 16

11

容量要求 必须是2的整数次幂 没有
继承的类 AbstractMap Dictionary
遍历方式 Iterator(迭代器) Iterator(迭代器)和Enumeration(枚举器)
Iterator索引遍历数组的顺序 索引从小到大 索引从大到小
根据key的hashCode确定到数组中的位置 位运算 位运算+取余

4、JDK1.7和JDK1.8中HashMap的不同

从JDK1.8对JDK1.7的HashMap做了改进,其中最重要的就是HashMap的底层结构由数组+链表->数组+链表/红黑树(前面分析过,当一个桶中的结点达到8个时,把链表转换为红黑树;如果结点减少到6个,红黑树转换为链表);

改动可以总结出以下这张表:

比较点 JDK1.8 JDK1.7
底层数据结构 数组+链表/红黑树 数组+链表
链表的插入方式 头插法 尾插法
扩容流程    
扩容后数据存储位置的计算方式    

补充1:为什么在JDK1.7的时候,是先扩容后面再插入,而到了JDK1.8的时候是先插入再扩容呢?

补充2:为什么JDK1.7的时候是用头插法,而到了JDK1.8就变成了尾插法

补充3:为什么在JDK1.8中要引入红黑树机制?

主要是考虑到同一个位置冲突太多,查找效率的问题,当链表过长时转换成红黑树可以把时间复杂度从O(n)降低到O(logn)。

总结

说了那么多,其实HashMap的重要知识点可以做出以下的要点总结

  • HashMap底层是数组+链表/红黑树(在JDK1.7及之前是数组+链表);
  • HashMap的元素排列是无序的(散列算法);
  • HashMap根据哈希算法,计算key的HashCode(具体可参考Object类的HashCode()方法),为了减少冲突概率,在与数组长度做与运算之前,将高16位与低16位做异或运算,高位与低位结合,增加随机性,减少碰撞的可能;
  • 当数组中同一个位置的冲突数量>=8,且散列表容量>=64时,链表转换为红黑树;当冲突数量从>=8减少至6以下时,红黑树转换为链表;
  • HashMap的默认装载因子是0.75,即当散列表中3/4的位置有元素时,HashMap会进行扩容;
  • HashMap当插入第一个元素时执行初始化操作;
  • HashMap默认初始容量为16(可以通过构造器自定义初始容量,但是一定是2的幂),而且每次扩容容量扩大两倍;
  • HashMap线程不安全,HashTable线程安全;现在HashTable因为效率等因素已经不常用,一般在并发环境下用JUC包下线程安全的容器ConcurrentHashMap来代替HashTable,或者加锁,也可以把HashMap包装成一个线程安全的容器;
  • HashMap初始容量的设置不能过大或者过小。过大会造成空间浪费,过小会导致频繁的扩容增加时间和空间的开销。

总的来说,HashMap还是比List集合类要难多了,也难怪面试官都喜欢在面试的时候把HashMap当做重要知识点来提问。

补充1:最后的最后需要注意的一点是,如果面试中HashMap回答的不错,面试官有可能会发出灵魂拷问:“为什么hashMap在冲突个数为8的时候链表转换为红黑树,而在冲突个数为6的时候在红黑树转换回链表?

关于这个,主要还是通过概率统计的方法的计算得出,涉及到数学问题,感兴趣的可以自行研究。

总的来说,根据概率统计中的泊松分布,得到6和8是最适合的两个点,同一个位置冲突数量大于8和remove操作之后冲突数量从大于8减少到小于6的情况都很少发生,是空间成本和时间成本的折中策略。至于为什么没用到7,是因为如果两个阈值只相差1,那么就会带来频繁的树形化和退树形化操作,反而会增加额外的时间成本。中间有个差值,可以防止频繁转换。

也可能会问到为什么装载因子默认的是0.75。其实道理是一样的,总结的来说,是减少空间成本(包括扩容带来的时间和空间的消耗)和提高查询效率的折中,主要是通过泊松分布计算出来的一个数值。

感兴趣的可以参考源码注释:

     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     *

补充2:HashMap中的key若为Object类型,需要实现哪些方法?

结论:如果HashMap中put的key为自定义的类,那么就必须要重写equals()方法和hashCode()方法。

  • equals():所有类都隐式地继承Object类,在Object类中equals()方法是用"=="来做判断,也就是比较两个对象引用指向的是不是同一块内存地址。如果没有重写过equals()方法,那么比较的是对象在内存中的地址,如果put了两个完全相同的独立的对象,按照正常的理解,两个对象内容完全相等就可以认定为两个对象相等,那么就会导致会导致HashMap中存在两个相同的key!
  • hashCode():道理同上,Object类的hashCode()方法的返回值是由对象的地址转换来的。那么会出现这么一种情况,对于两个内容完全相同的对象,由于是存放在两块不同的内存地址上,调用hashCode()方法返回了两个不同的哈希码。把这两个对象put到HashMap上,正常理解key相同,应该是映射到同一个位置上,但是由于hashCode不同,会导致HashMap中存在两个相同的key!其次,HashMap在put的时候比较key,会先判断哈希码是否相同之后再判断key是否相同,重写hashCode()方法也有助于提高效率。另外,恰当的实现hashCode()也能减少哈希碰撞~

补充3:为什么String,Integer这样的包装类适合做HashMap中的key?

  • 这些类被final修饰,保证了key的不可更改性,所以不会存在存取时哈希码不一致的情况;
  • 类的内部已经重写过equals()、hashCode()方法,道理同补充2。(更新:String内部会维护一个哈希码缓存,避免重复计算,Integer的hashCode()返回的是自身的int值,重复概率高,一般不推荐使用,一般HashMap的key推荐使用String)

参考资料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章