jdk1.7/jdk1.8 HashMap源码解读
HashMap实现原理
-
基于hashing原理,通过
put(key,value)
和get(key)
方法存储和获取对象 -
当我们存储对象的时候,把键值对传给
put(key,value)
方法时,它调用键对象key的hashCode()方法来计算hashCode,然后找到bucket位置,来存储值对象value。 -
当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的equals()方法找到正确的键值对key-value,然后返回值对象value。
-
HashMap里有hash碰撞(冲突),HashMap使用链表来解决碰撞问题,当发生了碰撞,对象将会存储在链表的下一个节点中。
HashMap的每个链表中存储的是key-value对象,当两个不同的键对象key的hashCode相同时,那就发生了碰撞(冲突),那它会存储在同一个bucket的位置,bucket的位置存不下这两个点,那么就通过链表来存储。(在jdk8里,链表长度大于8的话就会变成红黑树)
取数据可通过键对象key的equals()方法用来找到正确的键值对key-value。
HashMap底层数据结构
- JDK8以前的HashMap的底层数据结构是: 数组+链表,它之所以有相当快的查询速度,主要是因为它是通过Key计算hashCode来决定一维数组中存储的位置,而增删速度靠的是链表保证。
简单来说,HashMap由数组+链表组成的,Entry数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
-
JDK8中HashMap的底层数据结构:用**数组+链表+红黑树的结构来优化,链表长度大于8同时满足HashMap中元素个数大于64则 变红黑树**,长度小于6变回链表
什么是Hash表?
- 散列表(Hash Table,也叫哈希表)通过**关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度,这个映射函数叫做散列函数**,存放记录的数组叫做散列表。
- hash表里可以存储元素的位置叫做bucket(桶)。
什么是哈希冲突,如何解决?
- 不同的key值产生了相同的Hash地址,
H(key1) == H(key2)
- 解决方案:
- 开放地址法:不断地探测序列,查找一个空的单元进行插入。有线性探测、再平方、伪随机
- 链地址法:对于相同的hashCode值,使用链表进行连接。使用数组来存储每一个链表。HashMap中使用的方案。
- 公共溢出区法:建立一个特殊的存储空间,专门存储这些冲突的数据,适用数据冲突比较少的情况。
- 再散列法:准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数 以此类推。
HashMap源码读过吗?
核心成员变量有哪些知道吗?
JDK7中HashMap源码之核心变量
Entry[] table
这个Entry类型的数组存储了HashMap的真正数据。- size大小 代表HashMap内存储了多少个键值对。
- capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
- threshold阈值和loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap扩容会再次计算每个元素的哈希位置。
- entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
JDK8中HashMap源码之核心变量
Node[] table
这个Node类型的数组存储了HashMap的真正数据static class Node<K,V> implements Map.Entry<K,V>
。- size大小 代表HashMap内存储了多少个键值对。
- capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
- threshold阈值和loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap会扩容,哈希计算比之前有改进。
- entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
- TREEIFY_THRESHOLD 树化阈值 转换成红黑树的临界值,默认8
- UNTREEIFY_THRESHOLD 反树化阈值 红黑树转换成链表的临界值,默认6
- MIN_TREEIF_CAPACITY 链表被树化的最小数组容量 最小树形化阈值 默认64
为什么到8转为红黑树 到6转为链表?
为什么链表长度为8转为红黑树?
- TreeNode(红黑树中)占用空间是普通Node(链表中)的两倍,为了时间和空间的权衡。
- 节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006
为什么链表长度为6,红黑树就转为链表呢?
- 若是7,则当极端情况下(频繁插入和删除的都是同一个哈希桶)对一个链表长度为8的哈希桶进行频繁的删除和插入,同样也会导致频繁的树化<=>非树化
插入和获取数据的过程清楚吗?
-
JDK1.7中HashMap插入的过程:— put操作
public V put(K key, V value) { // 步骤1 if (table == EMPTY_TABLE) { //判断如果表空进行初始化 inflateTable(threshold); } // 步骤2 if (key == null) //判断是否键为空 return putForNullKey(value); //遍历table[0]是否存在 e.key == null // 步骤3 int hash = hash(key); //计算hash值 int i = indexFor(hash, table.length); //计算bucket的位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //遍历链表 Object k; // hash相同 equals比较是真 则替换 返回旧值 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //添加数据 这里包含hash冲突的处理。 return null; }
-
jdk7 中的hash函数处理
final int hash(Object k) { /** 获取哈希干扰因子,该因子会跟根据HashMap的容量进行变更 变更情况根据上一步的“final boolean initHashSeedAsNeeded(int capacity) ”方法动态变更 **/ int h = hashSeed; //如果为干扰因子不为0,且传入的key类型为String,则使用特定的算法(sun.misc.Hashing.stringHash32((String) k))对该key进行hash计算。并返回 if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } //如果哈希干扰因子为0 或者 k的类型不为String则使用异或操作变更key的hashcode h ^= k.hashCode(); //为了减少Hash冲突出现次数进行必要的位干扰,默认负载因子是8. // 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); }
扰动实现复杂,一共九次扰动: 5次异或 加上4次位运算
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //当size大小大于或等于阈值的时候,将会进行扩容操作,将数据元素重新计算位置后放入newTable中,Hash位置会重新计算 resize(2 * table.length); //2倍扩容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); //创建Entry的时候会有hash冲突的代码 }
不小于threshold阈值直接2倍扩容,Hash位置会重新计算。
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //根据头节点构建----头插法的实现 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
哈希冲突时链表插入数据使用头插(JDK1.7)
-
JDK7中获取数据的过程—get操作
//获取key值为key的元素值 public V get(Object key) { if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑 return getForNullKey(); Entry<K,V> entry = getEntry(key);//获取实体 return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值 } //获取key为null的实体 private V getForNullKey() { if (size == 0) {//如果元素个数为0,则直接返回null return null; } //key为null的元素存储在table的第0个位置 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null)//判断是否为null return e.value;//返回其值 } return null; }
get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。
//获取键值为key的元素 final Entry<K,V> getEntry(Object key) { if (size == 0) {//元素个数为0 return null;//直接返回null } int hash = (key == null) ? 0 : hash(key);//获取key的Hash值 for (Entry<K,V> e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶 e != null; e = e.next) {//进行遍历 Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值 return e; } return null; }
-
JDK8中HashMap源码插入的过程:
- 首先判断table成员是否初始化,如果没有,则调用resize扩容
- 通过传入键值对的key的hashCode和容量,马上得到了该映射所在的table数组下标。并通过数组的取下标操作,得到该哈希桶的头节点。
- 如果没有发生哈希碰撞(头节点为null),那么直接执行新增操作。
- 如果发生了哈希碰撞(头节点不为null),那么分为两种情况:
- 如果与桶内某个元素
==
返回True,或者equals判断相同,执行替换操作(图中的直接覆盖value) - 如果与桶内所有元素判断都不相等,执行新增操作,可能是链表也可能是红黑树的插入。
- 如果与桶内某个元素
- 链表新增操作后,会有两个判断:
- 如果哈希桶是单链表结构,且桶内节点数量超过了TREEIFY_THRESHOLD(8),且size大于等于了MIN_TREEIFY_CAPACITY(64),那么将该哈希桶转换成红黑树结构。(图中链表长度是否大于8)
- 链表长度不大于8时,如果新增后size大于了threshold,那么调用resize扩容。
static final int hash(Object key) { // 计算key的hash值 int h; // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//进行异或 }
将key的hashCode与该hashCode的无符号右移16位,异或起来得到的。
扰动实现简单 一共2次扰动 1次是异或 加上1次位运算
-
jdk8中hashmap put方法源码:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可 if ((p = tab[i = (n - 1) & hash]) == null) //头节点是否为空 tab[i] = newNode(hash, key, value, null); else { // table表该索引位置不为空,则进行查找 Node<K,V> e; K k; // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数 for (int binCount = 0; ; ++binCount) { // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部 if ((e = p.next) == null) { //一直遍历到链表的尾部,因为链表的尾部是null p.next = newNode(hash, key, value, null); // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点, // 减一是因为循环是从p节点的下一个节点开始的 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环 if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 将p指向下一个节点 } } // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用于LinkedHashMap return oldValue; } } ++modCount; // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 用于LinkedHashMap return null; }
-
JDK8 hashmap中的get方法
- 调用key的hashCode方法,根据返回值定位到map里数组对应的下标
- 判断这个数组下标对应的头节点是不是为null,如果是,返回null
- 如果头节点不是null,判断这个引用对应对象的key值的equals方法,跟查询的key值进行对比,判断是否为true,如果为true则返回这个对象的value值,否则继续遍历下一个节点。
- 如果遍历完map中所有的节点都无法满足上面的判断 则返回null
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; //计算hash值 } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 1.对table进行校验:table不为空 && table长度大于0 && // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//判断头节点是否为null // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// always check first node return first; // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历 if ((e = first.next) != null) { if (first instanceof TreeNode) // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点 if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // 6.找不到符合的返回空 return null; }