上一篇整理里list,本文继续整理hashmap.先说一下map的基本接口
Map将key和value封装至一个叫做Entry的对象中,Map中存储的元素实际是Entry。只有在keySet()和values()方法被调用时,Map才会将keySet和values对象实例化。
所以map的遍历右三种方式:
package com.daojia.collect;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
public class HashMapTest {
private static final int a[] = {10, 40, 30, 60, 90, 70, 20, 50, 80};
public static void main(String[] args) {
// TODO Auto-generated method stub
HashMap map = new HashMap();
for(int i=0; i keys = map.keySet();
for (Integer key : keys){
System.out.println(key+" "+map.get(key)+"");
}
System.out.println("");
System.out.println("value遍历:");
Collection values =map.values();
Iterator ite=values.iterator();
while(ite.hasNext()){
System.out.print(ite.next()+" ");
}
System.out.println();
Set entryset = map.entrySet();
for(Object o:entryset){
Entry entry = (Entry)o;
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
}
Map 的实现类主要有 4 种:
Hashtable 慢,线程安全
HashMap 速度很快,但没有顺序
TreeMap 有序的,效率比 HashMap 低
LinkedHashMap 结合 HashMap 和 TreeMap 的特点,有序的同时效率也不错,内存占用大。
HashMap 是一个采用哈希表实现的键值对集合,继承自 AbstractMap,实现了 Map 接口 。
HashMap 的特殊存储结构使得在获取指定元素前需要经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素。jdk1.7及以前版本是底层实现是 链表数组,JDK 1.8 后又加了 红黑树
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
实现了 Map 全部的方法
key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法
允许空键和空值(但空键只有一个,且放在第一位,下面会介绍)
元素是无序的,而且顺序会不定时改变
插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置)
遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大)
两个关键因子:初始容量、加载因子
除了不允许 null 并且同步,Hashtable 几乎和他一样。
默认初始容量:16,必须是 2 的整数次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量: 2^ 30 次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子的大小:0.75,
static final int TREEIFY_THRESHOLD = 8;//树形阈值,大于此值桶用红黑树而不是链表
static final int UNTREEIFY_THRESHOLD = 6;//非树形阈值,小于此值时,桶存储把树转变为链表存储
static final int MIN_TREEIFY_CAPACITY = 64; //树的最小容量,为避免resize时冲突,最小 least 4 * TREEIFY_THRESHOLD
transient Node
transient Set
transient int size;//键值对的数量
transient int modCount;//当前 HashMap 修改的次数,这个变量用来保证 fail-fast 机制
final float loadFactor;//哈希表的加载因子
由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:
容量:数组的数量
加载因子:决定了 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);
}
/**
* 默认:容量大小(16) ,load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
涉及的方法: tableSizeFor(int)
来根据指定的容量设置阈值,这个方法经过若干次无符号右移、求异运算,得出最接近指定参数 cap 的 2 的 N 次方容量。假如你传入的是 5,返回的初始容量为 8 。
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
链表节点
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
树节点:
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//table为空就创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//确定插入table的位置,算法是(n - 1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//在table的i位置发生碰撞,有两种情况,1、key值是一样的,替换value值,
//2、key值不一样的有两种处理方式:2.1、存储在i位置的链表;2.2、存储在红黑树中
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.2
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//2.1
else {
for (int binCount = 0; ; ++binCount) {
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e为空就替换旧的oldValue值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//threshold=newThr:(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//默认0.75*16,大于threshold值就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果该位置桶为空,新旧节点并放进去。否则从桶中第一个元素开始查找哈希值对应位置 如果桶中第一个元素的哈希值和要添加的一样,替换,结束查找。如果第一个元素不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal() 进行插入。否则还是从传统的链表数组中查找、替换,结束查找当这个桶内链表个数大于等于 8-1,就要调用 treeifyBin() 方法进行树形化
哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1 由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或。由于 int 只有 32 位,无符号右移 16 位相当于把高位的一半移到低位:这样可以避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,可以避免哈希值分布不均匀。
2 具体的计算过程用如下图表示,因为目前的table长度2的N次方,2n-1得到的二进制数的每个位上的值都为1,而计算下标的时候,使用&位操作,得到的和原hash的低位相同,而非%求余,更高效。加之上面设计的hash(key)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。所以说当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,g。高效查询,在jdk1.8更加入了红黑树来改善大链表的查询性能。
请看putVal代码27/28行,当桶bucket大于TREEIFY_THRESHOLD(8)值时就执行treeifyBin,如果是之前java7之前的代码的话是要进行扩容的,但是java8可能会把这个bucket的链表上的数据转化为红黑树
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
//当tab.length hd = null, tl = null;
do {
TreeNode p = replacementTreeNode(e, null);//新建一个树形节点,内容和当前链表节点 e 一致
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);
}
}
final Node[] resize() {
//复制一份当前的数据
Node[] oldTab = table;
//保存旧的元素个数、阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
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)
//如果旧容量小于等于 16,新的阈值就是旧阈值的两倍
newThr = oldThr << 1; // double threshold
}
//如果旧容量为 0 ,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
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;
//创建新链表数组,容量是原来的两倍
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//接下来就得遍历复制了
if (oldTab != null) {
//将原来map中非null的元素rehash之后再放到newTab里面去
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
//旧的桶置为空
oldTab[j] = null;
//当前 桶只有一个元素,直接赋值给对应位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果旧哈希表中这个位置的桶是树形结构,就要把新哈希表里当前桶也变成树形结构
((TreeNode)e).split(this, newTab, j, oldCap);
else { //保留旧哈希表桶中链表的顺序
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
//do-while 循环赋值给新哈希表
do {
next = e.next;
// 这里的操作就是 (e.hash & oldCap) == 0 这一句,起到了判断作用:0表示新位置下标不变,如果不是0那么表示位置有变动。
因为oldCap和newCap是2的次幂,并且newCap是oldCap的两倍,就相当于oldCap的唯一一个二进制的1向高位移动了一位
(e.hash & oldCap) == 0就代表了(e.hash & (newCap - 1))还会和e.hash & (oldCap - 1)一样。
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;
}
jdk1.7
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历旧表
for (Entry e : table) {
//当桶不为空
while(null != e) {
Entry next = e.next;
//如果hashSeed变了,需要重新计算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 = next;
}
}
}
从代码上看,resize的过程1.7中是通过控制hashSeed的变化导致hash()方法得到的hash值,而JDK1.8中一旦得到了一个键的hash值后,就不会再改变了,而是通过hash&cap==0为区分,将链表分散,而1.7是通过更新hashSeed将旧表中的链表分散,可以看出jdk1.8设计的优化点:省却了重新计算哈希值的过程。 所以区别:
1 当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表了尾部,而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法。 addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾 addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾 。
2 1.8rehash时保证原链表的顺序,而1.7中rehash时将改变链表的顺序
看了resize的过程就知道尽管jdk1.8有所优化,还是很耗性能,尽量避免,一开始预估好容量。
public V get(Object key) {
Node e;
//还是先计算 哈希值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
//tab 指向哈希表,n 为哈希表的长度,first 为 (n - 1) & hash 位置处的桶中的头一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶里第一个元素就相等,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则就得慢慢遍历找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果是树形节点,就调用树形节点的 get 方法
return ((TreeNode)first).getTreeNode(hash, key);
do {
//do-while 遍历链表的所有节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
查找就是先计算hash值,(n-1)&hash判断桶位置,接着判断是否第一个节点,不是就在桶里遍历查找(可能是树节点)。
我们在上面的put的方法里面,treeifyBin就是桶的树形化。
下面看看把一个桶中的链表结构变成红黑树结构
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) //当比较节点的哈希值比 x 大时
dir = -1;
else if (ph < h) //当比较节点的哈希值比 x 小时
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) //如果hash值相等,则比较k值,用其Compare,如果还相等,则走tieBreakOrder
dir = tieBreakOrder(k, pk);
TreeNode xp = p; //把 当前节点变成 x 的父亲
if ((p = (dir <= 0) ? p.left : p.right) == null) {// 根据dir的值选取左右子结点,子结点不为空,继续循环寻找
x.parent = xp; //x节点父节点指向当前节点
if (dir <= 0) //如果当前比较节点的哈希值比 x 大,x 就是左孩子,
xp.left = x;
else //否则x就是右孩子
xp.right = x;
root = balanceInsertion(root, x); // 插入后,平衡红黑树,使之满足红黑树性质
break;
}
}
}
}
moveRootToFront(tab, root);//将root移到桶中的第一个
}
balanceInsertion就是红黑树的修正。参见之前的。
如果:插入的如果一个桶中已经是红黑树结构,就要调用红黑树的添加元素方法 putTreeVal()。
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
Class> kc = null;
boolean searched = false;
TreeNode root = (parent != null) ? root() : this;
//每次添加元素时,从根节点遍历,对比哈希值
for (TreeNode p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//如果当前节点的哈希值、键和要添加的都一致,就返回当前节点
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//如果当前节点和要添加的节点哈希值相等,但是两个节点的键不是一个类,只好去挨个对比左右孩子
if (!searched) {
TreeNode q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
//如果从 ch 所在子树中可以找到要添加的节点,就直接返回
return q;
}
//哈希值相等,但键无法比较,只好通过特殊的方法给个结果
dir = tieBreakOrder(k, pk);
}
//经过前面的计算,得到了当前节点和要插入节点的一个大小关系
//要插入的节点比当前节点小就插到左子树,大就插到右子树
TreeNode xp = p;
//这里有个判断,如果当前节点还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next;
TreeNode x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode)xpn).prev = x;
//红黑树中,插入元素后必要的平衡调整操作
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
跟上面的桶转换红黑树类似,少了根节点的处理,没有双层循环。
上面的get方法里面:如果找到桶之后,头节点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。
final TreeNode getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode find(int h, Object k, Class> kc) {
TreeNode p = this;
do {
int ph, dir; K pk;
TreeNode pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
这里find如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回,不相等从子树查询。后面的判断条件有些多,反复的指向左右子树,不是特别理解。
其他:
HashMap 允许 key, value 为 null,同时他们都保存在第一个桶中。因为计算hash的时候判断是null返回0
参考:
http://www.cnblogs.com/huaizuo/p/5371099.html
https://blog.csdn.net/u011240877/article/details/53351188
其他:
HashMap 允许 key, value 为 null,同时他们都保存在第一个桶中。因为计算hash的时候判断是null返回0