文章从JDK1.7和JDK1.8两个版本解析HashMap的实现原理及其中常见的面试题(两个版本HashMap最大的区别,1.7版HashMap=数组+链表,1.8版HashMap=数组+红黑树+链表)

一、先讲讲哈希表

又叫散列表,是为了加快查找数组元素的速度,将每个要存进数组的数值进行哈希计算,从而获得另外一个唯一对应的数,将该数作为目标数值存进数组的索引,以后每次查询该数,只要再进行一次哈希计算,可以找到对应的索引,取值。

所以在不考虑哈希冲突的情况下,哈希表的增删改查都为O(1)

解决哈希冲突的方法:1、开放地址法;2、再哈希法;3、公共溢出区;4、链地址法(HashMap所采用的)

开放地址法:发生冲突时,向后查找一个空位插入

再哈希法:采用另外一个散列函数

二、JDK1.7

1、原理

HashMap底层为数组,加链表用于解决哈希冲突,并且链表的插入用的是头插法

  • 后插入的值被查询的概率更高,效率更高
  • 头插法扩容时链表顺序倒置,可能导致链表成环问题
2、初始化
/**
* 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); // Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}

以上为HashMap的初始化,有几个参数需要注意

capacity,容量,即数组长度,默认为16,计算index时与15进行异或计算,刚好保留hashcode后四位

初始化时,Map的容量必须都为2^n(a power of 2),为的是使得哈希计算求index的值尽可能不同,减少哈希冲突,哈希更均匀

capacity <<= 1是位运算,即二进制数值左移一位,循环递增,确保初始化后的容量为2^n

loadFactor,装载因子(默认为0.75)

threhold,阈值(capacity * loadFactor) 存放数值(size)达到阈值时进行扩容

0.75确保了不会存的值太少,空间利用率低,存的值太多,效率低

table,数组的长度 new Entry[]为键值对

3、插入元素
    static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1); //异或 比 取模更快
}
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //key为空,存在数组的第一位
int hash = hash(key.hashCode()); // hashcode()后得到二进制数需要右移,保证高位参与运算,减少哈希计算的冲突
int i = indexFor(hash, table.length); //指定到数组对应的索引,采用异或运算,速度更快
for (Entry<K,V> e = table[i]; e != null; e = e.next) { //数组对应索引上不为空时,进行遍历
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //替换新的value,访问oldValue
}
} modCount++;
addEntry(hash, key, value, i); //索引为空
return null;
}
4、获取元素
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) //equals()方法放在最后可以提高效率
return e.value;
}
return null;

删除元素时,不能用for循环,要用迭代器进行元素删除

5、扩容
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length); //全部元素个数超过阈值时,而且数组对应索引上必须有值,数组才会扩容两倍
}

扩容时,原数组的全部数据,进行重新计算(数组长度改变)插入到新的数组(最后插入的元素会转化为最先插入的),比较消耗性能

向HashMap添加1000个元素是怎么扩容的?1000,初始化1024大小的数组,达到阈值1024*0.75=768时进行扩容

所以在新建HashMap时最好自定义初始化数组的长度,减少扩容消耗性能

三、JDK1.8

1、原理

JDK1.8后的HashMap底层采用数组+部分链表+部分红黑树的组合,并采用尾插法

链表遍历的时间复杂度为O(n),红黑树为O(log n),提升了效率

当一个索引上要存储的元素个数超过8个,并且数组的长度大于64时,链表就会树化成红黑树

2、插入元素
 /**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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) // TREEIFY_THRESHOLD=8
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;
}
3、扩容

JDK8,HashMap的扩容大小与之前的一致,主要区别在于扩容后红黑树如何插入到新的数组

(源码过于复杂就不放上来了)红黑树里的元素迁移,不需要像1.7那样进行重新计算

因为数组长度整加了一倍,可以直接分为两组数据,一组保留原位置,另一组分到新增加的数组

迁移后的红黑树,如果同一数组上的元素小于6个,就会自动去树化成链表

四、jdk7和8的区别

  • JDK8链表会变成红黑树,加快查询
  • 新节点插入的链表的方式不同,JDK7是头插法,JDK8是尾插法因为需要遍历链表变成红黑树
  • JDK8的hash算法进行了简化
  • resize的逻辑修改,JDK7可能会出现死锁
  • JDK7键值对Entry,在构造方法时创建;JDK8称为Node,在put第一个元素时创建

五、equals和hashcode

  • equals继承Object,对引用对象的比较是通过对象的内存地址。
  • hashcode用于存入数组索引的计算,get和put的时候都会调用equals进行判断
  • 所以想要保证,equals相等hashcode也相等,对象不同的时候hashcode也要不同

六、安全的HashMap

  • Collections.synchronizedMap

    • 通过构造器传入的mutex参数作为互斥锁,底层代码是通过synchronized同步代码块实现的
  • Hashtable
    • get put方法都会加synchronized锁
    • 初始容量为11,扩容为翻倍+1
    • 不能存放null值,因为没有使用集合类的fail-fast安全机制(modcount标志来判断遍历的过程中是否遭到了修改,所以很多的集合类都不支持多线程)
  • ConcurrentHashMap
    • JDK7

      • 由segment数组和HashEntry组成,hashEntry同样为数组+链表
      • 其中HashEntry使用了Volatile修饰数据
      • segment继承了reentrantLock,即每个线程访问一个segment,只锁定该segment,不会影响到其他,所以并发度高。理论上,并发度就是segment数组的容量。
      • put方法:找到对应的segment,尝试加锁,存在竞争就scanAndLockForPut()自旋获取锁,达到一定次的会改为互斥锁。
      • get方法:因为HashEntry的value是使用了volatile修饰的,保证了每次读到的值都是最新,所以不用加锁,而且效率高
    • JDK8
      • 同HashMap,也会有红黑树。Node键值对用volatile修饰,保证了可见性
      • 放弃了segment分段锁,采用CAS+synchronized实现并发
      • put方法:
        • 计算hashcode,判断是否为空,需要初始化
        • 定位是否为null,null使用CAS写入
        • 不null存在元素了,判断是否扩容,不扩容再使用synchronize关键字加锁写入
      • get方法,同JDK7
    • 为什么更改为CAS+synchronized
      • 使用reentrantLock需要继续AQS类,增加了内存开销,synchronized属于JDK级别,性能会随着升级
      • 扩容的时候并不会对segment数组进行扩容,扩容的是HashEntry数组,所以随着元素越来越多,锁的粒度是变大的

六、常见面试题

  • HashMap的底层数据结构

  • HashMap的存取原理

    • 继承了Map接口是以键值对的形式保存数据,保存数据时,是将key的hashcode进行哈希计算得到的数,作为放在数组上的索引,如果计算之后的索引相同,就在那个节点后面加一个链表或者红黑树;取数据的时候,就只要进行一次哈希计算就可以确定索引的位置,遍历该索引上的节点就可以找打。
  • 为啥会出现线程不安全

    • JDK7,扩容时会出现环形链表的情况,因为扩容转移时链表的顺序会调换
    • JDK8,多线程会出现数据覆盖的情况
  • 有什么线程安全的类替换

    • currentHashMap、hashTable因为性能低,只是简单地在方法上加synchronized锁
  • 默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

    • 16,为了让哈希计算结果的分布更均匀。求索引的时候是用hashcode和数组的长度-1进行异或运算,15的二进制刚好为1111,只要hashcode的分布是均匀的,异或运算之后的数值也是均匀的
  • HashMap的扩容方式?负载因子是多少?为什是这么多?

    • 1.7扩容需要存储的个数大于阈值且存放新的值时刚好发生了哈希冲突,这时才会触发扩容机制。扩容后长度为原来的两倍,遍历原来的entry数组,将节点重新hash后复制到新的数组
    • 0.75,确保存的值不会太少,空间利用率低;也不会太多,导致遍历效率低
  • hash的计算规则

    • 将hashcode右移16位和原值进行异或运算,保证高16位和低16位参与计算,使返回的值足够均匀,再和数组的长度-1进行异或计算,得到索引(异或计算:相同为1不同为0)
  • 为什么长度为2^n

    • 是为了让哈希计算后索引的分布更均匀,减少哈希冲突。我记得源码里面的索引的计算是key的hashcode和数组长度-1进行异或运算,如果长度是2^n-1,二进制就全是11111,比如16就是四个一、和hascode进行进行异或运算,只要hashcode是均匀的,计算出来的索引也会是均匀的
  • 为什么长度超过8就会自动转为红黑树

    • 是根据泊松分布,负载因子为0.75时,单个hash槽内出现8个元素的概率已经很小了,就可以减少链表转换为红黑树这种比较耗时的操作。

总结

  • HashMap是一种利用key的hashcode来进行存储的复杂数据结构

原理一、Java中的HashMap的实现的更多相关文章

  1. Java中的HashMap的工作原理是什么?

    问答题23 /120 Java中的HashMap的工作原理是什么? 参考答案 Java中的HashMap是以键值对(key-value)的形式存储元素的.HashMap需要一个hash函数,它使用ha ...

  2. 沉淀再出发:java中的HashMap、ConcurrentHashMap和Hashtable的认识

    沉淀再出发:java中的HashMap.ConcurrentHashMap和Hashtable的认识 一.前言 很多知识在学习或者使用了之后总是会忘记的,但是如果把这些只是背后的原理理解了,并且记忆下 ...

  3. java 中遍历hashmap 和hashset 的方法

    一.java中遍历hashmap:    for (Map.Entry<String, Integer> entry : tempMap.entrySet()) {     String ...

  4. Java中关于HashMap的元素遍历的顺序问题

    Java中关于HashMap的元素遍历的顺序问题 今天在使用如下的方式遍历HashMap里面的元素时 1 for (Entry<String, String> entry : hashMa ...

  5. Java中关于HashMap的使用和遍历(转)

    Java中关于HashMap的使用和遍历 分类: 算法与数据结构2011-10-19 10:53 5345人阅读 评论(0) 收藏 举报 hashmapjavastringobjectiterator ...

  6. [转]为什么Java中的HashMap默认加载因子是0.75

    前几天在一个群里看到有人讨论hashmap中的加载因子为什么是默认0.75. HashMap源码中的加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75 ...

  7. 关于Java中的HashMap的深浅拷贝的测试与几点思考

    0.前言 工作忙起来后,许久不看算法,竟然DFA敏感词算法都要看好一阵才能理解...真是和三阶魔方还原手法一样,田园将芜,非常可惜啊. 在DFA算法中,第一步是需要理解它的数据结构,在此基础上,涉及到 ...

  8. 深入理解Java中的HashMap的实现原理

    HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口.关系图例如以下所看到的: Java中的Map<key, value>接口同意我们将一个对象作 ...

  9. Java中的HashMap

    今天到中关村软件园面试被问到Java中HashMap的存值原理,瞬间无言已对,回答用了一个数组,然后沉默,面试官说,一次的面试失败不算什么,之后...... 1.关于hashCode hashCode ...

  10. JDK学习---深入理解java中的HashMap、HashSet底层实现

    本文参考资料: 1.<大话数据结构> 2.http://www.cnblogs.com/dassmeta/p/5338955.html 3.http://www.cnblogs.com/d ...

随机推荐

  1. ESP32

    Main menu   Search Create account Log in Personal tools     Apply for a scholarship & submit you ...

  2. visio 2010 kit tools

    Getting Office License Configuration Information.---------------------------------------Backing Up L ...

  3. 14. 从零开始编写一个wmproxy(代理,内网穿透等), HTTP文件服务器的实现过程及参数

    用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP中的压缩gzip,deflate,brotli算法 项目 ++wmproxy++ gite: https://gitee.com/ ...

  4. 使用Spring Integration接收TCP与UDP请求

    1. 简介 Spring Integration 是一个开源的项目,它是 Spring 生态系统的一部分,旨在简化企业集成(Enterprise Integration)的开发.它提供了一种构建消息驱 ...

  5. svn的常规使用

    svn的常规使用 svn的常规使用 1 客户端 2 svn server 3 qt使用svn 4 svn项目迁移 Ubuntu上使用svn 1 安装 2 使用 svn的常规使用 1 客户端 下载地址: ...

  6. 持续集成(CI)、自动化构建和自动化测试--初探

    转自:http://blog.csdn.net/adparking/article/details/5796532 此文章是为了总结前一段时间由于Maven2的学习而引起的一个持续集成的学习. 一.什 ...

  7. 研读Java代码必须掌握的Eclipse快捷键

    1. Ctrl+左键 和F3 这个是大多数人经常用到的,用来查看变量.方法.类的定义 跳到光标所在标识符的定义代码.当按执行流程阅读时,F3实现了大部分导航动作. 2 Ctrl+Shift+G 在工作 ...

  8. Azure Data Factory(十)Data Flow 组件详解

    一,引言 随着大数据技术的不断发展,数据处理和分析变得越来越重要.为了满足企业对数据处理的需求,微软推出了 Azure Data Factory (ADF),它是一个云端的数据集成服务,用于创建.安排 ...

  9. Linux 本地AMH 服务器管理面板实现远程访问方法

    AMH 是一款基于 Linux 系统的服务器管理面板,它提供了一系列的功能,包括网站管理.FTP 管理.数据库管理.DNS 管理.SSL 证书管理等.使用 AMH 云主机面板可以方便地管理服务器,提高 ...

  10. string函数部分解释

    ```c1. 运算符重载+.+= 连接字符串= 字符串赋值>.>=.<.<= 字符串比较(例如a < b, aa < ab)==.!= 比较字符串<<. ...