HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap

声明:这是我参考多篇文章,并加入思考整理而成的文章,大都是借鉴大佬的文章,文中的图也都不是我画的,但由于参考的文章较多,整理也比较辛苦,故标为原创,文末已给出参考文章

HashMap的底层实现原理

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第1张图片

HashTable用绿色表示是因为现在不常用了,但面试也可能会考

TreeMap是基于树的实现

HashMap,HashTable,ConcurrentHashMap是基于hash表的实现

HashTable和HashMap在代码实现上,基本上是一样的,和Vector与Arraylist的区别大体上差不多,一个是线程安全的,一个非线程安全(HashMap线程安全)

ConcurrentHashMap也是线程安全的,但性能比HashTable好很多,HashTable是锁整个Map对象,而ConcurrentHashMap是锁Map的部分结构

Map其实很简单,就是一个key,对应一个value

HashMap的put方法的执行过程

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

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第2张图片
一些属性,一个key,一个value,用来保存我们往Map里放入的数据,next用来标记Node节点的下一个元素。

 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;
        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))))
                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;
    }

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第3张图片
看到了熟悉的hashCode,重写equals方法的时候,一定要重写hashCode方法,因为key是基于hashCode来处理的

resize方法比较复杂,这儿就不完全贴出来了,当放入第一个元素时(此时成员变量table为空),会触发resize方法的以下关键代码
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第4张图片

最后返回new出来的数组

然后我们看putVal的接下来的代码

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

这段代码初学者可能看起来比较费劲(没错,我就是这样),我们重写一下以便初学者能更好的理解,这两段代码等同,下面是重写后的代码,清晰了很多

i = (n - 1) & hash;//hash是传过来的,其中n是底层数组的长度,用&运算符计算出i的值 
p = tab[i];//用计算出来的i的值作为下标从数组中元素
if(p == null){
     //如果这个元素为null,用key,value构造一个Node对象放入数组下标为i的位置
     tab[i] = newNode(hash, key, value, null);
}

这个hash值是字符串“张三”这个对象的hashCode方法与hashMap提供hash()方法共同计算出来的结果,其中n是数组的长度,目前数组长度为16,不管这个hash的值是多少,经过(n - 1) & hash计算出来的i 的值一定在n-1之间。刚好是底层数组的合法下标,用i这个下标值去底层数组里去取值,如果为null,创建一个Node放到数组下标为i的位置。

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第5张图片
上图是简化后的堆内存图。继续往里添加“孙七”,通过(n - 1) & hash计算“孙七”这个key时计算出来的下标值是1,而数组下标1这个位置目前已经被“李四”给占了,产生了冲突。相信大家在看本文的过程中也有这样的疑惑,万一计算出来的下标值i重了怎么办?我们来看一看HashMap是怎么解决冲突的。

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第6张图片
上图中红框里就是冲突的处理,这一句是关键

p.next = newNode(hash, key, value, null);

也就是说new一个新的Node对象并把当前Node的next引用指向该对象,也就是说原来该位置上只有一个元素对象,现在转成了单向链表,继续画图
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第7张图片

继续添加其它元素,添加完成后如下
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第8张图片

红框中还有两行比较重要的代码

if (binCount >= TREEIFY_THRESHOLD - 1) //当binCount>=TREEIFY_THRESHOLD-1
      treeifyBin(tab, hash);//把链表转化为红黑树

当链表长度到8时,将链表转化为红黑树来处理,由于树相关的内容本专栏还未讲解,红黑树的内容这里就不深入了。树在内存中的样子我们还是画个图简单的了解一下
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第9张图片

在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率。在面试过程中,能说出这一点,面试官会对你加分不少

