集合框架 — HashMap
关于Map、Set和List的关系,有个说法很形象
- 把 Map里所有的 key 放一起就组成了一个 Set 集合(无序、不重复),
keySet()
- 把 Map 里所有的 value 放一起就组成了一个 List (可以重复,通过索引查找),
values()
Map常用方法:
clear()
:删除Map对象所有的 key-value 对
containsKey(Object key)
:查询 Map 中是否有指定的 key
get(Object key)
:返回指定 key 对应的 value(如果没有该 key,返回 null,注意也有可能值为 null)
put(Object key, Object value)
:添加一个 key-value 对,存在则覆盖
pubAll(Map m)
:将指定 Map 中的 key-value 对复制到本 Map中
remove(Object key)
:删除指定 key 对应的 key-value 对
entrySet()
:返回由 Map 中所有Map.Entry
组成的 Set集合
keySet()
:返回该 Map 中所有 key 组成的 Set 集合
values()
:返回该 Map 中所有 value 组成的 Conlection
size()
:返回 Map 里 key-value 的个数
一、HashMap的使用
![](https://img-blog.csdnimg.cn/66e01ddb8ba04ec68349400febf5fcdc.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA4oaS6ZW_5q2M,size_20,color_FFFFFF,t_70,g_se,x_16)
底层由数组
+链表
组成,数组里存放key-Value
这样的实例(也就是Node,jdk1.7叫Entry),初始所有的位置都为null
,赋值后每个节点中如上图所示,都会保存自身的hash
、key
、value
和下一个节点next
,源码如下
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap类的几个重要字段:
transient int size; // 数组key-value的个数
transient int modCount; // 记录内部结构变化次数,用于迭代的快速失败
int threshold; // 容纳最大键值对数量
final float loadFactor; // 负载因子
HashMap初始值:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始容量
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量(1<<30)
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子
static final int TREEIFY_THRESHOLD = 8; //树化阈值
static final int UNTREEIFY_THRESHOLD = 6; //反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64; //最小树容量(小于该值则扩容,而不是树化)
添加数据的时候,根据key的hash
和数组长度length
通过高位运算和取模运算
得出 index 值,也就是均匀的放在数组的哪个位置,在该数组位置存储数据,jdk1.7采用头插法,现在采用尾插法
放在链表最后(jdk1.8时,链表长度超过8,链表转红黑树存储)
如果数组键值对数量超出threshold
,数组进行2倍扩容,最大容量为1<<30
,达到了就不再扩容
二、HashMap注意点
1、jdk1.8采用尾插法的原因
防止多线程情况下,扩容时因为改变节点引用关系造成环形链表(死循环)
-
因为头插法中,
next
指向下一个节点,当扩容重新计算索引位置时,可能会出现next相互指向 -
使用尾插法时,扩容会保持链表原本的顺序,不会出现链表成环问题
2、jdk1.8转红黑树
红黑树具有比链表更高的效率,查找时间复杂度为O(logn),链表为O(n)
- 当
key-value
个数大于64(小于则进行扩容),且链表长度大于8时,由链表转为红黑树,红黑树key-vale
小于6时,重新转为链表
3、解决哈希冲突,初始值是16,扩容也为2倍
使数组length为2的幂,可以直接用位运算,效率更高,分布均匀
-
解决哈希冲突,可以把hash值对数组长度取模运算,但是模运算的消耗还是比较大
-
在HashMap中,当数组
length
总是2的n次方时,h&(length-1)
运算等价于对length
取模(length-1)的值二进制都是1,&运算就等于HashCode后几位的值,实现了均匀分布),也就是在数组的位置,但是&
比%
具有更高的效率 -
高16位异或低16位
可以在数组的length比较小的时候,也能保证高位与低位的特征,减少冲突的概率,同时不会有太大的开销。
static final int hash(Object key) {
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位低位异或运算
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
static int indexFor(int h, int length) {
//第三步 取模运算
return h & (length-1);
}
4、new HashMap时,为什么可以赋值不是2的幂
不管赋值是多少,该算法会使用比参数大,且最小的2的幂
static final int tableSizeFor(int cap) {
// cap-1防止已经是2的幂
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;
}
网上示意图:
![](https://img-blog.csdnimg.cn/fbd6d98cbc654f85a3e9b93a7eafca43.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA4oaS6ZW_5q2M,size_20,color_FFFFFF,t_70,g_se,x_16)
5、扩容时,不是直接复制过去,而是重新Hash
Hash的算法:
static int indexFor(int h, int length) {
return hashCode(key) & (length-1);
}
原先位运算出来的值,当扩容后,再次用位运算,位置就不一样了
6、安全问题(modCount字段)
多线程中不安全,方法都没有加同步锁,多线程使用ConcurrentHashMap