HashMap概述
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
1.7版本HashMap介绍
HashMap的几个重要参数如下:
static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* HashMap的底层数组,其中放的都是一个个的Entry,数组大小必须是2的次方。
*/
transient Entry<K,V>[] table;
/**
* HashMap中的映射数量。
*/
transient int size;
/**
* 扩容临界值
*/
int threshold;
/**
* 自己的负载因子
*/
final float loadFactor;
/**
* 快速失败机制的实现,每次修改记一次数。
transient int modCount;
/**
* 默认临界值
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)//将参数设为不小于初始值的2的次方。
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//设置临界值
table = new Entry[capacity];//初始化底层数组
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
/**
*调用上一个构造方法,传负载因子为默认值
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 调用第一个构造方法,设值全部为默认值
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 传入一个Map,利用这个来构造HashMap
*/
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
HashMap查找
HashMap通过key值来获取value的整体过程是这样的。首先判断key值是否为空,如果为空的话则在table[0]的位置取查找value值(HashMap默认将key为null的Entry放置在table[0]的位置上),若有值则返回否则返回null。若key值不为null,则根据key查找Entry,首先将key值进行hash,然后通过indexFor函数来获取key具体在哪个table[i]中,最后遍历整个table[i]下的整条链表,找到则说明存在,否则返回null。
public V get(Object key)
{
if (key == null) //判断key是否为空,为空则调用getfornullkey
return getForNullKey();
Entry<K,V> entry = getEntry(key);
//key非空,获得Entry
return null == entry ? null : entry.getValue();
} /** * 从table[0]中来获取value,有则返回value,否则返回null */
private V getForNullKey() {
for (Entry<K,V> e = table[0];
e != null; e = e.next)
{ if (e.key == null) return e.value; } return null; }
/** *查找非空Entry */
final Entry<K,V> getEntry(Object key) { int hash = (key == null) ? 0 : hash(key);
//计算hash值 for (Entry<K,V> e = table[indexFor(hash, table.length)];
//根据hash值获取Entry所在的桶中
e != null; e = e.next) { Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 若hash相等并且key完全一致说明找到了 return e;
} return null;
//没有找到则返回null。
}
HashMap存放数据
HashMap的put方法则稍微复杂一点。首先判断key是否为空,若为空则调用putForNull方法,否则计算hash值,计算bucketIndex位置,然后在bucketIndex位置上进行逐个判断,若此位置上的单链表删存在key值,则修改value,并返回之。否则遍历完成之后就说明此Entry不存在,需要添加Entry。然后调用addEntry方法,此方法会判断是否超过临界值是否需要自动扩容。然后调用createEntry方法,在bucketIndex头部插入一个全新的Entry。
/**
*
*/
public V put(K key, V value) {
if (key == null) //若key为null,则调用putForNullKey方法。
return putForNullKey(value);
int hash = hash(key); //根据key值计算hash值
int i = indexFor(hash, table.length); //根据hash和表长度确定插入位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果在table[i]找到key ,说明是修改value值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//调用一下空方法。
return oldValue;
}
}
//如果上面在table[i]中没有找到一致的key值,说明该key值不存在,
modCount++;
addEntry(hash, key, value, i); //在table[i]的位置上放置一个新的Entry
return null;
}
/**
* 提供null的key的插入
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {//先看看table[0]位置上是否存在相同key
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//不存在则添加一个Entry。(特值添加)。
return null;
}
/**
* 根据唯一的key值在table[i]的位置上放置Entry(带判断的createEntry)
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//判断是否映射个数超过临界值
resize(2 * table.length); //扩容,乘以原来的两倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//若扩容,bucketIndex(table[i])会发生变化,重新计算之
}
createEntry(hash, key, value, bucketIndex);//在bucketIndex上添加一个Entry
}
/**
* 根据唯一的key值在table[i]的位置上放置Entrvoid createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//获取table[i]位置的头部 table[bucketIndex] = new Entry<>(hash, key, value, e);
//构造新的Entry连上去,可以看出,此处为头部插入。
size++; //映射数目 加一。
}
/** 扩容算法的实现。把HashMap重新hash到一个新的数组中去。如果当前容量已经是最大容量了,把临界值置为最大临界值即可
* */
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY)
{
//特值判断,如果已经最大了,那么把临界值置为Integer.Max_VALUE即可
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new
Entry[newCapacity];
boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted());
boolean rehash = oldAltHashing useAltHashing;
transfer(newTable, rehash);//重新hash到新表上
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
/**
* 把所有的Entry放到新表上去。其实就相当对每一个table[i]里的那条单链表进行遍历,然后给他放在新的table里去。
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//获取table上的每一个单链表的Entry (其实就是个头结点)
while(null != e) {
Entry<K,V> next = e.next; //从头结点往下遍历
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//next保存旧链表的next信息,e.next用于保存新链表的next信息。头插
newTable[i] = e;
e = next;//多线程下会出现并发问题。
}
}
}
/** 当使用Map作为参数进行构造时使用此方法*/
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());//遍历map然后把他们逐个放入HashMap中去
}
*根据key值删除一个Entry,调用removeEntryForKey,返回value值。
*/
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
/**
* 根据key来删除Entry
*/
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);//计算hash
int i = indexFor(hash, table.length);//计算bucketIndex位置
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
//设置两个指针,一个指向当前,一个指向下一个。
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {//找到了要删除的Entry
modCount++;
size--;
if (prev == e)
table[i] = next;//如果发现是头结点,那么table[i]直接置为next
else
prev.next = next;//不是头结点的话,那么就这条语句就相当于删除e
e.recordRemoval(this);//调用一个空方法。
return e;
}
prev = e;
e = next;//向下移动指针
}
return e;
}
HashMap中的迭代器。
使用expectedModCount来记录一次迭代过程中是否有添加、修改、删除Entry的操作,一旦出现这样的操作之后,expectedModCount和原来的modCount的值不相等,这时会造成fast-fail,抛出ConcurrentModificationException异常。
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // 返回下一个Entry
int expectedModCount; // 对比修改次数,用于快速失败
int index; // bucketIndex
Entry<K,V> current; // 当前Entry
HashIterator() {//在一次迭代过程中,expectedModCount就是初始变化次数。
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();//所有迭代操作都要看看是否发生修改
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {//自己的迭代器里删除
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
JDK 1.8 HashMap的实现
HashMap 1.8 中的具体实现,1.8相较于1.7而言,多了一个红黑树的概念,它将原来映射在同一个桶中的单链表数据结构根据实际情况分成了单链表和红黑树两种结构保存。
HashMap参数
/**
* 当某一个table[i]中的节点数量大于等于8时,原来的链式结构会自动转化为树形结构。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当某一个table[i]的节点数量小于等于6时,树形结构会转化链表结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap获取数据
HashMap中的get方法。和1.7不同的地方在于判断了一次节点是否为树节点,如果是树节点则调用不同的方法。否则的话都是遍历单链表进行查询。
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//类似与1.7的indexFor函数,但这里更简洁,用于查找key对应桶的位置
if (first.hash == hash && // 先看首节点是不是要找的节点
((k = first.key) == key || (key != null && key.equals(k))))
return first;//相同的返回
if ((e = first.next) != null) {
if (first instanceof TreeNode)//如果是树节点,则调用getTreeNode查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//否则作为单链表查找,和1.7相同
return e;
} while ((e = e.next) != null);
}
}
return null;//找不到返回空
}
HashMap存放数据
HashMap的put方法。put方法和1.7相比多了一个树形结构的判断以及把链表结构转化成树形结构的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//如果table为空或者是长度为零,则直接resize并返回n
if ((p = tab[i = (n - 1) & hash]) == null)//若table[i]的位置为空,则直接插入。
tab[i] = newNode(hash, key, value, null);
else {//如果在table[i]的位置上不为空
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//hash值和key值相等则将p赋值给e
e = p;
else if (p instanceof TreeNode)//否则如果p是个TreeNode类型则调用putTreeVal
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//这里采用的是尾插法。。。。。。。跟1.7完全不一样
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//尾插。。。。
if (binCount >= TREEIFY_THRESHOLD - 1) //如果正好binCount的数量大于等于7了,直接转化成树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//继续遍历
}
}
if (e != null) { // 如果已经存在,那么直接用新值替换旧值就可以了。
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)//判断需不需要扩容
resize();
afterNodeInsertion(evict);
return null;
}
HashMap扩容机制
1.8中的扩容方法的实现。临界值thr就是当前表容量cap和负载因子的乘积,如果表中的某一时间容量大于临界值,那么就需要使用扩容函数了。1.8扩容比1.7有意思很多,1.7是简单的从table的每一个桶里的单链表上去取出节点,重新计算hash然后放到新的表上的某一个桶里去。而1.8则相较而言复杂很多,它是从一个桶里的一条链或者说是一棵红黑树开始,我们知道同一个桶里的hash值是不一样的,1.8将他们分成了高低位分别操作。然后按低位hash一条链放在新的桶里,高位hash一条链放在桶里。这样相较于1.7而言会少很多计算。
/**
* 初始化时给表赋初始化或者是在size大于临界值的时候给表重新分配空间
*
*/
final Node<K,V>[] resize() {
Node<K,V>[] 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)//新容量扩容后小于最大容量,则将新容量和新临界值赋值为初始的两倍
newThr = oldThr << 1;
}
else if (oldThr > 0) // 如果只有一个旧的临界值,说明这是传进来的初始值,给容量赋为旧的临界值即可
newCap = oldThr;
else { // 初始化临界值为0,那么容量和临界值都使用默认的就行了
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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<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) {//找到具体某一个桶的位置,将它的首节点赋值给e
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//如果某一个桶里只有一个元素,那么重新hash放到新的桶里就可以了
else if (e instanceof TreeNode)//如果这个头结点是树节点的话,调用函数分为更小粒度
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
Node<K,V> loHead = null, loTail = null;//hash为0的低位链表
Node<K,V> hiHead = null, hiTail = null;//hash为1的高位链表
Node<K,V> next;
do {
next = e.next;//将低位的节点串成一条链
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) {//低位链放在table[i]de
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//高位链放在table[i+oldcap]的桶里
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap删除
1.8的remove方法相较而言除了树这一部分,其他的和1.7差别不大。都是查找到一个具体的节点,然后再对这个节点进行删除操作。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> 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<K,V>)p).getTreeNode(hash, key);
else {//否则遍历一条链,和1.7类似
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<K,V>)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;
}
1.7与1.8HashMap的区别
1.7
数据结构是单链表+数组,采用头插的方式处理映射到同一个桶里的数据。
核心参数比较简单,临界值,负载因子,最大容量等。
扩容机制采用挨个计算节点,重新分发到新的表中去。
代码比较清晰
1.8
数据结构是单链表+数组+红黑树,采用尾插的方式处理映射到同一个桶里的数据。
核心参数比1.7多了转化成树的阈值(8),转化成单链表的阈值(6)等。
扩容机制比1.7复杂,按照hash分成和容量相与操作为0的低位和为1的高位,转换成两条链之后放到不同的通里去。
代码看上去有点晦涩。