HashMap源码解读—Java8版本,2024年最新java菜鸟教程集合

在这里插入图片描述

1.5 优劣分析

HashMap 树化

链表的查找性能是O(n),若节点数较小性能不回收太大影响,但数据较大时差距将逐渐显现。树的查找性能是O(log(n)),性能优势瞬间体现

  • 优点:超级快速的查询速度,如果有人问你什么数据结构可以达到O(1)的时间复杂度,没错是HashMap,动态的可变长存储数据(和数组相比较而言)

  • 缺点:需要额外计算一次hash值,如果处理不当会占用额外的空间

二、定义


我们先来看看HashMap的定义:

public class HashMap<K,V> extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable

HashMap的类结构图

在这里插入图片描述

如何查看类的完整结构图可以参考如下文章:

IDEA如何查看类的完整结构图

三、数据结构


在这里插入图片描述

四、域的解读


public class HashMap<K,V> extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable {

//序列号,序列化的时候使用。

private static final long serialVersionUID = 362498820763181265L;

/默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。/

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//最大容量,2的30次方。

static final int MAXIMUM_CAPACITY = 1 << 30;

//加载因子,用于扩容使用。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当某个桶节点数量大于8时,会转换为红黑树。

static final int TREEIFY_THRESHOLD = 8;

//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。

static final int UNTREEIFY_THRESHOLD = 6;

//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。

static final int MIN_TREEIFY_CAPACITY = 64;

//存储元素的数组,transient关键字表示该属性不能被序列化

transient Node<K,V>[] table;

//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。

transient Set<Map.Entry<K,V>> entrySet;

//元素数量

transient int size;

//统计该map修改的次数

transient int modCount;

//临界值,也就是元素数量达到临界值时,会进行扩容。

int threshold;

//也是加载因子,只不过这个是变量。

final float loadFactor;

其中最主要的成员变量

table变量:HashMap的底层数据结构,是Node类的实体数组,Node是一个静态内部类,一种数组和链表相结合的复合结构,用于保存key-value对;

size变量:表示已存储的HashMap的key-value对的数量;

loadFactor变量:装载因子,用于衡量满的程度,默认值为0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;);

threshold变量:临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子;

capacity:并不是一个成员变量,但却是一个必须要知道的概念,表示容量,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;)

为什么默认容量大小为16,加载因子为0.75,主要原因是这两个常量的值都是经过大量的计算和统计得出来的最优解,仅仅是这样而已。

链表节点Node

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;//哈希值

final K key;//key

V value;//value

Node<K,V> next;//链表后置节点

Node(int hash, K key, V value, Node<K,V> next) {

this.hash = hash;

this.key = key;

this.value = value;

this.next = next;

}

public final K getKey() { return key; }

public final V getValue() { return value; }

public final String toString() { return key + “=” + value; }

//每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。

public final int hashCode() {

return Objects.hashCode(key) ^ Objects.hashCode(value);

}

//设置新的value 同时返回旧value

public final V setValue(V newValue) {

V oldValue = value;

value = newValue;

return oldValue;

}

public final boolean equals(Object o) {

if (o == this)

return true;

if (o instanceof Map.Entry) {

Map.Entry<?,?> e = (Map.Entry<?,?>)o;

if (Objects.equals(key, e.getKey()) &&

Objects.equals(value, e.getValue()))

return true;

}

return false;

}

}//这是一个单链表,每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。

五、构造方法


HashMap 共提供了 4 种 构造方法,满足各种常见场景下对容量的需求

// 第1种:创建一个 HashMap 并指定 容量(initialCapacity) 和装载因子(loadFactor)

public HashMap(int initialCapacity, float loadFactor) {

// 指定容量不可小于0,但可设置为 0 。之后通过put()添加元素时,会resize()

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " +

initialCapacity);

// 如果指定的容量超过了最大值,则自动置为最大值,也就是 1 << 30(也就是2的30次方)

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

// 装载因子不可小于等于 0 或 非数字(NaN)

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +

loadFactor);

// 初始化装载因子

this.loadFactor = loadFactor;

// 初始化下次需要调整到的容量(容量*装载因子)。

this.threshold = tableSizeFor(initialCapacity);

}

// 第2种:创建一个指定容量的 HashMap,装载因子使用默认的 0.75

