HashMap中的负载因子和容量
实际容量 = 负载因子 x 容量,也就是 12 = 0.75 x 16
//默认初始容量-必须为2的幂次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,如果传入的值大于下面的值,则使用下面定义的值
static final int MAXIMUM_CAPACITY = 1 << 30;
//在构造函数中未指定时使用的负载系数
//默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的长度 >= 8的时候,将链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
//在resize()扩容的时候,HashMap的数据存储位置会重新进行计算
//在重新就散存储的位置后,当原有的红黑树数量 <= 6 的时候,则将红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
//为了避免扩容/调整树化阀值之间的冲突,这个值不能 < 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
构造函数
1.HashMap(int initialCapacity, float loadFactor)
2.HashMap(int initialCapacity)
3.HashMap()
4.HashMap(Map<? extends K, ? extends V> m)
//初始容量为32,负载因子0.75,容量必须为2的幂次方,调用tableSizeFor()实现
Map<String, String> map1 = new HashMap<String, String>(32,(float) 0.75);
//初始容量为18
Map<String, String> map2 = new HashMap<String, String>(18);
//默认容量为16
Map<String, String> map3 = new HashMap<String, String>();
tableSizeFor源码:使得容量初始化为2的幂次方
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;
}
resize()源码:
Hash冲突
- 两个节点的key值相同,导致冲突
- 两个节点的key值不同,由于hash函数的局限性导致hash值相同,导致冲突
- 两个节点的key值不同,hash值不同,但hash值对数组长度取模后相同,导致冲突
解决方案:链地址法
HashMap 中数组的每一个元素不仅是一个 Entry 对象,还是一个链表的头节点。每一个 Entry 对象通过 next 指针指向它的下一个Entry 节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可
HashMap常用方法
get():
//调用getNode()方法,根据key获取hash值,有值就返回value,没有值返回null
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
put():
//调用putVal()方法,根据key获取hash值,找到该hash值对应的数组下标,插入到链表或红黑树中
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
jdk1.7与jdk1.8对比
jdk1.7 | jdk1.8 | |
---|---|---|
插入方式 | 头插法 | 尾插法 |
存储结构 | 数组+链表 | 数组+链表+红黑树 |
扩容后数据存储位置 | hash值和需要扩容的二进制数进行& | 扩容前的原始位置+扩容的大小值 |
resize时间复杂度 | n | nlog(n) |
线程安全问题
-
put()操作的时候导致的多线程数据不一致
两个线程同时进行put()操作,导致其中一个值被覆盖 -
resize()而引起死循环
这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表 -
如何实现线程安全
HashMap<String,Integer> hashMap = new HashMap<String,Integer>(); Map<String, Integer> map = Collections.synchronizedMap(hashMap);
性能
Entry的key最坏的情况下在Map中是一个链表,JDK8为优化这个问题在链表数目大于8的时候转化为红黑树,但是resize中,又必需拆解和重建红黑树
遍历方式
1.通过Iterator进行删除操作是线程安全的
2.通过map.keySet().removeIf(key -> key == 1)是线程安全的
Iterator+EntrySet()
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
System.out.print(entry.getKey());
System.out.print(entry.getValue());
}
Iterator+KeySet
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
Integer key = iterator.next();
System.out.print(key);
System.out.print(map.get(key));
}
ForEach+EntrySet
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.print(entry.getKey());
System.out.print(entry.getValue());
}
ForEach+KeySet
for (Integer key : map.keySet()) {
System.out.print(key);
System.out.print(map.get(key));
}
ForEach+value
for (String value : map.value()) {
System.out.print(value);
}
Lambda
map.forEach((key, value) -> {
System.out.print(key);
System.out.print(value);
});
Streams API 单线程
map.entrySet().stream().forEach((entry) -> {
System.out.print(entry.getKey());
System.out.print(entry.getValue());
});
Streams API 多线程
map.entrySet().parallelStream().forEach((entry) -> {
System.out.print(entry.getKey());
System.out.print(entry.getValue());
});