hash方法的实现

 public V put(K key, V value) {
     
        return putVal(hash(key), key, value, false, true);
    }
 static final int hash(Object key) {
     
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在put放入元素时,HashMap又自己写了一个hash方法来计算hash值,大家想想看,为什么不用key本身的hashCode方法,而是又处理了一下?

HashMap的最底层是数组来实现的,数组里的元素可能为null,也有可能是单个对象,还有可能是单向链表或是红黑树。

文中的resize在底层数组为null的时候会初始化一个数组,不为null的情况下会去扩容底层数组,并会重排底层数组里的元素。

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第10张图片

执行完红框里的代码,personMap里放入了8个元素,放置完成后在堆内存表现如下图

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第11张图片
如果忽略底层实现细节,是这样的
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第12张图片
在Map中,一个key,对应了一个value,如果key的值已经存在,Map会直接替换value的内容,
HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第13张图片

总结,在hashMap中放入(put)元素,有以下重要步骤

1、计算key的hash值,算出元素在底层数组中的下标位置。
2、通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。
3、取到元素,判断放入元素的key是否等于等于或equals当前位置的key,成立则替换value值,返回旧值。
4、如果是树,循环树中的节点,判断放入元素的key是否等于等于或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。
5、否则就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。

精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key满足以下两个条件即可替换:
1、hash值相等。

2、==或equals的结果为true。
由于hash算法依赖于对象本身的hashCode方法,所以对于HashMap里的元素来说,hashCode方法与equals方法非常的重要

MAP

map是JAVA里面的一个接口,常见的实现类有HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。

HashMap底层是数组+链表/红黑树
LinkedHashMap底层是数组+链表+双向链表
TreeMap底层是红黑树
ConcurrentHashMap底层是数组+链表/红黑树
HashMap默认大小是16,负载因子是0.75,HashMap的大小只能是2的次幂 HashMap:为什么容量总是为2的次幂
2的次幂一种解释是2的幂都是11111结尾的,所以hash碰撞(分布不均匀,计算出来存在同一个位置,减慢了查询的效率,造成空间的浪费)几率小。
假设你传一个10进去,实际上最终HashMap的大小是16,你传一个7进去,HashMap最终的大小是8,具体的实现在tableSizeFor可以看到。

HashMap为什么容量总为2的次幂

结论:①因为2的幂都是11111结尾的,所以碰撞几率小。
②HashMap通过位运算代替取模,更加高效地算出元素所在的位置,当HashMap的大小为2的次幂时,才能合理利用位运算代替取模

首先,hashMap中的tableSizeFor 方法做了处理,能保证n永远都是2次幂。

其次,既然是通过hash的方式,那么不可避免的会出现hash冲突的场景。
hash冲突就是指 2个key 通过hash算法得出的哈希值是相等的。
hash冲突是不可避免的,所以如何尽量避免hash冲突,或者在hash冲突时如何高效定位到数据的真实存储位置就是HashMap中最核心的部分。

首先要提的一点是 HashMap 中 capacity 可以在构造函数中指定,如果不指定默认是2 的 (n = 4) 次方,即16。

HashMap中的hash也做了比较特别的处理,(h = key.hashCode()) ^ (h >>> 16)。

先获得key的hashCode的值 h,然后 h 和 h右移16位 做异或运算。
实质上是把一个数的低16位与他的高16位做异或运算,因为在 (n - 1) & hash 的计算中,hash变量只有末x位会参与到运算。使高16位也参与到hash的运算能减少冲突。

p = tab[i = (n - 1) & hash])

