登陆

极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读

admin 2019-10-29 184人围观 ,发现0个评论

简介:

在Java Collections Framework的系统中中,主要有两个重要的接口,一个是List、Set和Queue所属的Collection,还有一个便是Map接口了。在上一篇文章中介绍了List接口,它适用于按数值索引拜访元素的景象。本文中将介绍的Map则供给了一个更通用的元素存储办法。

Map 调集类用于存储元素对(称作“键”和“值”)也叫键值对(key/value pair),其间每个键映射到一个值。从概念上而言,你可以将 List 看作是具有数值键的 Map。Map接口规则key值是不能重复的,而value值可以重复。

Map接口有三种重要的具体完成类——HashMap、WeakHashMap和TreeMap,其间HashMap还有一个重要的子类LinkedHashMap,它们都对错线程安全的类,本文将经过剖析源码要点介绍HashMap类,关于别的几个类的内容则留到后续文章再讲。

Map接口的架构如下图所示:

在图中可以看到,Map接口还有一个叫做HashTable的完成类,它是JDK前期的产品,与HashMap完成根本类似,不过是它是线程安全的,因为该容器现已过期,现在根本被弃用,因而在该文章中就不多加翰墨去介绍了。

概述

HashMap是依据哈希表完成的,HashMap的每一个元素是一个key-value对,其内部经过单链表和红黑树处理抵触问题,容量缺乏时会主动扩容。

HashMap对错线程安全的,只适用于单线程环境下,多线程环境下可以选用Concurrent并发包下的ConcurrentHashMap。

哈希抵触

关于每个目标 X 和 Y, 假如当且仅当 X.equals(Y) 为 false, 使得 X.hashCode()!= Y.hashCode() 为 true,这样的函数叫做完美 Hash 函数。当哈希函数对两个不同的数据项发作了相同的hash值时,这就称为哈希抵触。

依据目标中改动的字段,咱们可以很简略地结构一个完美哈希函数,可是这需求无限的内存巨细,这种假定显然是不或许的。而且,即便咱们可以为每个 POJO(Plain Ordinary Java Object)或许 String 目标结构一个理论上不会有抵触的哈希函数,可是 hashCode() 函数的返回值是 int 型。依据鸽笼理论,当咱们的目标超越 232 个时,这些目标会发作哈希抵触。

因而,完成HashMap的一个重要考量,便是尽或许地防止哈希抵触。HashMap在JDK 1.8中的做法是,用链表和红黑树存储相同hash值的value。当Hash抵触的个数比较少时,运用链表,不然运用红黑树。

底层完成

HashMap完成的接口如下:

public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable

HashMap承继自抽象类AbstractMap,完成了Map接口,AbstractMap类完成了Map接口的部分办法,因而Map的终究完成类直接承继AbstractMap,可以削减许多工作量。

先来看HashMap内部两个重要的静态内部类。

单向链表的节点Node

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;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

Node完成了Map的内部接口Entry,Entry接口界说了键值对(key-value pair)的根本操作,Node类供给了这些办法的完成而且还含有一个next引证,作为单链表的完成用来指向下一个Node。

红黑树的节点TreeNode:

static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
Tree极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读Node 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);
}

/** * Returns root of tree containing this node. */
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
……
}

当一个单链表抵触的结点数超越预设值时,将会把这个单链表主动调整为红黑树。这样做的优点是,最坏的状况下即一切的key都Hash抵触,选用链表的话查找时刻为O(n),而选用红黑树为O(logn)。

HashMap的几个重要字段如下:

//存储数据的Node数组,长度是2的幂。 transient Node[] table; //键值对缓存,它们的映射联系调集保存在entrySet中。即便Key在外部修正导致hashCode改动,缓存中还可以找到映射联系 transient Set> entrySet; //map中保存的键值对的数量 transientint size; //map结构被改动的次数 transient int modCount; //需求调整巨细的极限值(容量*装载因子) int threshold; //装载因子,在后面会进行具体介绍 final float loadFactor;