public HashMap(int initialCapacity) {

// 调用上个构造方法初始化

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

// 第3种:创建一个默认初始值的 HashMap ,容量为16,装载因子为0.75

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

}

// 第4种:创建一个 Hashmap 并将 m 内包含的所有元素存入

public HashMap(Map<? extends K, ? extends V> m) {

this.loadFactor = DEFAULT_LOAD_FACTOR;

putMapEntries(m, false);

}

tableSizeFor(int cap)

获取一个既大于 cap 又最接近 cap 的 2 的整数次幂数值

// 假设 cap = 128

static final int tableSizeFor(int cap) {

int n = cap - 1; // 则 n = 127 = 01111111

n |= n >>> 1; // n = 01111111 , n >>> 1 = 00111111 , 按位或后 n = 01111111

n |= n >>> 2; // n = 01111111 , n >>> 1 = 00011111, 按位或后 n = 01111111

n |= n >>> 4; // n = 01111111 , n >>> 1 = 00000111, 按位或后 n = 01111111

n |= n >>> 8; // n = 01111111 , n >>> 1 = 00000000, 按位或后 n = 01111111

n |= n >>> 16; // n = 01111111 , n >>> 1 = 00000000, 按位或后 n = 01111111

// 如果 n 小于 0 则返回 1,否则判断 n 是否大于等于最大容量,是的话返回最大容量,不是就返回 n+1(也就是128)

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

六、核心方法


6.1 tableSizeFor(int cap)

获取一个既大于 cap 又最接近 cap 的 2 的整数次幂数值

// 假设 cap = 128

static final int tableSizeFor(int cap) {

int n = cap - 1; // 则 n = 127 = 01111111

n |= n >>> 1; // n = 01111111 , n >>> 1 = 00111111 , 按位或后 n = 01111111

n |= n >>> 2; // n = 01111111 , n >>> 1 = 00011111, 按位或后 n = 01111111

n |= n >>> 4; // n = 01111111 , n >>> 1 = 00000111, 按位或后 n = 01111111

n |= n >>> 8; // n = 01111111 , n >>> 1 = 00000000, 按位或后 n = 01111111

n |= n >>> 16; // n = 01111111 , n >>> 1 = 00000000, 按位或后 n = 01111111

// 如果 n 小于 0 则返回 1,否则判断 n 是否大于等于最大容量,是的话返回最大容量,不是就返回 n+1(也就是128)

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

图解

在这里插入图片描述

6.2 hash() 方法

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这个根据key取hash值的函数,称之为“扰动函数”

最开始的hashCode: 1111 1111 1111 1111 0100 1100 0000 1010

右移16位的hashCode:0000 0000 0000 0000 1111 1111 1111 1111

异或运算后的hash值: 1111 1111 1111 1111 1011 0011 1111 0101

将hashcode 与 hashcode的低16位做异或运算,混合了高位和低位得出的最终hash值(扰动算法),冲突的概率就小多了。

而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。

因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。

但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。

扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)

6.3 put(K key, V value)

public V put(K key, V value) {

/四个参数,第一个hash值,第四个参数表示如果该key存在值,如果为null的话,则插入新的value,最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可/

return putVal(hash(key), key, value, false, true);

}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

//tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标

Node<K,V>[] tab; Node<K,V> p; int n, i;

//获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

/如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p/

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

tab[i] = newNode(hash, key, value, null);

//发生哈希冲突的几种情况

else {

// e 临时节点的作用, k 存放该当前节点的key

Node<K,V> e; K k;

//第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

//第二种,hash值不等于首节点,判断该p是否属于红黑树的节点

else if (p instanceof TreeNode)

/为红黑树的节点,则在红黑树中进行添加,如果该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操作是否成功,如果添加成功返回null/

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

//第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点

else {

//遍历该链表

for (int binCount = 0; ; ++binCount) {

//如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加

if ((e = p.next) == null) {

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

//判断是否要转换为红黑树结构

if (binCount >= TREEIFY_THRESHOLD - 1)

treeifyBin(tab, hash);

break;

}

//如果链表中有重复的key,e则为当前重复的节点,结束循环

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

//有重复的key,则用待插入值进行覆盖,返回旧值。

if (e != null) {

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

//到了此步骤,则表明待插入的key-value是没有key的重复,因为插入成功e节点的值为null

//修改次数+1

++modCount;

//实际长度+1,判断是否大于临界值,大于则扩容

if (++size > threshold)

resize();

afterNodeInsertion(evict);

//添加成功

return null;

}

6.4 resize()

final Node<K,V>[] resize() {

// 把当前底层数组赋值给oldTab,为数据迁移工作做准备

Node<K,V>[] oldTab = table;

// 获取当前数组的大小,等于或小于0表示需要初始化数组,大于0表示需要扩容数组

int oldCap = (oldTab == null) ? 0 : oldTab.length;

// 获取扩容的阈值(容量*负载系数)

int oldThr = threshold;

// 定义并初始化新数组长度和目标阈值

int newCap, newThr = 0;

// 判断是初始化数组还是扩容,等于或小于0表示需要初始化数组,大于0表示需要扩容数组。若 if(oldCap > 0)=true 表示需扩容而非初始化

if (oldCap > 0) {

// 判断数组长度是否已经是最大,MAXIMUM_CAPACITY =(2^30)

if (oldCap >= MAXIMUM_CAPACITY) {

// 阈值设置为最大

threshold = Integer.MAX_VALUE;

return oldTab;

}

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

// 目标阈值扩展2倍,数组长度扩展2倍

newThr = oldThr << 1; // double threshold

}

// 表示需要初始化数组而不是扩容

else if (oldThr > 0)

// 说明调用的是HashMap的有参构造函数,因为无参构造函数并没有对threshold进行初始化

newCap = oldThr;

// 表示需要初始化数组而不是扩容,零初始阈值表示使用默认值

else {

// 说明调用的是HashMap的无参构造函数

newCap = DEFAULT_INITIAL_CAPACITY;

// 计算目标阈值

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

// 当目标阈值为0时需重新计算,公式:容量(newCap)*负载系数(loadFactor)

if (newThr == 0) {

float ft = (float)newCap * loadFactor;

newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

}

// 根据以上计算结果将阈值更新

threshold = newThr;

// 将新数组赋值给底层数组

@SuppressWarnings({“rawtypes”,“unchecked”})

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

// -------------------------------------------------------------------------------------

// 此时已完成初始化数组或扩容数组,但原数组内的数据并未迁移至新数组(扩容后的数组),之后的代码则是完成原数组向新数组的数据迁移过程

// -------------------------------------------------------------------------------------

// 判断原数组内是否有存储数据,有的话开始迁移数据

if (oldTab != null) {

// 开始循环迁移数据

for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

// 将数组内此下标中的数据赋值给Node类型的变量e,并判断非空

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

// 1 - 普通元素判断:判断数组内此下标中是否只存储了一个元素,是的话表示这是一个普通元素,并开始转移

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

// 2 - 红黑树判断:判断此下标内是否是一颗红黑树,是的话进行数据迁移

else if (e instanceof TreeNode)

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

// 3 - 链表判断:若此下标内包含的数据既不是普通元素又不是红黑树,则它只能是一个链表,进行数据转移

else { // preserve order

Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

do {

next = e.next;

if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

if (loTail != null) {

loTail.next = null;

newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

newTab[j + oldCap] = hiHead;

}

}

}

}

}

// 返回初始化完成或扩容完成的新数组

return newTab;

}

6.5 putAll(Map<? extends K, ? extends V> m)

往表中批量增加数据

public void putAll(Map<? extends K, ? extends V> m) {

//将另一个Map的所有元素加入表中,参数evict初始化时为false,其他情况为true

putMapEntries(m, true);

}

6.6 putIfAbsent(K key, V value)

只会往表中插入 key-value, 若key对应的value之前存在,不会覆盖。(jdk8增加的方法)

@Override

public V putIfAbsent(K key, V value) {

return putVal(hash(key), key, value, true, true);

}

demo:

package com.uncle;

import java.util.HashMap;

public class Main {

public static void main(String[] args) throws Exception {

HashMap<Object, Object> hashMap = new HashMap<>(5);

hashMap.put(1, null);

hashMap.putIfAbsent(1, 2);

System.out.println(hashMap.get(1));//2

hashMap.putIfAbsent(2, 3);

hashMap.putIfAbsent(2, 4);

System.out.println(hashMap.get(2));//3

// Class<? extends HashMap> aClass = hashMap.getClass();

// Field table = aClass.getDeclaredField(“table”);

// table.setAccessible(true);

// Object[] o = (Object[]) table.get(hashMap);

//

// System.out.println(o.length);

}

}

6.7 remove(Object key)

删除还有clear方法,把所有的数组下标元素都置位null,下面在来看看较为简单的获取元素与修改元素操作。

public V remove(Object key) {

//临时变量

Node<K,V> e;

/调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作/

return (e = removeNode(hash(key), key, null, false, true)) == null ?

null : e.value;

}

/第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第四个如果为false,则表示删除后,不移动节点/

final Node<K,V> removeNode(int hash, Object key, Object value,

boolean matchValue, boolean movable) {

//tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标

Node<K,V>[] tab; Node<K,V> p; int n, index;

//哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置

if ((tab = table) != null && (n = tab.length) > 0 &&

(p = tab[index = (n - 1) & hash]) != null) {

//nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value

Node<K,V> node = null, e; K k; V v;

//如果数组下标的节点正好是要删除的节点,把值赋给临时变量node

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

node = p;

//也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点

else if ((e = p.next) != null) {

if (p instanceof TreeNode)

//遍历红黑树,找到该节点并返回

node = ((TreeNode<K,V>)p).getTreeNode(hash, key);

else { //表示为链表节点,一样的遍历找到该节点

do {

if (e.hash == hash &&

((k = e.key) == key ||

(key != null && key.equals(k)))) {

node = e;

break;

}

/注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点/

p = e;

} while ((e = e.next) != null);

}

}

//找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true

if (node != null && (!matchValue || (v = node.value) == value ||

(value != null && value.equals(v)))) {

//如果删除的节点是红黑树结构,则去红黑树中删除

if (node instanceof TreeNode)

((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

//如果是链表结构,且删除的节点为数组下标节点,也就是头结点,直接让下一个作为头

else if (node == p)

tab[index] = node.next;

else /为链表结构,删除的节点在链表中,把要删除的下一个结点设为上一个结点的下一个节点/

p.next = node.next;

//修改计数器

++modCount;

//长度减一

–size;

/此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理/

afterNodeRemoval(node);

//返回删除的节点

return node;

}

}

//返回null则表示没有该节点,删除失败

return null;

}

6.8 remove(Object key, Object value)

@Override

public boolean remove(Object key, Object value) {

//这里传入了value 同时matchValue为true

return removeNode(hash(key), key, value, true, true) != null;

}

6.9 “update()”

元素的修改也是put方法,因为key是唯一的,所以修改元素,是把新值覆盖旧值。

6.10 get(Object key)

public V get(Object key) {

Node<K,V> e;

//也是调用getNode方法来完成的

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

final Node<K,V> getNode(int hash, Object key) {

//first 头结点,e 临时变量,n 长度,k key

Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

//头结点也就是数组下标的节点

if ((tab = table) != null && (n = tab.length) > 0 &&

(first = tab[(n - 1) & hash]) != null) {

//如果是头结点,则直接返回头结点

if (first.hash == hash &&

((k = first.key) == key || (key != null && key.equals(k))))

return first;

//不是头结点

if ((e = first.next) != null) {

//判断是否是红黑树结构

if (first instanceof TreeNode)

//去红黑树中找,然后返回

return ((TreeNode<K,V>)first).getTreeNode(hash, key);

do { //链表节点,一样遍历链表,找到该节点并返回

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

return e;

} while ((e = e.next) != null);

}

}

//找不到,表示不存在该节点

return null;

}

6.11 containsKey(Object key)

判断是否包含该key

public boolean containsKey(Object key) {

return getNode(hash(key), key) != null;

}

6.12 containsValue(Object value)

判断是否包含value

public boolean containsValue(Object value) {

Node<K,V>[] tab; V v;

//遍历哈希桶上的每一个链表

if ((tab = table) != null && size > 0) {

for (int i = 0; i < tab.length; ++i) {

for (Node<K,V> e = tab[i]; e != null; e = e.next) {

//如果找到value一致的返回true

if ((v = e.value) == value ||

(value != null && value.equals(v)))

return true;

}

}

}

return false;

}

6.13 getOrDefault(Object key, V defaultValue)

以key为条件,找到了返回value。否则返回defaultValue

@Override

public V getOrDefault(Object key, V defaultValue) {

Node<K,V> e;

return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;

}

6.14 遍历

//缓存 entrySet

transient Set<Map.Entry<K,V>> entrySet;

*/

public Set<Map.Entry<K,V>> entrySet() {

Set<Map.Entry<K,V>> es;

return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;

}

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {

public final int size() { return size; }

public final void clear() { HashMap.this.clear(); }

//一般我们用到EntrySet,都是为了获取iterator

public final Iterator<Map.Entry<K,V>> iterator() {

return new EntryIterator();

}

//最终还是调用getNode方法

public final boolean contains(Object o) {

if (!(o instanceof Map.Entry))

return false;

Map.Entry<?,?> e = (Map.Entry<?,?>) o;

Object key = e.getKey();

Node<K,V> candidate = getNode(hash(key), key);

return candidate != null && candidate.equals(e);

}

//最终还是调用removeNode方法

public final boolean remove(Object o) {

if (o instanceof Map.Entry) {

Map.Entry<?,?> e = (Map.Entry<?,?>) o;

Object key = e.getKey();

Object value = e.getValue();

return removeNode(hash(key), key, value, true, true) != null;

}

return false;

}

//。。。

}

final class EntryIterator extends HashIterator

implements Iterator<Map.Entry<K,V>> {

public final Map.Entry<K,V> next() { return nextNode(); }

}

abstract class HashIterator {

Node<K,V> next; // next entry to return

Node<K,V> current; // current entry

int expectedModCount; // for fast-fail

int index; // current slot

HashIterator() {

//因为hashmap也是线程不安全的,所以要保存modCount。用于fail-fast策略

expectedModCount = modCount;

Node<K,V>[] t = table;

current = next = null;

index = 0;

//next 初始时,指向 哈希桶上第一个不为null的链表头

if (t != null && size > 0) { // advance to first entry

do {} while (index < t.length && (next = t[index++]) == null);

}

}

public final boolean hasNext() {

return next != null;

}

//由这个方法可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。属于无序集合。

final Node<K,V> nextNode() {

Node<K,V>[] t;

Node<K,V> e = next;

//fail-fast策略

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

if (e == null)

throw new NoSuchElementException();

//依次取链表下一个节点,

if ((next = (current = e).next) == null && (t = table) != null) {

//如果当前链表节点遍历完了,则取哈希桶下一个不为null的链表头

do {} while (index < t.length && (next = t[index++]) == null);

}

return e;

}

public final void remove() {

Node<K,V> p = current;

if (p == null)

throw new IllegalStateException();

fail-fast策略

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

current = null;

K key = p.key;

//最终还是利用removeNode 删除节点

removeNode(hash(key), key, null, false, false);

expectedModCount = modCount;

}

}

七、阿里面试实战


7.1、为什么需要散列表

HashMap中的数据结构为散列表,又名哈希表。在这里我会对散列表进行一个简单的介绍,在此之前我们需要先回顾一下 数组、链表 的优缺点。

  • 数组:数组删除、插入性能不佳,寻址性能极优

  • 链表:链表查询性能不佳,删除、插入性能极优

数组的优缺点取决于他们在内存中存储的模式,也就是直接使用顺序存储或链式存储导致的。无论是数组还是链表,都有明显的缺点。而在实际业务中,我们想要的往往是寻址、删除、插入性能都很好的数据结构,散列表就是这样一种结构,它巧妙的结合了数组与链表的优点,并将其缺点弱化(并不是完全消除)

7.2 能说一下HashMap的数据结构吗?

JDK1.7的数据结构是数组+链表

JDK1.8的数据结构是数组+链表+红黑树。

数据结构示意图如下:

在这里插入图片描述

其中,桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。

  • 数据元素通过映射关系,也就是散列函数,映射到桶数组对应索引的位置

  • 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素

  • 如果链表长度>8&数组大小>=64,链表转为红黑树

  • 如果红黑树节点个数<6 ,转为链表

7.3 你对红黑树了解多少?为什么不用二叉树/平衡树呢?

红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:

  • 每个节点要么是红色,要么是黑色;

  • 根节点永远是黑色的;

  • 所有的叶子节点都是是黑色的(注意这里说叶子节点其实是图中的 NULL 节点);

  • 每个红色节点的两个子节点一定都是黑色;

  • 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

*

之所以不用二叉树:

红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。

之所以不用平衡二叉树:

平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。

7.4 红黑树怎么保持平衡的知道吗?

红黑树有两种方式保持平衡:旋转和染色。

  • 旋转:旋转分为两种,左旋和右旋

  • 染⾊

7.5 HashMap的put流程知道吗?

在这里插入图片描述

首先进行哈希值的扰动,获取一个新的哈希值。(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

判断tab是否位空或者长度为0,如果是则进行扩容操作。

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

根据哈希值计算下标,如果对应小标正好没有存放数据,则直接插入即可否则需要覆盖。tab[i = (n - 1) & hash])

判断tab[i]是否为树节点,否则向链表中插入数据,是则向树中插入节点。

如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树。treeifyBin(tab, hash);

最后所有元素处理完成后,判断是否超过阈值;threshold,超过则扩容。

7.6 HashMap怎么查找元素的呢?

在这里插入图片描述

HashMap的查找就简单很多:

  • 使用扰动函数,获取新的哈希值
  • 计算数组下标,获取节点
  • 当前节点和key匹配,直接返回
  • 否则,当前节点是否为树节点,查找红黑树
  • 否则,遍历链表查找

7.7 HashMap的哈希/扰动函数是怎么设计的?

HashMap的哈希函数是先拿到 key 的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作。

static final int hash(Object key) {

int h;

// key的hashCode和key的hashCode右移16位做异或运算

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这么设计是为了降低哈希碰撞的概率。

7.8 为什么哈希/扰动函数能降hash碰撞?

因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为 -2147483648~2147483647,加起来大概 40 亿的映射空间。

只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。

假如 HashMap 数组的初始大小才 16,就需要用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

源码中模运算就是把散列值和数组长度 - 1 做一个 “与&” 操作,位运算比取余 % 运算要快。

bucketIndex = indexFor(hash, table.length);

static int indexFor(int h, int length) {

return h & (length-1);

}

顺便说一下,这也正好解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为这样(数组长度 - 1)正好相当于一个 “低位掩码”。与 操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度 16 为例,16-1=15。2 进制表示是 0000 0000 0000 0000 0000 0000 0000 1111。和某个散列值做 与 操作如下,结果就是截取了最低的四位值。

在这里插入图片描述

这样是要快捷一些,但是新的问题来了,就算散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。如果散列本身做得不好,分布上成等差数列的漏洞,如果正好让最后几个低位呈现规律性重复,那就更难搞了。

这时候 扰动函数的价值就体现出来了,看一下扰动函数的示意图:

在这里插入图片描述

右移 16 位,正好是 32bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

7.9 为什么HashMap的容量是2的倍数呢?

第一个原因是为了方便哈希取余:

  • 将元素放在table数组上面,是用hash值%数组大小定位位置,而HashMap是用hash值&(数组大小-1),却能和前面达到一样的效果,这就得益于HashMap的大小是2的倍数,2的倍数意味着该数的二进制位只有一位为1,而该数-1就可以得到二进制位上1变成0,后面的0变成1,再通过&运算,就可以得到和%一样的效果,并且位运算比%的效率高得多
  • HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

第二个方面是在扩容时,利用扩容后的大小也是2的倍数,将已经产生hash碰撞的元素完美的转移到新的table中去.

7.10 如果初始化HashMap,传一个17的值new HashMap<>,它会怎么处理?

简单来说,就是初始化时,传的不是2的倍数时,HashMap会向上寻找离得最近的2的倍数,所以传入17,但HashMap的实际容量是32。

我们来看看详情,在HashMap的初始化中,有这样⼀段⽅法;

public HashMap(int initialCapacity, float loadFactor) {

this.loadFactor = loadFactor;

this.threshold = tableSizeFor(initialCapacity);

}

  • 阀值 threshold ,通过⽅法 tableSizeFor 进⾏计算,是根据初始化传的参数来计算的。

  • 同时,这个⽅法也要要寻找⽐初始值⼤的,最⼩的那个2进制数值。⽐如传了17,我应该找到的是32。

static final int tableSizeFor(int cap) {

int n = cap - 1;

n |= n >>> 1;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值