这样能保证 索引值 肯定在 capacity 中,不会超出数组长度
(n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,通过 (n - 1) & hash 可决定桶的索引

因为 n 永远是2的次幂,所以 n-1 通过 二进制表示,永远都是尾端以连续1的形式表示(00001111,00000011)
当(n - 1) 和 hash 做与运算时,会保留hash中 后 x 位的 1,
例如 00001111 & 10000011 = 00000011

这样做有3个好处

&运算速度快,至少比%取模运算块
能保证 索引值 肯定在 capacity 中,不会超出数组长度
(n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n

为什么要通过 (n - 1) & hash 决定桶的索引呢?
(1)key具体应该在哪个桶中,肯定要和key挂钩的,HashMap顾名思义就是通过hash算法高效的把存储的数据查询出来,所以HashMap的所有get 和 set 的操作都和hash相关。
(2)既然是通过hash的方式,那么不可避免的会出现hash冲突的场景。hash冲突就是指 2个key 通过hash算法得出的哈希值是相等的。hash冲突是不可避免的,所以如何尽量避免hash冲突,或者在hash冲突时如何高效定位到数据的真实存储位置就是HashMap中最核心的部分。
(3)首先要提的一点是 HashMap 中 capacity 可以在构造函数中指定,如果不指定默认是2 的 (n = 4) 次方,即16。

(4)HashMap中的hash也做了比较特别的处理,(h = key.hashCode()) ^ (h >>> 16)。
先获得key的hashCode的值 h,然后 h 和 h右移16位 做异或运算。
实质上是把一个数的低16位与他的高16位做异或运算,因为在前面 (n - 1) & hash 的计算中,hash变量只有末x位会参与到运算。使高16位也参与到hash的运算能减少冲突

例如1000000的二进制是 00000000 00001111 01000010 01000000
右移16位: 00000000 00000000 00000000 00001111
异或 00000000 00001111 01000010 01001111

HashMap的负载因子

还有就是扩容这个操作肯定是耗时的,那能不能把负载因子调高一点,比如我要调至为1,那我的HashMap就等到16个元素的时候才扩容呢。

当然是可以的,但是不推荐。负载因子调高了,这意味着哈希冲突的概率会增高,哈希冲突概率增高,同样会耗时(因为查找的速度变慢了)

通俗来讲,当负载因子为1.0时,意味着只有当hashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是这就会导致大量的hash冲突,使得查询效率变低。

当负载因子为0.5或者更低的时候,hash冲突降低,查询效率提高,但是由于负载因子太低,导致原来只需要1M的空间存储信息,现在用了2M的空间。最终结果就是空间利用率太低。

负载因子是0.75的时候,这是时间和空间的权衡,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度也比较低,提升了空间效率。

HashMap在put元素时,怎么计算hash值

实现就在hash方法上,可以发现的是,它是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。这样做的好处可以增加了随机性,减少了碰撞冲突的可能性

HashMap在get方法执行过程

在get的时候,还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突
假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。

什么情况下转化为红黑树

当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树大小为6时,会退化为链表。
这里转红黑树退化为链表的操作主要出于查询和插入时对性能的考量。链表查询时间复杂度O(N),插入时间复杂度O(1),红黑树查询和插入时间复杂度O(logN)

ConcurrentHashMap如何实现线程安全

从JDK1.2起,就有了HashMap,HashMap不是线程安全的,因此多线程操作时需要格外小心。
在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从此Map也有安全的了。

ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。

从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中:

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第14张图片HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第15张图片
ConcurrentHashMap的工作机制
不能全锁(HashTable)又不能不锁(HashMap),所以就搞个部分锁,只锁部分,用到哪部分就锁哪部分。一个大仓库,里面有若干个隔间,每个隔间都有锁,同时只允许一个人进隔间存取东西。但是,在存取东西之前,需要有一个全局索引,告诉你要操作的资源在哪个隔间里,然后当你看到隔间空闲时,就可以进去存取,如果隔间正在占用,那你就得等着

ConcurrentHashMap vs Hashtable vs Synchronized Map

引入ConcurrentHashMap是为了在同步集合HashTable之间有更好的选择,HashTable与HashMap、ConcurrentHashMap主要的区别在于HashMap不是同步的、线程不安全的和不适合应用于多线程并发环境下,而ConcurrentHashMap是线程安全的集合容器,特别是在多线程和并发环境中,通常作为Map的主要实现。除了线程安全外,他们之间还有一些细微的不同

Hashtable是jdk1的一个遗弃的类,它把所有方法都加上synchronized关键字来实现线程安全。所有的方法都同步这样造成多个线程访问效率特别低。Synchronized Map与HashTable差别不大,也是在并发中作类似的操作,两者的唯一区别就是Synchronized Map没被遗弃,它可以通过使用Collections.synchronizedMap()来包装Map作为同步容器使用。

另一方面,ConcurrentHashMap的设计有点特别,表现在多个线程操作上。它不用做外的同步的情况下默认同时允许16个线程读和写这个Map容器。因为其内部的实现剥夺了锁,使它有很好的扩展性。不像HashTable和Synchronized Map,ConcurrentHashMap不需要锁整个Map,相反它划分了多个段(segments),要操作哪一段才上锁那段数据。

Hashtable和SynchronizedMap使用synchronized来保证线程安全,使用当前对象作为锁,同一时刻只能有一个线程操作,其他线程会被阻塞,所以竞争越激烈效率越低。ConcurrentHashMap无论是读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响对其它段的访问。 ConcurrentHashMap只能保证单个方法是同步的,不能保证先读后写的原子性

HashMap与ConcurrentHashMap的区别

ConcurrentHashMap是线程安全的和在并发环境下不需要加额外的同步。虽然它不像Hashtable那样需要同样的同步等级(全表锁),但也有很多实际的用途。

以使用Collections.synchronizedMap(HashMap)来包装HashMap作为同步容器,这时它的作用几乎与Hashtable一样,当每次对Map做修改操作的时候都会锁住这个Map对象,而ConcurrentHashMap会基于并发的等级来划分整个Map来达到线程安全,它只会锁操作的那一段数据而不是整个Map都上锁

ConcurrentHashMap有很好的扩展性,在多线程环境下性能方面比做了同步的HashMap要好,但是在单线程环境下,HashMap会比ConcurrentHashMap好一点。

总结一下以上两者的区别,它们在线程安全、扩展性、同步之间的区别。如果是用于缓存的话,ConcurrentHashMap是一个更好的选择,在Java应用中会经常用到。ConcurrentHashMap在读操作线程数多于写操作线程数的情况下更胜一筹。

LinkedHashMap

LinkedHashMap底层结构是数组+链表+双向链表,实际上它继承了HashMap,在HashMap的基础上维护了一个双向链表
有了这个双向链表,我们的插入可以是有序的,这里的有序不是指大小有序,而是插入有序
LinkedHashMap在遍历的时候实际用的是双向链表来遍历的,所以LinkedHashMap的大小不会影响到遍历的性能

TreeMap

TreeMap底层是红黑树,TreeMap的key不能为null(如果为null,那还怎么排序呢), TreeMap有序是通过Comparator来进行比较的,如果comparator为null,那么就使用自然顺序

线程安全的Map

HashMap不是线程安全的,在多线程环境下,HashMap有可能会有数据丢失和获取不了最新数据的问题,比如说:线程A put进去了,线程B get不出来。

我们想要线程安全,一般使用ConcurrentHashMap
ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。
线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。
当然了,也可以使用Collections来包装出一个线程安全的Map。
但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize),所以我们一般有线程安全问题考量的,都使用ConcurrentHashMap

ConcurrentHashMap的底层数据结构是数组+链表/红黑树,它能支持高并发的访问和更新,是线程安全的。
ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步,在get的时候没有加锁,Node都用了volatile给修饰
在扩容时,会给每个线程分配对应的区间,并且为了防止putVal导致数据不一致,会给线程的所负责的区间加锁

JDK 7的HashMap在扩容时是头插法,在JDK8就变成了尾插法,在JDK7的HashMap还没有引入红黑树。ConcurrentHashMap在JDK7还是使用分段锁的方式来实现,而JDK 8就又不一样了。

HashMap与HashTable的不同

hashmap简介

HashMap底层原理、hashMap与hashTable的区别、ConcurrentHashMap_第16张图片

图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中

HashMap是在JDK1.2中引入的Map的实现类。
1.HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阈值)时,同样会自动增长
2. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。

