副标题#e#
HashMap简介
HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表办理斗嘴问题,容量不敷(高出了阀值)时,同样会自动增长。
HashMap长短线程安详的,只是用于单线程情况下,多线程情况下可以回收concurrent并发包下的concurrentHashMap。
HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
HashMap源码分解
HashMap的源码如下(插手了较量具体的注释):
package java.util;
import java.io.*;
public class HashMapextends AbstractMapimplements Map, Cloneable, Serializable
{
// 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必需是2的整数次幂。
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必需是2的幂且小于2的30次方,传入容量过上将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的Entry数组,长度是2的幂。
// HashMap回收链表法办理斗嘴,每一个Entry本质上是一个单向链表
transient Entry[] table;
// HashMap的底层数组中已用槽的数量
transient int size;
// HashMap的阈值,用于判定是否需要调解HashMap的容量(threshold = 容量*加载因子)
int threshold;
// 加载因子实际巨细
final float loadFactor;
// HashMap被改变的次数
transient volatile int modCount;
// 指定“容量巨细”和“加载因子”的结构函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因此不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 找出“大于initialCapacity”的最小的2的幂
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 配置“加载因子”
this.loadFactor = loadFactor;
// 配置“HashMap阈值”,当HashMap中存储数据的数量到达threshold时,就需要将HashMap的容量更加。
threshold = (int)(capacity * loadFactor);
// 建设Entry数组,用来生存数据
table = new Entry[capacity];
init();
}
// 指定“容量巨细”的结构函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认结构函数。
public HashMap() {
// 配置“加载因子”为默认加载因子0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 配置“HashMap阈值”,当HashMap中存储数据的数量到达threshold时,就需要将HashMap的容量更加。
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// 建设Entry数组,用来生存数据
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// 包括“子Map”的结构函数
public HashMap(Map m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 将m中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
//求hash值的要领,从头计较hash值
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 返回h在数组中的索引值,这里用&取代取模,旨在晋升效率
// h & (length-1)担保返回值的小于length
static int indexFor(int h, int length) {
return h & (length-1);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 获取key对应的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值便是key”的元素
for (Entrye = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判定key是否沟通
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
//没找到则返回null
return null;
}
// 获取“key为null”的元素的值
// HashMap将“key为null”的元素存储在table[0]位置,但不必然是该链表的第一个位置!
private V getForNullKey() {
for (Entrye = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
// HashMap是否包括key
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
// 返回“键为key”的键值对
final EntrygetEntry(Object key) {
// 获取哈希值
// HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则挪用hash()计较哈希值
int hash = (key == null) ? 0 : hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值便是key”的元素
for (Entrye = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计较该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entrye = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value代替旧的value。然退却出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
// putForNullKey()的浸染是将“key为null”键值对添加到table[0]位置
private V putForNullKey(V value) {
for (Entrye = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 假如没有存在key为null的键值对,则直接题阿见到table[0]处!
modCount++;
addEntry(0, null, value, 0);
return null;
}
// 建设HashMap对应的“添加要领”,
// 它和put()差异。putForCreate()是内部要领,它被结构函数等挪用,用来建设HashMap
// 而put()是对外提供的往HashMap中添加元素的要领。
private void putForCreate(K key, V value) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
// 若该HashMap表中存在“键值便是key”的元素,则替换该元素的value值
for (Entrye = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
// 若该HashMap表中不存在“键值便是key”的元素,则将该key-value添加到HashMap中
createEntry(hash, key, value, i);
}
// 将“m”中的全部元素都添加到HashMap中。
//本栏目
#p#副标题#e#1、首先要清楚HashMap的存储布局,如下图所示:#p#分页标题#e#图中,紫色部门即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来办理斗嘴的,假如差异的key映射到了数组的同一位置处,就将其放入单链表中。#p#分页标题#e#2、首先看链表中节点的数据布局:// Entry是单向链表。
// 它是 “HashMap链式存储法”对应的链表。
// 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
static class Entryimplements Map.Entry{
final K key;
V value;
// 指向下一个节点
Entrynext;
final int hash;
// 结构函数。
// 输入参数包罗"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
Entry(int h, K k, V v, Entryn) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判定两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 不然,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 实现hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 当向HashMap中添加元素时,绘挪用recordAccess()。
// 这里不做任那里理惩罚
void recordAccess(HashMapm) {
}
// 当从HashMap中删除元素时,绘挪用recordRemoval()。
// 这里不做任那里理惩罚
void recordRemoval(HashMapm) {
}
}它的布局元素除了key、value、hash外,尚有next,next指向下一个节点。别的,这里覆写了equals和hashCode要领来担保键值对的唯一无二。3、HashMap共有四个结构要领。结构要领中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap机能的重要参数,个中容量暗示哈希表中槽的数量(即哈希数组的长度),初始容量是建设哈希表时的容量(从结构函数中可以看出,假如不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以到达多满的一种标准,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表举办 resize 操纵(即扩容)。下面说下加载因子,假如加载因子越大,对空间的操作更充实,可是查找效率会低落(链表长度会越来越长);假如加载因子太小,那么表中的数据将过于稀疏(许多空间还没用,就开始扩容了),对空间造成严重挥霍。假如我们在结构要领中不指定,则系统默认加载因子为0.75,这是一个较量抱负的值,一般环境下我们是无需修改的。别的,无论我们指定的容量为几多,结构要领城市将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能高出2的30次方4、HashMap中key和value都答允为null。5、要重点阐明下HashMap顶用的最多的两个要领put和get。先从较量简朴的get要领着手,源码如下:// 获取key对应的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 获取key的hash值
int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值便是key”的元素
for (Entrye = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
/判定key是否沟通
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
没找到则返回null
return null;
}
// 获取“key为null”的元素的值
// HashMap将“key为null”的元素存储在table[0]位置,但不必然是该链表的第一个位置!
private V getForNullKey() {
for (Entrye = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}#p#分页标题#e#首先,假如key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。记着,key为null的键值对永远都放在以table[0]为头结点的链表中,虽然不必然是存放在头结点table[0]中。#p#副标题#e#假如key不为null,则先求的key的hash值,按照hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与方针key相等,有就返回对应的value,没有则返回null。put要领稍微巨大些,代码如下:
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计较该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entrye = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value代替旧的value。然退却出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}假如key为null,则将其添加到table[0]对应的链表中,putForNullKey的源码如下:// putForNullKey()的浸染是将“key为null”键值对添加到table[0]位置
private V putForNullKey(V value) {
for (Entrye = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 假如没有存在key为null的键值对,则直接题阿见到table[0]处!
modCount++;
addEntry(0, null, value, 0);
return null;
}假如key不为null,则同样先求出key的hash值,按照hash值得出在table中的索引,尔后遍历对应的单链表,假如单链表中存在与方针key相等的键值对,则将新的value包围旧的value,比将旧的value返回,假如找不到与方针key相等的键值对,可能该单链表为空,则将该键值对插入到改单链表的头结点位置(每次新插入的节点都是放在头结点的位置),该操纵是有addEntry要领实现的,它的源码如下:// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 生存“bucketIndex”位置的值到“e”中
Entrye = table[bucketIndex];
// 配置“bucketIndex”位置的元素为“新Entry”,
// 配置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry(hash, key, value, e);
// 若HashMap的实际巨细 不小于 “阈值”,则调解HashMap的巨细
if (size++ >= threshold)
resize(2 * table.length);
}留意这里倒数第三行的结构要领,将key-value键值对赋给table[bucketIndex],并将其next指向元素e,这便将key-value放到了头结点中,并将之前的头结点接在了它的后头。该要领也说明,每次put键值对的时候,老是将新的该键值对放在table[bucketIndex]处(即头结点处)。两外留意最后两行代码,每次插手键值对时,都要判定当前已用的槽的数目是否大于便是阀值(容量*加载因子),假如大于便是,则举办扩容,将容量扩为本来容量的2倍。6、关于扩容。上面我们看到了扩容的要领,resize要领,它的源码如下:// 从头调解HashMap的巨细,newCapacity是调解后的单元
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}#p#分页标题#e#很明明,是新建了一个HashMap的底层数组,尔后挪用transfer要领,迁就HashMap的全部元素添加到新的HashMap中(要从头计较元素在新的数组中的索引位置)。transfer要领的源码如下:// 将HashMap中的全部元素都添加到newTable中
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entrye = src[j];
if (e != null) {
src[j] = null;
do {
Entrynext = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}本栏目很明明,扩容是一个相当耗时的操纵,因为它需要从头计较这些元素在新的数组中的位置并举办复制处理惩罚。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的机能。#p#副标题#e#7、留意containsKey要领和containsValue要领。前者直接可以通过key的哈希值将搜索范畴定位到指定索引对应的链表,尔后者要对哈希数组的每个链表举办搜索。8、我们重点来阐明下求hash值和索引值的要领,这两个要领即是HashMap设计的最为焦点的部门,二者团结能担保哈希表中的元素尽大概匀称地散列。计较哈希值的要领如下:static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}它只是一个数学公式,IDK这样设计对hash值的计较,自然有它的长处,至于为什么这样设计,我们这里不去追究,只要大白一点,用的位的操纵使hash值的计较效率很高。由hash值找到对应索引的要领如下:static int indexFor(int h, int length) {
return h & (length-1);
}这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种要领根基能担保元素在哈希表中散列的较量匀称,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的要领来取代取模,同样实现了匀称的散列,但效率要高许多,这也是HashMap对Hashtable的一个改造。接下来,我们阐明下为什么哈希表的容量必然要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便担保了散列的匀称,同时也晋升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便担保了h&(length-1)的最后一位大概为0,也大概为1(这取决于h的值),即与后的功效大概为偶数,也大概为奇数,这样便可以担保散列的匀称性,而假如length为奇数的话,很明明length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位必定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便挥霍了近一半的空间,因此,length取2的整数次幂,是为了使差异hash值产生碰撞的概率较小,这样就能使元素在哈希表中匀称地散列。