HashMap内部运用Node数组完成了一个哈希桶数组table。可以看出,HashMap仍是凭仗数组完成的,数组的元素是单链表或红黑树,关于key的hash值持平的key-value pair,它们将别离作为一个结点(Node或TreeNode)存储在同一个单链表或红黑树中。咱们知道数组的特色:寻址简略,刺进和删去困难,而链表的特色是:寻址困难,刺进和删去简略,红黑树则对刺进时刻、删去时刻和查找时刻供给了最好或许的最坏状况担保。HashpMap将这三者结合在一起。

HashMap的数据结构如下图所示:

此外,这儿的modCount特点,记录了map结构被改动的次数,它与“fail-fast”机制的完成休戚相关。fail-fast机制是Java调集的一种过错检测机制,假定存在两个线程(线程1、线程2),线程1经过Iterator在遍历调集A中的元素,在某个时分线程2修正了调集A的结构(是结构上面的修正,而不是简略的修正调集元素的内容),那么这个时分程序就会抛出 ConcurrentModificationException 反常,然后发作fail-fast机制。

关于HashMap内容的修正都将使modCount的值增加,在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判别modCount跟expectedModCount是否持平,假如不持平就表明现已有其他线程修正了Map。

HashMap的密一些重要的静态全局变量如下,它们与HashMap躲避哈希磕碰的战略休戚相关

/** * table默许的初始容量,它的值有必要是2的整数幂 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/** * table的最大容量,有必要小于2的30次方,假如传入的容量大于这个值,将被替换极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读为该值 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/** * 默许装载因子,假如在结构函数中不显式指定装载因子,则默许运用该值。 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/** * 结点抵触数到达8时,就会对哈希表进行调整,假如table容量小于64,那么会进行扩容, * 假如不小于64,那么会将抵触数到达8的那个单链表调整为红黑树. */
static final int TREEIFY_THRESHOLD = 8;

/** * 假如原先便是红黑树,resize今后抵触结点数少于6了,就把红黑色康复成单链表 */
static final int UNTREEIFY_THRESHOLD = 6;

/** * 假如table的容量少于64,那么即便抵触结点数到达TREEIFY_THRESHOLD后不会把该单链表调整成红黑数,而是将table扩容 */
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap运用的hash算法如下:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

运用hash值的高位16位与低16进行XORs操作,算法简练有用。

常用API

看完了HashMap的根本数据结构今后,来看一下常用办法的源码,首要天然想到的是get(key)和put(key,value)。

get(key)

get(key)办法的效果是的源码如下:

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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if 极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读(first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

咱们即将查找的key值传给get,它调用hashCode核算hash然后得到bucket方位,并进一步调用equals()办法确认键值对。取模算法中的除法运算功率很低,在HashMap中经过h & (n-1)替代取模,得到地点数组方位,功率会高许多(条件是确保数组的容量是2的整数倍)。

resize()

在介绍put办法之前还要先来看一下resize()办法,

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)
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;
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[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
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 { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

当HashMap中的元素个数超越 数组巨细 * loadFactor 时,就会进行数组扩容,loadFactor的默许值为0.75,这是一个折中的取值。也便是说,默许状况下,数组巨细为16,那么当HashMap中元素个数超越 16 * 0.75=12 的时分,就把数组的巨细扩展为 2 * 16=32 ,即扩展一倍,然后从头核算每个元素在数组中的方位,而这是一个十分耗费功能的操作,所以假如咱们现已预知HashMap中元素的个数,那么预设元素的个数可以有用的进步HashMap的功能。

put(key,value)

put(key,value)办法的效果是向HashMap中增加一对key-value pair。源码如下:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
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, 极彩登录网址是什么-面试不慌系列:HashMap结构源码之深化解读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))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

将key-value pair传给put办法时,它调用hashCode核算hash然后得到bucket方位,然后,HashMap依据当时bucket的占用状况主动调整容量(超越Load Factor则resize为本来的2倍)。假如没有发作磕碰就直接放到bucket里,假如发作磕碰,Hashmap先经过链表将发作磕碰抵触的元素组织起来,假如一个bucket中磕碰抵触的元素超越某个约束(默许是8),则运用红黑树来替换链表,然后进步速度。

最终,我是小架!

咱们下篇文章见!

请关注微信公众号
微信二维码
不容错过
Powered By Z-BlogPHP