3.HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

4.HashMap存数据的过程是:

HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

5.HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中

HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机制。HashMap内部有:

变量size,它记录HashMap的底层数组中已用槽的数量;

变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)

变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75

HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能

HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。

下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。

hashTable简介

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。

Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

hasghMap与hashTable的区别

HashMap HashTable
jdk引入 1.2 1.0
父类 AbstractMap Dictionary(已被废弃的类)
实现接口 Map Cloneable Serializable Map Cloneable Serializable
线程安全
contains方法
是否允许null值
计算hash值方式 重载hashCode方法 直接使用hashCode方法
初始容量 16 11
扩容方式 2倍 2倍+1
解决hash冲突方式 链表+红黑树 链表

HashMap是没有contains方法的,而包括containsValue和containsKey方法;hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。

Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;HashTable键值对都不能为空,否则包空指针异常。HashMap在put的时候会调用hash()方法来计算key的hashcode值,可以从hash算法中看出当key==null时返回的值为0。因此key为null时,hash算法返回值为0,不会调用key的hashcode方法。当Hashtable存入的value为null时,抛出NullPointerException异常。如果value不为null,而key为空,在执行到int hash = key.hashCode()时同样会抛出NullPointerException异常
为什么HashMap的key允许空值,而Hashtable却不允许
HashMap是之后的版本引进的类,它的接口Map表达的意义更为广泛,也许HashMap的设计者认为null作为key和value是有实际意义的,所以才允许为null.

