文章目录
HashMap
哈希表
是元素和地址之间的映射表。
根据设定的hash函数 H(key) 和处理冲突的方法将一组关键字【元素】映射到一个有限的连续的地址集【区间】上。
映像过程称为散列,所得的存储地址称为散列地址。
哈希函数
将元素的关键字作为哈希函数的参数,结果可以得出一个哈希地址。
注意:该哈希地址一定是在给定的区间内。因为哈希函数中,有一个操作是 所得的初始结果 mod (区间长度)=最终的结果。
处理冲突的方法
两个不同元素的关键字,通过哈希函数,得出了相同的地址映射。
注意:一定是两个不同的关键字,才叫做地址冲突。
所以在哈希表中,所有的元素,关键字都不相同。如果插入元素与表内元素关键字出现相同,则被认定为是替换表内元素,而不是插入新元素。
以下对不同元素key,产生相同的地址的叫做同义词。否则为非同义词。
-
开放定址法:Address= ( H(key) + d ) MOD m
其中Address为散列地址、H()为hash函数 、key 元素关键字、d为增量序列、m为哈希表长度。
如果发生地址冲突,将依据该地址向左或向右寻找,直到有其他地址为空【即该地址无元素占据时】,将插入元素放入。hash表由线性表实现时,使用这种方法
初始d=0 只有d=0出现冲突时,d开始变化,继续计算hash地址,直到无冲突的出现。d的变化方式有以下3种:
- 线性探测再散列:向右边探测下去 d= 1 2 3 4 5 6 ···
- 二次探测再散列:右边一次,左边一次 d= 1 -1^2 2 -2^2 3 -3^2 ···
- 伪随机探测再散列:随机给一个数进行探测 d=Random number
缺点:在处理同义词冲突过程中,又添加了非同义词的冲突【位置被占据】。发生二次聚集
-
再哈希法:Address=RH(key) RH是多个不同的哈希函数,当元素关键字产生地址冲突时,就使用另一个hash函数计算地址,直到不出现冲突。
缺点:不易产生聚集,但增加了计算时间
-
链地址法:将所有关键字为同义词的记录存储在同一线性链表中。
- 设立一个Node数组,每个数组节点都是一个链表head节点。
- 将hash地址为 i 的元素,插入到 list[i] 为 head节点的链表中。
- 保证了所有同义词都在一个链表中,非同义词之间不会互相影响。
- 插入链表的过程中,进行必要的大小判断,可以做到链表元素有序。
节点大致结构
class Node
{
T key; //元素的关键字
T val; //元素值
Node next; //指向下一个节点
}
JDK1.7 HashMap
此版本的HashMap的结构为 桶数组+链表
结构分析
该数组作为哈希表的桶数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
//参数分别为 h哈希值 K键 V值 Entry下一个结点
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
构造方法
(1)真实 capacity 为大于 initialCapacity的第一个 2^n,比如initialCapacity=17,则capacity=32
(2)计算阈值
(3)创建哈希桶数组
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
//capacity 为大于 initialCapacity的第一个 2^n
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//计算阈值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建数组
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
hash算法
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
// 0 ^ hashCode = hashCode
h ^= k.hashCode();
//一种算法,进行4次位移,得到相对比较分散的链表
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
根据hash获取桶的索引
注意:桶的长度始终为偶数,则length-1始终为奇数。 这个时候 h&(length-1) 等价于 h mod (length-1)
static int indexFor(int h, int length) {
return h & (length-1);
}
put操作
(1)【key判断】key是否为null? 如果key=null,则hash为0,桶index=0,进行(3.2)。否则(2)
(2)【获得桶index】获取新元素key的hash值,根据hash获得桶index
(3.1)【遍历index桶】遍历 index桶链表 ,看看有没有 key重复,如果重复,则进行value替换,然后return。否则,出现两种情况 【桶链表=null】 or【没有发生key冲突】 先更新下集合操作次数modCount,进行(4)
(3.2)【遍历0桶】遍历 index桶链表 ,看看有没有 key重复,如果重复,则进行value替换,然后return。否则,出现两种情况 【桶链表=null】 or【没有发生key冲突】 先更新下集合操作次数modCount,进行(4)
(4)【插入结点之前的扩容判断】 判断 [当前大小集合size >= 阈值threshold的大小] 以及 [当前桶链表存在] 则扩容
(5)【插入元素】使用头插法
//
public V put(K key, V value) {
//HashMap允许key为空
if (key == null)
return putForNullKey(value);
//获取新元素的哈希值
int hash = hash(key);
//根据哈希值获得桶的索引
int i = indexFor(hash, table.length);
//开始循环 准备插入
//从index为i的桶的头节点开始进行
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//新元素 与 链表中原有元素的键完全相同 进行value的替换
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//否则 插入元素
//modCount 表示对该集合有效修改的次数 insert delete
modCount++;
//添加链表结点
addEntry(hash, key, value, i);
return null;
}
//put操作涉及到的其他方法
//单独对 key=null的元素 进行插入操作
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
//putForNullKey涉及的方法
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断下 [当前大小size 和 阈值threshold] 以及 [当前桶是否存在]
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//以上将所有条件准备好了
//将新元素的信息插入
createEntry(hash, key, value, bucketIndex);
}
//addEntry涉及的方法
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++;
}
扩容操作
(1)容量的边界判断
(2)根据新容量创建新的哈希表
(3)进行transfer扩容 大循环每个桶,小循环每个桶的链表,依次取出每个链表结点根据hash获得新表的index,使用头插法即可。
//newCapacity = 2 * oldCapacity
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//边界判断
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建新的哈希表
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//标记 扩容时是否需要重新hash
boolean rehash = oldAltHashing ^ useAltHashing;
//进行扩容
transfer(newTable, rehash);
//将扩容后的新数组 赋给 原来的引用
table = newTable;
//计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//for循环遍历old哈希桶
for (Entry<K,V> e : table) {
while(null != e) {
//记录当前结点的下一个
Entry<K,V> next = e.next;
//true 表示每个元素都需要重新hash
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//得到新哈希表的桶索引
int i = indexFor(e.hash, newCapacity);
//头插法
e.next = newTable[i];
newTable[i] = e;
//e = e.next
e = next;
}
}
}
get操作
(1)根据key得出hash值,如果key=null 则hash=0
(2)根据hash得出对应的桶
(3)从桶头节点开始遍历,找到 key一致且hash一致的结点,返回该节点
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key); //key不为空,取得key的hash值
//通过indexFor取得该hash值在数组table中的偏移量得到Entry类的单向链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//通过循环在单向链表中寻找相同hash值,相同key值确定链表中的具体实例。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
JDK1.8 HashMap
结构分析
结构
1.8的HashMap是 数组+链表+红黑树
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。
其中
- 数组 通过 Node []table 实现
- 链表 是由多个Node连接
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是Map中的一个内置接口,定义了map元素的存、取等方法
HashMap就是使用哈希表的方式来存储元素
字段解释
除此之外,还有几个HashMap中的必要字段
- DEFAULT_INITIAL_CAPACITY:哈希表数组的默认大小 1<<4=16,会在第一次插入元素时,通过resize()扩容创建一个index在【0~15】的table哈希表数组 数组的元素为Node。 注意:执行对象无参数构造方法时,并不会创建table数组
- MAXIMUM_CAPACITY:哈希表数组的最大容量,为2^30。
- DEFAULT_LOAD_FACTOR:默认负载因子,0.75
- size:map当前包含的键值对数量
- modCount:用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
- threshold:map所能容纳的最大键值对个数,threshold = length * Load factor
- loadFactor:负载因子,可以在创建HashMap对象时指定,默认为0.75
构造函数
根据初始化容量得出大于初始化容量的最小 2^n ,当前阈值直接等于2^n,并不会初始化哈希桶。
初始化创建桶会在第一次扩容时创建,容量为当前的阈值,阈值=容量x负载因子,此后新容量即为旧容量的2倍。
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);
}
//cap为初始化容量
//实际就是找出了
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;
}
hash算法
hash算法在插入、删除和查找键值对的过程中非常重要,定位到哈希桶数组的位置都是很关键的第一步。
HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。
HashMap中【JDK1.7、JDK1.8】,Hash算法本质上就是三步:取key的hashCode值、补充高位运算、取模运算。
JDK1.8 哈希算法
//返回值为hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
根据hash可以确定该元素放在 哈希表数组的 index=(n - 1) & hash
问题1:对于hash & mask【mask=table.length-1】 起初有些不解,如果要保证每个元素都能得到 【0 ~table.length-1】区间的地址,不应该是对hash进行 MOD mask。因为如果mask为偶数,【即哈希表数组长度为奇数】,进行 hash & mask操作的话,岂不是 mask之前的hash得到的地址为 0,mask之后的hash为mask了么?
举例:
分析:通过阅读源码可知,哈希表数组的初始长度为16是偶数,这样保证了mask为奇数,执行 hash & mask不会出现问题。 且哈希表进行扩容后的数组长度仍然是偶数。
当length总是2的n次方时,hash & mask运算等价于对mask取模,也就是hash%mask,但是&比%具有更高的效率。
问题2:为什么不直接用 hashCode() 而是用它的高 16 位进行异或计算新 hash 值?
int 类型占 32 位,可以表示 2^32 种数(范围:-2^31 到 2^31-1),而哈希表长度一般不大,在 HashMap 中哈希表的初始化长度是 16(HashMap 中的 DEFAULT_INITIAL_CAPACITY),如果直接用 hashCode 来寻址,那么相当于只有低 4 位有效,其他高位不会有影响。这样假如几个 hashCode 分别是 2^10、 2^20、 2^30,那么寻址结果 index 就会一样而发生冲突,所以哈希表就不均匀分布了。
为了减少这种冲突,HashMap 中让 hashCode 的高位也参与了寻址计算(进行扰动),即把 hashCode 高 16 位与 hashCode 进行异或算出 hash,然后根据 hash 来做寻址。
插入
//插入元素操作
//hash() 哈希函数可以得出 该元素所对应的 初始地址 【即没有与mask进行& 操作的地址】
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash算法
static final int hash(Object key) {
int h;
//如果key=null 则返回为0 【即一个map中,只能存在一个 key=null的键值对】
// '>>>'表示无符号数右移 16位 高位补0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//hash为插入元素的哈希值 key元素的键 value元素的值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab为全局变量table桶数组的引用
//p为根据hash&mask得出的桶节点 即table[i]
//n为table数组长度
//i为 hash&mask
Node<K,V>[] tab; Node<K,V> p; int n, i;
//***如果全局table数组==null 则进行创建***
//并且将新创建的数组长度 赋值给n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//***如果根据 hash&mask 得出的桶节点=null 则直接进行插入***
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//***否则 此时出现hash冲突 开启判断过程***
//1、遍历 以p为head节点的链表中 如果有与插入元素key相同的节点 则覆盖原有value
//2、如果没有 则进行插入【此时 先进行插入 再根据原有链表的节点个数决定是否进行 链表——>红黑树操作 】
else {
//e是引用 插入节点是在e的引用上进行的
//k表示 p节点的键
Node<K,V> e; K k;
//***判断1、链表的head节点 和插入节点的 key一致***
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//e指向这个头节点
e = p;
//***否则 判断2、该链表p节点的类型是否是TreeNode,如果是,则处理冲突的结构已经从链表变为红黑树***
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//***否则 判断3、 处理冲突的结构还是一个链表 判断下该链表里 是否有与插入元素key相同的元素【从表头节点p开始遍历】***
else {
//binCount 边遍历边计数 当遍历到末尾时 也就知道链表一共有多少个节点了
for (int binCount = 0; ; ++binCount) {
//此时已经判断到链表尾部
//链表里并没有与插入元素key相等的元素 准备进行插入操作
//e指向当前节点的下一个节点
if ((e = p.next) == null) {
//生成新节点 并插入
p.next = newNode(hash, key, value, null);
//判断下此时 该链表的节点数是否>=8 如果满足 则链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//此时还没到链表尾部
//继续判断 下一个节点key与插入元素key的关系
//如果等于则 break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果不等于则继续判断 p=p.next
p = e;
}
}
//e不为null 说明链表中存在与插入元素key相同的元素
//e指向的是被替换元素 即【break循环时 当前节点的下一个元素】
//执行Value覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//执行完更新操作直接返回
return oldValue;
}
}
//该HashMap对象 内部结构发生变化的次数 +1
++modCount;
//实际键值对数量+1 与 最大键值对容量 进行比较
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容
JDK1.8做了哪些优化。
经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
看下图可以明白这句话的意思,n为table的长度
图(a)表示扩容前的key1和key2两种key确定索引位置的示例
图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
通过 hash & oldLength 即可得出 hash的高位是0 or 1。
- 若为0,则该元素在新哈希表的位置与旧哈希表一致
- 若为1,该元素在新哈希表的位置 为 旧哈希表位置+旧哈希表数组的长度【容量】
final Node<K,V>[] resize() {
//oldTab为指向当前全局变量table的引用
Node<K,V>[] oldTab = table;
//oldCap 记录当前哈希表的容量 即数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr 记录当前哈希表最多可以存储的键值对
int oldThr = threshold;
//设置新的
int newCap, newThr = 0;
//当前哈希表容量>0 进行扩容前的判断
if (oldCap > 0) {
//如果当前容量已经超过了最大容量 那么不会发生扩容
if (oldCap >= MAXIMUM_CAPACITY) {
//此时将threshold更新为最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果当前的哈希表容量*2<最大值 且 当前的哈希表容量>16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//对 threshold进行2倍扩容
//因为负载因子不变 所以表length*2 给threshold*2 即可
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//HashMap是带参构造 参数为initialCapacity
//哈希表为null
//哈希表初始容量 设置为 哈希表键值对阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//HashMap是无参构造
//说明此时哈希表为空 进行容量 和 最大存储键值对 的设置
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//进入此if有两种可能
// 第一种:进入此“if (oldCap > 0)”中且不满足该if中的两个if
// 第二种:进入这个“else if (oldThr > 0)”
//分析:进入此if证明该map在创建时用的带参构造,如果是第一种情况就说明是进行扩容且oldCap(旧容量)小于16,
//如果是第二种说明是第一次put
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指向新数组
table = newTab;
//如果“oldTab != null”说明是扩容,否则直接返回newTab
if (oldTab != null) {
//对旧哈希表数组的所有元素 重新进行散列
for (int j = 0; j < oldCap; ++j) {
//指向——>旧哈希表数组节点
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//将当前旧节点对象 设置为null 以便于垃圾回收
oldTab[j] = null;
//则e指向的以oldTab[j]为head节点的链表 只有一个元素
if (e.next == null)
//在新哈希表 对元素 进行散列
newTab[e.hash & (newCap - 1)] = e;
//如果e指向的不是尾节点 判断该处理冲突的结构是否为 红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//处理冲突的结构是否为链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;//此对象接收会放在原来位置
Node<K,V> hiHead = null, hiTail = null;//此对象接收会放在“j + oldCap”(当前位置索引+原容量的值)
Node<K,V> next;
do {
next = e.next;
//oldCap 为偶数,正好 oldCap & e.hash,可以得出e.hash的【高一位 即第n位 2^n为旧的哈希表数组长度】
//若e.hash高一位=0 则该元素在新哈希表的位置与旧哈希表一致
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;
}
获取元素
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;
}
1.8与1.7比较
-
创建哈希桶的时机不同
1.7 在调用构造函数的时候,就已经确定好了容量和阈值,并且创建好了哈希桶数组。
1.8 调用构造函数时候,根据初始化容量 得出了大于的2^n 赋值给当前的阈值。在put操作时,当哈希桶数组为空才会去创建。此时再将阈值赋值给容量,将阈值=容量x负载因子。 -
插入结点的方式不同
1.7采用头插法,插入速度快。但是在多线程扩容情况下,如果一个桶上的所有节点均在新表的一个桶,会出现循环链表问题。
头插的原理:
1、先记录当前待插入结点的下一个结点
2、将待插入结点.next 置为新桶的头节点
3、更新桶的头节点为待插入节点。
旧表
(1)
线程1、2进行扩容操作
线程1先执行,记录了 next=b , e=a ,随后CPU切换调度到线程2,线程2完成了扩容后
如下图,其中的next 和 e是线程1的引用
此时线程1继续执行,next=b ,e=a,开始继续执行,结果如下
此时e=b,next=a
此时e=a,next=null
线程1 执行 a.next = bucket[7]头节点,又指向b
此时出现了循环链表,存在线程安全问题
1.8采用尾插法,插入速度慢,但是解决了这种问题。
- 数组扩容后的索引计算方式不同
JDK1.7的时候是直接用hash & (newLength-1),判断桶索引
JDK1.8 hash & oldLength 计算出最高位是0 还是1
哈希表容量为2的n次幂原因
(1)根据hash计算索引的时候,直接是 & 操作即, hash&(length-1) ,& 比 mod更高效。
&操作必须保证 length-1为奇数,二进制全为1,保证了结果的正确性。如果length-1为偶数,则二进制最高位=1,其余为0,会出现散列全部向两端。
(2)1.8进行元素的扩容操作时,计算元素在新表的索引 是通过 hash & newLength-1 来判断hash最高位的,若为1,则该元素在新表的位置=旧表的位置+旧表长度。若为0,新老位置不变。
HashMap为什么线程不安全
put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。
为什么不直接使用红黑树
红黑树的插入、删除操作涉及到自旋平衡等一系列操作,比较麻烦。
在少数据量的情况下,还是使用链表直接插入、删除比较简单。
大数据量情况下,查询成本比较高,这时考虑使用红黑树。
6 和 8
从 treeifyBin 函数中可以看到,虽然链表个数>8触发红黑树转换,但是红黑树实际转化之前先会判断下当前的表长度,如果table.length < 64,则会进行resize扩容。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
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);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
Map的遍历
(1)map中的结点类型为Map.Entry ,全部都存储在EntrySet<Map.Entry<K, V>>中
这里直接遍历EntrySet的每个结点
for(Map.Entry<Integer,Integer> entry : map.entrySet())
{
System.out.println(entry.getKey()+"-"+entry.getValue());
}
(2)map的键key全部在 keySet()中,value全部在values()中
for(Integer key: map.keySet()) {
System.out.print(key+" ");
}
System.out.println();
for(Integer value:map.values()) {
System.out.print(value+" ");
}
(3)迭代器遍历,访问EntrySet的迭代器
Iterator it = map.entrySet().iterator();
while(it.hasNext())
{
Map.Entry entry = (Map.Entry)it.next();
System.out.println(entry.getKey()+"-"+entry.getValue());
}
(4)根据keySet()取出key,get(key)遍历
TreeMap
作为一个Key-Value,底层使用红黑树作为实现
TreeMap节点结构
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
get()时间复杂度
- 在理想状态下,未发生任何hash碰撞,数组中的每一个链表都只有一个节点,那么get方法可以通过hash直接定位到目标元素在数组中的位置,时间复杂度为O(1)。
- 若发生hash碰撞,则可能需要进行遍历寻找,n个元素的情况下,链表时间复杂度为O(n)、红黑树为O(logn)