Map
java中的Map是一种可以存放键值对的数据集合,Map中的Key是不可重复的,同时一个Key只能对应一个 Value.
Map是用来替换Java中的Dictionary,
Map可以提供三个视图:
1. 将所有的Key返回为一个Set keySet()
1. 将所有的Value返回为一个Set valueSet()
1. 或者将Key-value返回为一个Set
像TreeMap这一类,可以保证元素的存放和获取顺序,但是HashMap并不能保证。
1. HashMap
HashMap 是Map的实现类,同时HashMap 允许使用null作为key,或者value。 HashMap大致上跟HashTable是相同的(Hashtable是同步的,并且不允许null)
HashMap是基于哈希表结构,其在没有hash冲突的情况下,进行添加,删除,查找等操作性能是很高的,只需要对指定位置进行一次从操作即可,其时间复杂度为 O(1),
在HashMap中,其主要的数据存储方式就是数组。 我们通过Hash算法,将当前元(Entry)的关键字通过某一个函数直接映射到数组中的某个位置,通过数组下标一次定位就可以完成操作。
在HashMap中,我们将上面题导的映射函数称之为 哈希函数,哈希函数的设计,决定了Hash冲突的次数,也就决定了当前HashMap的性能。
HashMap的基本操作例如 get,put 所需要的时间是固定的,HashMap的Iterator方法跟当前HashMap的容量成正比。 因此如果你想保证迭代器的性能,那么就不能将HashMap的初始容量设置的太大。
影响HashMap的关键因素:
1. **initial capacity**
1. **loadFactor** (初始值和**loadfactory**共同决定了当前**hashMap**的扩容次数)
1. **key**的**hash**算法 (如果**Key**的**hash**值重复较多,那么也可以直接降低当前**hashmap**的性能)
1.1 HashMap基本原理
假设我们需要存入两个
A:
B:
固定哈希算法为 函数f(x), indexA = f(Chen), indexB = f(Wang)
这样我们得到了A,B两个元素的数组角标,这样就把相应的Entry放入对应数组位置就可以,用图表示可以为:
1.2hash冲突
上面说到的hash函数,仅仅是指 将元素的Key转换成 index的算法,有时候我们并不能保证我们使用的hash算法能够保证 不同的键值对元素对应不同的 数组index,这样就有可能出现 hash(Chen) == hash(Wang)的情况,这就是我们说的hash冲突。
通常情况下解决hash冲突的方法有很多种,例如:开放定址算法(发生冲突,继续寻找下一块未被使用的地址),再散列算法,链地址法,在HashMap中,设计者使用了链地址法,也就是对于冲突的元素,使用链表进行存储
2 HashMap的实现
对于HashMap 如何存储键值对数据的呢?
HashMap在内存中是基于数组形式实现的:
transient Node[] table; // 内部使用一个数组存储键值对元素
键值对元素的存储格式, 使用Node对键值对进行包装:
static class Node implements Map.Entry {
final int hash; // Node包含当前键值对的hash值
final K key; //key值
V value; //value值
Node next; //下一个节点的Node, 当出现hash冲突时使用
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
可以推出,一个HashMap的基本形式如下:
当链表数量超过8:
对于 index0、index3、index7出现了hash碰撞,所以,这个节点存储的node就形成了一个单链表的形式。
如果通过hash算法定位到的数组位置 没有链表,那么 删除,替换,添加等操作的时间复杂度都是 O(1)
如果定位到的数组位置有hash冲突,那么这些操作的时间复杂度就为 O(n), n = 链表长度
3 HashMap源码分析
下面我们就从HashMap的一些基本操作代码入手,来探究下 HashMap的实现原理。
3.1 构造方法
HashMap的两个关键构造因子:
initial compacity 初始化容量, 这个参数 决定了当前HashMap可以拥有多少个key-value 实体
loadFactor: 这个值 决定了当前HashMap的 装填程度, 如果当前 容量超过 capacity loadFactor,那么就表示当前HashMap需要进行一次重新扩容,同时需要重新hash*。
因此,如果想要保证当前HashMap的性能, 适当的Map大小以及加载因子是关键。
另一个影响HashMap性能的关键就是 Key的hash值,如果有大量Key的hash值是重复的,那么当前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; // hashMap的加载因子,简单的说就是 hashMap可以进行扩容时的容量占比
this.threshold = tableSizeFor(initialCapacity); //对于给定的容量,hashTable都转换为 相应的2^n.
}
3.2 HashMap.put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
关于 hash(key)算法解释参见:
HashMap的hash() - Black_Knight - 博客园 (cnblogs.com)
HashMap中的hash函数 - 淡腾的枫 - 博客园 (cnblogs.com)
可以看到 HashMap.hash确实在兼容性能的基础上做到了尽量减少hash碰撞。
3.2.1 putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 参见 3.2.2 创建一个长度为16的数组
if ((p = tab[i = (n - 1) & hash]) == null) //如果 通过hash值找到的位置没有存放,那么直接创建新的node,并将值放入。
tab[i] = newNode(hash, key, value, null);
else { //以下就是处理hash冲突的步骤了。
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果指定hash位置已经存放了Node,并且key的值 相等,那么就直接进行替换
else if (p instanceof TreeNode) //如果指定结点已经变成了 树,说明这里冲突太多,执行树图的存放操作,数的操作参见 # 4.1.1
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { //这里开始遍历链表形式。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //链表接入新的结点之后,长度超出阈值,那么就需要将此链表变成树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //编辑寻找当前Key的Node,找到就跳出。
break;
p = e;
}
}
if (e != null) { // existing mapping for key 只有当链表中有一个已经存在相同Key的node时,走这里,
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //这里暂时是空实现
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize(); //检查当前数组的长度,看是否需要进行扩容
afterNodeInsertion(evict);
return null;
}
3.2.2 数组的初始化方法:
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //第一次调用的话, table为null,
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //第一次初始化,默认的容量就是16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //第一次初始化,扩容阈值就是 16*0.75
}
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[] newTab = (Node[])new Node[newCap]; //第一次初始化(不设置容量的情况下),这里就创建一个长度为16的数组
table = newTab;
if (oldTab != null) { //第一次,这里不会走
。。。
}
return newTab;
}
3.3.3 链表长度太长,链表将会变成树
static final int TREEIFY_THRESHOLD = 8; // 默认链表最长的长度为8
判断是否满足将当前链表变成树的条件:
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 如果数组为空,或者当前数组长度小于 默认长度64,那么就直接进行扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) { //指定 hash位置的结点存在
TreeNode hd = null, tl = null;
do {
TreeNode 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);//这里的操作就是关键一部了, 将链表变成标准的树结构
}
}
将链表变成树结构:
有关红黑树的介绍:
【老实李】JDK1.8中HashMap的红黑树 - (jianshu.com) //这个只是说明白了一小部分
解读HashMap中的红黑树操作 - 知乎 (zhihu.com) // 这个讲的比较深入。
final void treeify(Node[] tab) {
TreeNode root = null;
for (TreeNode x = this, next; x != null; x = next) { //开始遍历并且格式化之前创建的 树结构
next = (TreeNode)x.next;
x.left = x.right = null; //首先将当前树的左右二叉树置为空
if (root == null) { //第一次进行的时候,这里就将第一个作为当前树的跟。
x.parent = null;
x.red = false; //红黑树根节点必须是黑的
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class> kc = null;
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
以上,分析了HashMap的插入方法,
- 第一次存放数据的时候,首先创建一个数组,(默认数组长度为16, 默认加载因子为0.75)
- HashMap通过特殊的hash算法尽可能的减少Hash碰撞。 // keyhash值得前16位和16位异或,然后取与当前容量,就是当前节点得index值
- 如果出现hash碰撞,那么就将相同 index位置变成一条链表
- 如果链表长度较长(>=8),并且当前hashMap得容量超过 64,那么就需要将当前链表变成一个红黑树结构,同时又由于红黑树得自平衡性,可以保证查找删除等操作得时间复杂度在 O(logn)
3.3 HashMap.remove()
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) { //tab不为空并且数组长度>0,
Node 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)p).getTreeNode(hash, key);//找到树结构得节点
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;//找到链表结构的节点
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode)node).removeTreeNode(this, tab, movable); //树节点需要特殊处理
else if (node == p)
tab[index] = node.next;//如果是链表的第一个,那么就直接移除
else
p.next = node.next;//如果是链表中间的一个,那么就删除中间的
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结
HashMap在进行数据存储的时候使用了尽可能减少碰撞的hash算法,同时 使用了 数组、链表、红黑树的数据结构,尽可能的将性能和空间进行平衡,这也体现了源码工程师的智慧