当然实际项目中,真的是有value为null的情况的。key为null的情况比较少见,但不代表没有。HashMap允许null为key和value应当是类的设计者思考让这个类更有用的设计吧。

计算hash值方式不同
为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。

①:HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值的方法:

static final int hash(Object key) {
     
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

注意这里计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。
②:Hashtable通过计算key的hashCode()**来得到hash值就为最终hash值。

它们计算索引位置方法不同:
HashMap在求hash值对应的位置索引时,index = (n - 1) & hash。将哈希表的大小固定为了2的幂,因为是取模得到索引值,故这样取模时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashTable在求hash值位置索引时计算index的方法:

int index = (hash & 0x7FFFFFFF) % tab.length;

&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号位改变,而后面的位都不变。

Java8,HashMap中,当出现冲突时可以:

1.如果冲突数量小于8,则是以链表方式解决冲突。
2.而当冲突大于等于8时,就会将冲突的Entry转换为红黑树进行存储。
3.而又当数量小于6时,则又转化为链表存储。

而在HashTable中, 都是以链表方式存储。

在hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
故解决方法就是使用 使用ConcurrentHashMap。
这里要说一下 就是HashMap的迭代器(Iterator)是fail-fast迭代器,故当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException异常,而Hashtable的enumerator迭代器不是fail-fast的。但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

线程安全性不同,HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎都是被synchronized关键字修饰的。但是,当我们需要HashMap是线程安全的时,怎么办呢?我们可以通过==Collections.synchronizedMap(hashMap)==来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定

参考文献及推荐阅读:

HashMap底层实现原理(上)
HashMap底层实现原理(下)

这几道Java集合框架面试题在面试中几乎必问

HashMap底层实现原理及面试问题

小公司面试官问我Map,我硬杠了他
HashMap的负载因子初始值为什么是0.75?

HashMap和Hashtable的区别

HashMap与ConcurrentHashMap的区别

HashMap、ConcurrentHashMap、HashTable的区别

推荐一篇文章:HashMap详细解释+全站最硬核手撕源码分析
有关 HashMap 面试会问的一切

高端的面试从来不会在HashMap的红黑树上纠缠太多

并发下的 HashMap 为什么会引起死循环???
阿里面试官没想到一个HashMap,我能跟他扯半小时
我把ConcurrentHashMap & HashTable的知识点都整理了一下
厉害了!把 HashMap 剖析的只剩渣了!同内容文章:深入剖析HashMap

你可能感兴趣的:(Java)