预备知识
异或运算
异或的基本运算
异或是二元按位运算符的一种,最基本的概念是在当前位下,两个二进制相同则为0,不同则为1
public static void main(String []args){
int i = -5;
int j = 5;
System.out.println("i : " + Integer.toBinaryString(i));
System.out.println("j : " + Integer.toBinaryString(j));
System.out.println("i ^ j : " + Integer.toBinaryString(i ^ j));
System.out.println("i ^ j : " + (i ^ j));
/**
* output :
* i : 11111111111111111111111111111011
* j : 101
* i ^ j : 11111111111111111111111111111110
* i ^ j : -2
*/
}
异或规律
0异或A结果都为A : 0 ^ 1 = 1,0 ^ 0 = 0
1异或A结果都为A的相反数 : 1 ^ 1 = 0,1 ^ 0 = 1
A异或A结果都为0
异或进阶
关于异或进阶不再赘述,大家可以观看维基百科中的异或讲解
值得一提的是面试经常会考到:如何不使用第三个参数来交换a和b的值,这时候就可以使用异或。
int a = 10;
int b = 41;
a = b ^ a;
b = b ^ a; // b = b ^ ( b ^ a) = a
a = a ^ b; // a = ( b ^ a ) ^ a = b
左移和右移运算
先从最简单的开始说,左移就是低位补0,右移就是高位补符号位,无符号右移运算符就是高位补0(这里不再赘述)。
接着 在一般情况下 左移和右移都有下面的规律 :
a << b = a * (2 ^ b)
a >> b = a / (2 ^ b)
例如 :
public static void main(String []args){
int i = 5;
int j = 32;
int m = 40;
System.out.println(3 << i);
System.out.println(3 << j);
System.out.println(3 << m);
/**
* output :
* 96
* 3
* 768
*/
}
大家可能发现了,有些结果好像与公式并不符合,这是因为int类型的“限制”。
众所周知,在Java中int占四个字节,也就是32位,在做移位操作时,无论是左移还是右移,右操作运算符都不能大于或者等于32(当然,我说的是左边的数值是int类型时),因为一旦超过了32,左边的数值完全变了个样。就拿1 << 32而言:
我们发现1 << 32如果按照正常计算的话就会变成0,更别提3 << 32,5 << 91了,如果按照正常流程来的话,只要右操作数大于32,结果都变成了0,当然,Java不可能这样子计算,它采取了一种聪明的操作方式,至于为什么,目前不在讨论范围之内。另一种状况是,如果恰巧将1移到了符号位上,即便移位之前是正的数字,也会变成负的,就像1 << 31 = - 2147483648 那样。所以,在上边说的公式有诸多的限制。
这种聪明的方式就是,在左操作数类型为int时,当右侧数值大于等于32,都可以对右操作数取余操作( % 32),然后再做移位运算,例如:
3 << 40 = 3 << (40 % 32) = 3 << 8 = 3 * (2 ^ 8) = 768
以上都说的是int类型下的移位操作,如果左边操作数为char、byte、short类型都会被转换成int类型处理,当左操作数是long的时候,右边操作数的最大限制变成了64。
HashMap的结构
JDK1.8的HashMap结构如下图所示,左边部分为哈希表,也叫哈希数组,该数组里面的每个元素都是一条单链表的头结点或者是红黑树的根节点。
HashMap又称为哈希桶,在JDK1.8前,HashMap的实现方式为:数组+链表,往桶里面添加元素时是通过计算hash值来确定该元素在桶中的位置,即hash值对应着数组的下标,当多个元素的hash值相同时,这些元素会存储在一个链表上,从而解决了冲突。当hash值相同的元素过多时,即一条链表上的节点过多时,通过key查找的效率较低,因此在JDK1.8时HashMap采用 数组+链表+红黑树 实现,当链表长度大于阀值8并且数组长度大于64时,链表结构会转换为红黑树结构,从而大大提高了查询效率。
将链表转化为红黑树前会判断,即使链表长度大于8,但是如果数组长度小于64,此时并不会将链表转为红黑树,而是选择进行数组扩容,扩容后链表长度很有可能将小于8。
这样做点目的是,因为数组比较小时尽量避开红黑树结构,这种情况下如果变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋、右旋、变色这些操作来保持平衡。同时,数组长度小于64时,搜索时间相对快些。综上所述,为了提高性能和减少搜索时间,底层在链表长度大于8并且数组长度大于64时,链表才转为红黑树。具体可以参考treeifyBin方法。
在单链表查找一个元素的时间复杂度为O(n),当单链表中的节点数过多时,查找效率会大大降低;增删元素快,只需处理节点间的引用,时间复杂度为O(1)。
因此当链表过长时,查询耗时严重,这时我们引入了红黑树,它查找、插入、删除的平均时间复杂度为O(log(n))。
总的来说,JDK1.8,HashMap的结构是:数组+链表+红黑树。来看下源码中的数据结构。
数组:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
链表节点:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V 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;
}
......
红黑树节点:
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
......
小结:
1.HashMap是存取无序的
2.键和值都可以是null,但是只有一个键可以为null,键和值都不能是基本数据类型(即不能为boolean、byte、char、short、int、long 、float、double)
3.键是唯一的,底层数据结构控制键的唯一性
4.jdk1.8前数据结构是:数组+链表,jdk1.8后数据结构是:数组+链表+红黑树
5.当链表长度大于阀值8并且数组长度大于64时,才将链表转为红黑树,变为红黑树的目的是为了高效的查询。
HashMap的属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列号,序列化的时候使用。
private static final long serialVersionUID = 362498820763181265L;
/**数组的默认初始容量,16,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对象,方便在不知道key的情况下遍历HashMap对象
transient Set<Map.Entry<K,V>> entrySet;
//数组中存放的元素的个数,不是数组的长度(数组的某些位置可能没有存放元素)
transient int size;
//HashMap结构被修改的次数
transient int modCount;
//数组扩容的临界值,当HashMap当前的元素个数大于等于threshold时,数组就会进行扩容。threshold = capacity * load factor
int threshold;
//加载因子
final float loadFactor;
capacity
容量,默认值为16。就是指HashMap中桶的数量,即哈希表的数组的长度。容量都是2的幂。
loadFactor
装载因子,默认值为0.75f。
threshold
扩容的阈值。
threshold=capacity*loadFactor
当HashMap的size大于threshold(size > threshold)时会执行resize操作。
size
size表示HashMap中存放的元素的总数,即所有存放的所有KV键值对的数量。
构造方法
// 默认构造函数,加载因子为默认值 0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 指定了初始容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 指定了初始容量和加载因子,会对参数进行校验
// 初始容量不能为负数,不能大于最大容量 1 << 30
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor()方法会将我们传入的容量设置为大于并最接近的2^N,例如之前传入的初始容量为11,通过tableSizeFor(11),则返回16
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
put方法
put方法
当我们调用put方法添加元素时,实际是调用了其内部的putVal方法
public V put(K key, V value) {
//先根据key值获得hash值,然后调用putVal方法添加元素
return putVal(hash(key), key, value, false, true);
}
第一个参数需要对key求hash值,为了减少hash碰撞。
hash方法
扰动函数,该方法是为了防止一些实现的比较差的 hashCode() 方法,减少碰撞,尽可能使元素散列地存储。
hash = key的hashCode值与该hashCode值的高16位做异或运算
static final int hash(Object key) {
int h;
//首先获得该对象的hashCode值,然后将该值和该值右移16位后的值进行异或运算后返回结果
//目的是为了减少哈希冲突
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该方法是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
putVal方法
第一步其实就是对数组进行初始化;
如果定位到的数组位置没有元素就直接插入;
如果定位到的数组位置有元素就要与插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入红黑树;如果不是就遍历链表尾部插入。
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;
//判断当前数组是否已经初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;// 若未初始化,调用resize方法进行初始化(默认长度为16)
//i = (n - 1) & hash用来计算待添加的元素应在数组中的下标
//如果计算出来的位置还未存储元素,则新生成一个链表结点存放在此处
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果计算出来的位置已经有元素存放,即发生哈希冲突
else {
Node<K,V> e; K k;
//如果该位置的第一个元素的hash值和key值都和待添加的元素的相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将第一个元素赋值给e,用e来记录
e = p;
//如果节点为红黑树结点
else if (p instanceof TreeNode)
//将待添加的元素放入红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果节点为链表结点
//在链表尾部插入结点(jdk1.7采用头插法,容易造成死循环)
//遍历当前链表
for (int binCount = 0; ; ++binCount) {
//p.next为空表示p为链表的尾部
if ((e = p.next) == null) {
//将待添加的元素插入该链表尾部
p.next = newNode(hash, key, value, null);
//当该链表的结点数量达到阈值,转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//跳出循环
break;
}
//如果链表的当前结点的hash值和key值都和插入的元素相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//用来遍历链表
p = e;
}
}
// e!= null,表示在桶中找到的元素的hash值和key值与插入元素都相同的结点
if (e != null) {
//记录结点的原来的value值
V oldValue = e.value;
//如果onlyIfAbsent为false或者原来的value值为空
if (!onlyIfAbsent || oldValue == null)
//用待添加的元素的vaule值来替换原来的value值
e.value = value;
//回调
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//结构改变时计数器加一
++modCount;
// 每次put一个元素++size,判断当前数组的元素个数是否大于临界值
if (++size > threshold)
//大于的话对数组进行扩容
resize();
//插入后回调,这是一个空实现的函数,用作LinkedHashMap重写使用。
afterNodeInsertion(evict);
return null;
}
下面图片是美团技术团队的put函数的流程总结 :
上面就是具体的元素添加过程,在元素添加过程里涉及到扩容,我们来看看扩容方法resize。
resize()方法
扩容过程中,会遍历hash表中的所有元素,是非常耗时的,在写程序时要尽量避免扩容的发生。
HashMap在进行扩容时,使用的方式非常巧妙,因为每次扩容都是翻倍,也就是新容量=原来的容量*2,并且因为计算index值的公式为 (n-1)&hash,hash值不变,影响的只是n变为原来的2倍。扩容后,n在二进制看就是在原来的基础上向左移动了1位,就是说,扩容后的 n-1 的二进制有效位比原来的多一个1(如:原来n-1的二进制为1111,扩容后n-1的二进制则是11111)。所以与相同的hash进行&运算后,index要么在原来的位置,要么是原来位置+原来的容量值,取决于hash值在(n-1)扩容后新增的这个bit位上对应的值是0还是1。
以容量为16扩容为32为例:
总结:
因为容量是2的次幂,所以n-1在扩容之后会在高bit位多个1,所以扩容之后的新索引位置要么等于原来索引,要么等于原来索引+旧数组的容量。 我们在扩容时,不需要重新计算hash值,只需要看看原来的hash值在扩容后新增的那个bit位上是1还是0就可以了,是0的话,索引就没变,即新索引=原索引;是1的话,新索引=原索引+旧容量。
可以看看下图为容量由16扩容为32的resize过程的元素移动的示意图:
正因为这样巧妙的方式,既省去了重新计算hash值的时间,而且同时,由于hash值在扩容后的那个新增bit位上的值是0还是1可以认为是随机的,在resize的过程中保证了每个哈希桶上的节点数量一定<=原来哈希桶上的节点数量,保证了扩容之后不会出现更严重的哈希冲突。均匀的把之前的冲突节点分散到新的哈希桶中。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; // table如果为空,oldCap长度设置为0
int oldThr = threshold;
int newCap, newThr = 0;
// 1.0 旧数组中有元素,说明已初始化过,调用resize()是进行扩容的
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 1.1 旧数组长度大于最大容量2^30,则将阈值设置为Integer的最大值
threshold = Integer.MAX_VALUE;
// 但数据大小不变
return oldTab;
}
// 1.2 旧数组双倍扩容后小于最大容量 并且 旧数组大于默认的初始容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将阈值threshold*2得到新的阈值
newThr = oldThr << 1;
}
// 2.0
// 旧阈值=threshold大于0
// 说明使用的构造方法是HashMap(int initialCapacity, float loadFactor)
// 该方法中 this.threshold = tableSizeFor(initialCapacity);
// tableSizeFor方法返回的是数组的容量(2^N),例如initialCapacity是1000,那么得到的threshold就是1024,这里threshold就等于数组的容量
else if (oldThr > 0)
// 容量设置为阈值threshold
newCap = oldThr;
else {
// 3.0 阈值为初始化时的0,oldCap为空,即创建数组时无参,调用resize()是为了初始化为默认值
// 将新的长度设置为默认的初始化长度,即16
newCap = DEFAULT_INITIAL_CAPACITY;
// 负载因子0.75*数组长度16=12 新阈值为12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4.0 如果新阈值为0,根据负载因子设置新阈值
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]; // 创建一个长度为newCap的新的Node数组
table = newTab;
// 如果旧的数组中有数据,则将数组复制到新的数组中
if (oldTab != null) {
// 循环遍历旧数组,将有元素的节点进行复制
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 旧数组在索引j的哈希桶上存储有元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//将旧数组中存储的引用置为null
// 该哈希桶中只有一个元素
if (e.next == null)
// 重新计算元素的位置
newTab[e.hash & (newCap - 1)] = e;
// 该哈希桶是红黑树
else if (e instanceof TreeNode)
// 将原本的二叉树结构拆分组成新的红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 该哈希桶是链表
else { // preserve order
// jdk1.8中 旧链表迁移新链表 链表元素相对位置没有变化; 实际是对对象的内存地址进行操作
// jdk1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置
//loHead用于存储低位(位置不变)key的链头,loTail用于指向链尾位置。
Node<K,V> loHead = null, loTail = null;
//hiHead用于存储即将存储在高位的key的链头,hiTail用于指向链尾位置。
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// hash值与旧的长度做与运算用于判断元素的在数组中的位置是否需要移动
/**
* 举例:
* (e.hash & oldCap) == 1
* e.hash & (oldCap - 1) e.hash & (newCap - 1) e.hash & oldCap
* ...0101 0010 ...0101 0010 ...0101 0010
* & 0 1111 1 1111 1 0000
* 0 0010 1 0010 1 0000
*
* (e.hash & oldCap) == 0
* e.hash & (oldCap - 1) e.hash & (newCap - 1) e.hash & oldCap
* ...0100 0010 ...0100 0010 ...0100 0010
* & 0 1111 1 1111 1 0000
* 0 0010 0 0010 0 0000
*/
// 总结:
// 数组的长度为2^N,即高位为1,其余为0,计算e.hash & oldCap只需看oldCap最高位1所对应的hash位
// 因为newCap进行了双倍扩容,即将oldCap左移一位,那么oldCap-1相当于newCap-1右移一位,右移后高位补0,与运算只能得到0。
// 如果(e.hash & oldCap) == 0,hash值需要与运算的那一位为0,那么oldCap - 1与newCap - 1的高位都是0,其余位又是相同的,表明旧元素与新元素计算出的位置相同。
// 同理,当其 == 1 时,oldCap-1高位为0,newCap-1高位为1,其余位相同,计算出的新元素的位置比旧元素位置多了2^N,即得出新元素的下标=旧下标+oldCap
//如果为0,意味着在新数组中的位置是不变的,将这些元素组成一个链条
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 首位
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 如果不为0,元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长,将这些元素组成一个链条。
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;
}
不难看出,loHead和loTail两个节点分别记录不需要移动的链表的头部和尾部,hiHead和hiTail分别记录需要移动的链表头部和尾部。
假设在扩容的时候某个数组下有这样一个链表 :
其中,假设天蓝色部分的不需要挪动,红色部分的需要挪动。
第一步 : 建立loHead, loTail, hiHead, hiTail四个链表节点
第二步 :
第三步 :
…
第N步 :
最后一步 :
把以loHead为首的链表放到新数组的原索引位置,把以hiHead为首的链表放到新数组的原索引位置+oldCap的位置,这就是扩容时链表的操作。
treeifyBin方法
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*
* 替换哈希表中指定索引的哈希桶中的所有节点,除非哈希表太小,哈希表太小时将进行扩容操作。
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//当数组长度 < MIN_TREEIFY_CAPACITY 时进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd:红黑树的头结点,tl:红黑树的尾结点
TreeNode<K,V> hd = null, tl = null;
//进行while循环把链表中的所有节点替换为树节点,此循环结束后并没有转化为红黑树,只是把所有节点转为树节点
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//把头结点hd放入哈希桶中,并把转换好的所有树节点进行树化,此时,index位置的哈希桶中的所有节点转化为红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object 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 && // always check first node
((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;
}
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
根据给定的key来删除键值对;如果key对应的value值存在,删除该键值对并返回value值,如果不存在,返回null。
removeNode方法
先找到元素的索引位置,如果是链表,就遍历链表,找到元素后删除。如果是红黑树,就遍历红黑树,找到元素后删除,树中节点数小于6时转为链表。
//如果matchValue值为true时,必须key和value值都相同时才能删除该结点
//如果movable值为false,删除一个结点时不移动其他结点
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab;
//待删除结点的前置结点
Node<K,V> p;
int n, index;
//如果数组不为空且根据hash算出来的数组下标对应的位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node用来保存待删除结点
Node<K,V> node = null, e; K k; V v;
//如果第一个元素正好是要删除的节点
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;
//链表长度减1
--size;
afterNodeRemoval(node);
//返回旧的已经删除的结点
return node;
}
}
return null;
}
HashMap jdk1.8比1.7的优化
jdk1.8的hash方法比1.7性能有所提升;
jdk1.7链表采用头插法,头插法比较快,但容易造成死循环;jdk1.8是尾插法;
jdk1.8中 旧链表迁移新链表 链表元素相对位置没有变化; 实际是对对象的内存地址进行操作,jdk1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置
参考:
Java 8系列之重新认识HashMap
图解HashMap(转载)
详细梳理JAVA7和JAVA8 HashMap的hash实现
Map中的hash()分析的最透彻的文章
HashMap源码解析JDK1.8
HashMap源码解读(jdk1.8)
HashMap(JDK1.8)源码分析(超详细)