HashMap/HashTable/HashSet/LinkedHashSet源码上区别概述:
HashMap/HashTable
HashMap、 Hashtable都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。主要区别在于Hashtable在方法层面加上了synchronized
。
HashSet/LinkedHashSet
而HashSet的底层是基于HashMap,所以HashSet的数据结构就是HashMap的数据结构,只是HashSet的每个key对应的value值都为HashSet的成员变量private static final Object PRESENT = new Object();
。
不过HashMap和HashSet实现的接口规范不同。
LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。
HashMap特点
HashMap是一种非常常见、方便和有用的集合,是一种键值对(K-V)形式的存储结构。
HashMap键和值都允许为空,为线程不安全的,无序且Key值重复会覆盖。
JDK1.7之前数据结构图为:
JDK1.8对HashMap优化后的数据结构图为:
如果链表的长度大于阈值(TREEIFY_THRESHOLD = 8) 其结构改变成红黑树 ,如果红黑树节点小于某个阈值(UNTREEIFY_THRESHOLD = 6)就变成链表。
HashMap结构
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
-
Map:用来存储键值对的,它是一个双列数据结构
-
AbstractMap:是个抽象类,抽象类一般用来提取一些共有的属性,AbstractMap包含了HashMap,TreeMap,ConcurrentHashMap等Map集合类共同的属性。AbstractMap有containsValue(),containsKey(),get(),put(),remove(),putAll()等方法。
-
Cloneable:ArrayList 实现了Cloneable接口,并覆盖了函数clone(),能被克隆。
-
Serializable:实现java.io.Serializable 接口后ArrayList支持序列化,能通过序列化去传输。
HashMap属性
//默认初始化容量 <<是算术左移,移几位就是*2的几次方
//必须是2的倍数,下面会有讲解
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大所能容纳的key-value 个数
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;
//最小TreeNode的容量为64
static final int MIN_TREEIFY_CAPACITY = 64;
//存储数据的Node数组,长度是2的幂。
transient Node<K,V>[] table;
//keyset 方法要返回的结果
transient Set<Map.Entry<K,V>> entrySet;
//map中保存的键值对的数量,注意这个不等于数组的长度
transient int size;
//每次扩容和更改map结构的计数器
transient int modCount;
//英文是 阈值,门槛的意思。临界值数值等于容量*加载因子;当实际大小size超过该值时,会进行扩容。比如默认临界值为12,当HashMap实际大小超过12时,HashMap会进行扩容操作。
int threshold;
//加载因子,初始值=0.75,与扩容有关
final float loadFactor;
HashMap默认初始化容量为16;默认加载因子为0.75;如果链表的长度大于阈值(TREEIFY_THRESHOLD = 8) 其结构改变成红黑树 ;如果红黑树节点小于某个阈值(UNTREEIFY_THRESHOLD = 6)就变成链表。
HashMap构造方法
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);
}
public HashMap(int initialCapacity) {//使用默认容量,指定加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {//使用默认初始化容量与加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Returns a power of two size for the given target capacity.
* 百度翻译:(返回给定目标容量的二次幂。)
* 也就是获取比传入参数大的最小的2的N次幂。
* 比如:传入8,就返回8,传入9,就返回16.
*/
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;
}
HashMap方法分析
类源码方法层面的分析最好的方法是使用Debug一步步走一遍该方法。
put(K key, V value)方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
------------------------
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
------------------------
/**
* 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) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
//判断tab是否为空
if ((tab = table) == null || (n = tab.length) == 0)
//为空,构建tab
n = (tab = resize()).length;
//(n - 1) & hash 按位与运算得到tab的索引值,判断该索引值处是否有元素
if ((p = tab[i = (n - 1) & hash]) == null)
//该索引值处无元素,构建Node节点放入桶中(桶中的第一个元素,位于数组中),存储key-value
tab[i] = newNode(hash, key, value, null);
else {
//该索引值处有元素(对应桶中的第一个元素,位于tab数组中)
HashMap.Node<K,V> e; K k;
//比较桶中的第一个元素(数组中)与要存储的元素的hash值和key值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//相等(key值相同),将桶中的第一个元素赋值给e
e = p;
//hash值不相等,存储在链表或是红黑树中
else if (p instanceof HashMap.TreeNode)
//存储在红黑树中(此时p所在的桶中的那条链表(Node节点)已经转换成了红黑树(TreeNode节点)了)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//存储在链表中
for (int binCount = 0; ; ++binCount) {
//判断桶中的第一个元素是否有next节点
if ((e = p.next) == null) {
//为null,没有next节点,构建Node,赋值给p.next(p关联起来形成链表结构)
p.next = newNode(hash, key, value, null);
//链表中的节点数量大于阈值,将那条链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//跳出循环
break;
}
//判断链表中的节点的key值与要存储的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//相等,跳出循环(此时e == p.next)
break;
//用于遍历链表中的节点,和e = p.next结合,可以遍历链表中的所有节点
p = e;
}
}
if (e != null) { // existing mapping for key
//要存储的key找到了对应的mapping,替换value并返回oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//onlyIfAbsent为false或者旧值为null,替换
e.value = value;
//访问后回调
afterNodeAccess(e);
//返回oldValue
return oldValue;
}
}
//结构性修改加一
++modCount;
//map中实际元素的大小大于阈值则进行扩容
if (++size > threshold)
resize();
//插入后回调
afterNodeInsertion(evict);
//no existing mapping for key,返回null值
return null;
}
方法执行逻辑:
-
1.首先进入put方法,会通过hash(key)方法(高16位和低16位进行异或运算来使哈希值更加随机和散列来减少碰撞)来获取一个散列值
-
2.然后判断数组是否为空,和数组长度是否为零,是就进行reSize()方法初始化(还可以扩容)。
-
3.进入reSize()中,判断数组容量是否大于0,是就进行初始化,数组容量初始化为DEFAULT_INITIAL_CAPACITY=16,阈值为16*0.75(加载因子);
-
4.接下来数组长度-1和hash值进行&运算来计算出数组下标
该位置为空:新建节点,直接存入该下标位置
该位置不为空,出现冲突:a.遍历链表,发现key值相同,则将对应的value更新 ; b.key值不同,放到链表尾部。链表超过阈值8则变为红黑树 -
5、将当前的key-value 数量标识size自增,然后和threshold对比,如果大于threshold的值,则调用resize()方法,扩大当前HashMap对象的存储容量。
-
6、返回oldValue或者null。
另外HashMap无序的体现在于:
HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的.
顺序放入HashMap中键值对,然后取出键值对,代码如下:
import cn.hutool.core.util.RandomUtil;
import java.util.HashMap;
import java.util.Map;
public class TestMain {
public static void main(String[] args) {
Map map = new HashMap();
for (int i = 0; i < 10; i++) {
map.put(RandomUtil.getRandom().nextInt(), "zhangsan" +i);
}
System.out.println(map);
}
}
执行结果:
{141276649=zhangsan6, -510021043=zhangsan2, 1985812697=zhangsan8, 937630534=zhangsan0, 807705777=zhangsan4, -409489824=zhangsan1, -370251303=zhangsan5, -1405551616=zhangsan9, -859502487=zhangsan7, 1740205364=zhangsan3}
通过结果可以看到,HashMap获取元素的顺序与put的顺序并不一致。这就是HashMap无序的表现的一个实例。
get(Object key)方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
-----------------------------
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
//判断table是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//table不为空并将桶中的第一个元素(位于数组中)赋值给first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//根据hash值和key判断是桶中的第一个元素(位于table数组中)
return first;
//不是桶中的第一个元素
if ((e = first.next) != null) {
if (first instanceof HashMap.TreeNode)
//要找的Node节点位于红黑树中
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
//位于链表中,将e.next赋值给e,循环获取和key匹配的Node节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//table为空,返回null
return null;
}
说明:hashMap中并没有直接提供getNode接口给用户使用,而是通过get函数,再通过get函数调用getNode来获取元素的.
resize()方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //未扩容时数组的容量
int oldThr = threshold;
int newCap, newThr = 0;//定义新的容量和临界值
//当前Map容量大于零,非第一次put值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //超过最大容量:2^30
//临界值等于Integer类型的最大值 0x7fffffff=2^31-1
threshold = Integer.MAX_VALUE;
return oldTab;
}
//当前容量在默认值和最大值的一半之间
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; //新临界值为当前临界值的两倍
}
//当前容量为0,但是当前临界值不为0,让新的容量等于当前临界值
else if (oldThr > 0)
newCap = oldThr;
//当前容量和临界值都为0,让新的容量为默认值,临界值=初始容量*默认加载因子
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的临界值为0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
//临界值赋值
threshold = newThr;
//扩容table
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;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//此时newCap = oldCap*2
else if (e instanceof TreeNode) //节点为红黑树,进行切割操作
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //链表的下一个节点还有值,但节点位置又没有超过8
//lo就是扩容后仍然在原地的元素链表
//hi就是扩容后下标为 原位置+原容量 的元素链表,从而不需要重新计算hash。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//循环链表直到链表末再无节点
do {
next = e.next;
//e.hash&oldCap == 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);
//循环链表结束,通过判断loTail是否为空来拷贝整个链表到扩容后table
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
方法实现思路:
- 1.如果当前数组为空,则初始化当前数组
- 2.如果当前table数组不为空,则将当前的table数组扩大两倍,同时将阈值(threshold)扩大两倍。数组长度和阈值扩大成两倍之后,将之前table数组中的值全部放到新的table中去。
HashMap什么时候进行扩容与怎么规避扩容这个性能消耗大的操作?
当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,hashmap也自动会将其设置为1024。
但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75* size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
JDK7中的HashMap闭环和丢失问题
闭环产生的原因:1.7的HashMap在扩容复制时,采用的是头插入法,这会导致原数组中的链表反转,即将原来的正向,复制成反向链表。
而在多线程环境下,可能存在其他线程完成了扩容复制操作,完成了后的数组链表变成了原来的反向链表。
由于可见性的保证,当前线程继续复制时,复制的时其他线程已经完成了的反向链表,但当前线程又会将其反转,导致又变成了正向链表,那么在当前线程复制过程中会既有正向的链表又有反向的链表,从而可能产生闭环的情况。
丢失和闭环均是由于改变链表方向导致的,设计者一开始使用头插入法是考虑碰撞时后加入的数据访问的可能性更大,头插入有更好的查找性能。
1.8改为尾插入,不会进行链表方向的改变,因此不会发生丢失和闭环.
网友评论