近两年出场率最高的 Java 面试知识点之一
作死:平时用用,没看的那么深
简答:这三者都是 Map 的实现,HashMap 是基于哈希表的 Map 接口的实现, Hashtable 同样是基于哈希表,但是 Hashtable 是线程安全的,ConcurrentHashMap 是从1.5 版本出现的,线程安全的可以替代 Hashtable 的 Map 实现。
详答:(以上重复的不再多说) HashMap 采用哈希表来管理元素,在调用 put 方法向 HashMap 中写入值的时候,先计算 key 的哈希值来进行快速定位,然后写入值或者替换值。
当然,也会有多个不同元素计算出同一个哈希值,这就是我们说的哈希碰撞,此时再配合 equals 方法,共同判断这个 Key 是否真正的存在,然后再进行下面的操作。
关于键值存储:HashMap 允许存在 null 的键和值,但是键值只能有一个为 null,Hashtable 不允许有 null 的键值存在。
Hashtable 的默认初始化大小是 11 ,影响因子同样是 0.75,扩容采用的是在影响因子的约束下 的 rehash() 方法 newCapacity = (oldCapacity << 1) + 1
public Hashtable() {
this(11, 0.75f);
}
Hashtable 可以算是1.4 遗留类,现在还有很多冗余和可优化项。而且单线程下也要加锁,所以现在的代码中,多线程通常采用 ConcurrentHashMap 来实现。
ConcurrentHashMap 见问题4~
作死:有区别么,不是一样的么?
简答:是底层数据结构的实现发生了变化
详述:在 1.7 版本中,HashMap 底层采用了 Entry 数组+链表的数据结构,通过对 Key 的 Hashcode 进行取模来决定 key 存放的位置。相同 Hashcode (或者说取模后) 的 key 值会存储到一个 Entry 中,他们会形成一个链表 Node<>,一旦链表过长,查找复杂度可能会达到O(n)。
在 1.8 版本之后,HashMap 底层采用数组 + 链表/红黑树的数据结构,在同 Hashcode 的 key 的数量小于等于8个的时候,还是采用链表的结构,如果大于8个,那么就采用红黑树的数据结构。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
因为红黑树的性能特点,在大量 hashcode 值相同的时候,查找某个特定元素,也只是需要O(log n) 的开销.
但是想要使用好1.8版本的 HashMap,请正确的实现 Compare 接口,如果实现的不好或者没实现,效率可能还要低于 1.7 版本。
作死: 没太研究过,可能是翻倍吧
简答: 初始化容量16,超过的话是原来的二倍
详细:默认情况下,HashMap 初始化的容量大小为 16,同时还有约束 HashMap 什么时候扩容的影响因子为 0.75。
// 默认初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默认影响因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
影响因子的存在,是为了维持 HashMap 中的空间开销与时间开销。
当 HashMap 的容量超过 16*0.75=12 的时候,HashMap 调用resize() 中 newCap = oldCap << 1 << 1 的方法开始第一次扩容。
如果说一个HashMap 中有12个键值对,那么它的大小为16,如果是13的时候,那么他它大小即为32。
我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
HashMap 中用到了大量的位运算,这是我们在设计自己的工具类中可以借鉴的。
在这,我从美团点评技术团队博客盗了个图,非常有益于我们理解 HashMap 的 put 和 扩容机制,感谢美团点评技术团队(无利益,侵删)~
对于ConcurrentHashMap,同样的,从 1.7 到 1.8 版本,Java开发者对 ConcurrentHashMap 也做了修改。
在 1.7 版本中,大小默认为16,ConcurrentHashMap 由一个 Segment 数组和多个 HashEntry 组成。使用 Segment 是锁分离技术的实现,将一个大的 table 转换为多个小的 table 来进行加锁,每个 Segment 元素存储的是一个Entry数组+链表,与 HashMap 结构相同。
到了 1.8 版本,初始化大小为 16 ,0.75。ConcurrentHashMap 抛弃了 Segment 数组,采用 Node数组 + 链表 + 红黑树 的数据结构实现,当链表长度大于8的时候会构建为红黑树。并发控制使用Synchronized和CAS来操作(整个看起来就像是优化过且线程安全的HashMap)。
ConcurrentHashMap 的 put 操作是并发进行的,所以可能会发生 put 与扩容操作同时进行的情况,那么先扩容,再 put 。只有发生冲突的时候,才采用乐观锁的方式进行并发处理。
关于 ConcurrentHashMap 涉及到并发和乐观锁的扩容,嗯….给你个链接
http://www.importnew.com/22007.html
总的来说,就是 put 和 扩容 的同时,要考虑到锁的问题,不考虑锁的情况下,大体上和 HashMap 是相似的。