HashMap对于使用Java的小伙伴们来讲最熟悉不过,天天都在使用它。此次主要是分析下HashMap的工做原理,为何我会拿这个东西出来分析,主要是最近面试的小伙伴们,中被人问起HashMap,HashMap涉及的知识远远不止put和get那么简单。java
为何叫作HashMap?内部是怎样实现的呢?使用的时候大多数都是用String做为它的key呢?下面就让咱们来了解HashMap,并给你详细解释这些问题。面试
其实HashMap的由来是基于Hasing技术(Hasing),Hasing就是将很大的字符串或者任何对象转换成一个用来表明它们的很小的值,这些更短的值就能够很方便的用来方便索引、加快搜索。
算法
HashMap是一个用于存储Key-Value键值对的集合,你能够用一个”key”去存储数据。当你想得到数据的时候,你能够经过”key”去获得数据,每个键值对也叫作Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。数组
先介绍一下HashMap的变量安全
size,就是HashMap的存储大小。threshold是HashMap临界值,也叫阀值,若是HashMap到达了临界值,须要从新分配大小。loadFactor是负载因子, 默认为75%。阀值 = 当前数组长度✖负载因子。modCount指的是HashMap被修改或者删除的次数总数。bash
Entry分散存储在一个Entry类型的数组table, table里的每个数据都是一个Entry对象。Y轴方向表明的就是数组,X轴方向就是链表的存储方式。数据结构
table里面存储的Entry类型,Entry类里包含了hashcode变量,key,value 和另一个Entry对象。由于这是一个链表结构。经过我找到你,你再找到他。不过这里的Entry并非LinkedList,它是单独为HashMap服务的一个内部单链表结构的类。
app
数组的特色是特色是查询快,时间复杂度是O(1),插入和删除的操做比较慢,时间复杂度是O(n)。而链表的存储方式是非连续的,大小不固定,特色与数组相反,插入和删除快,查询速度慢。HashMap引用他们,选取了他们的有段,能够说是在查询,插入和删除的操做,都会有些提速。
函数
一、首先判断Key是否为Null,若是为null,直接查找Enrty[0],若是不是Null,先计算Key的HashCode,而后通过二次Hash,获得Hash值。
源码分析
二、根据Hash值,对Entry[]的长度length求余,获得的就是Entry数组的index。
三、根据对应的索引找到对应的数组,就是找到了其所在的链表,而后按照链表的操做对Value进行插入、删除和查询操做。
咱们都知道在Java中每一个对象都有一个hashcode()方法用来返回该对象的 hash值。HashMap先对hashCode进行hash操做,而后再经过hash值进一步计算下标。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}复制代码
HashMap是怎么经过Hash查找数组的索引的呢,调用indexFor,其中h是hash值,length是数组的长度,这个按位与的算法其实就是h%length求余。
/** * Returns index for hash code h. */
static int indexFor(int h, int length) {
return h & (length-1);
}
复制代码
其中h是hash值,length是数组的长度,这个按位与的算法其实就是h%length求余。
通常什么状况下利用该算法,典型的分组。例如怎么将100个数分组16组中,就是这个意思。应用很是普遍。
static int indexFor(int h, int length) {
return h & (length-1);
}复制代码
举个例子
int h=15,length=16;
System.out.println(h & (length-1));
System.out.println(Integer.parseInt("0001111", 2) & Integer.parseInt("0001111", 2));
h=15+16;
System.out.println(h & (length-1));
System.out.println(Integer.parseInt("0011111", 2) & Integer.parseInt("0001111", 2));
h=15+16+16;
System.out.println(h & (length-1));
System.out.println(Integer.parseInt("0111111", 2) & Integer.parseInt("0001111", 2));
h=15+16+16+16;
System.out.println(h & (length-1));
System.out.println(Integer.parseInt("1111111", 2) & Integer.parseInt("0001111", 2));
复制代码
调用put方法时,尽管咱们设法避免碰撞以提升HashMap的性能,仍是可能发生碰撞。听说碰撞率还挺高,平均加载率到10%时就会开始碰撞。
默认状况下,大多数人都调用 HashMap hashMap = new HashMap();来初始化的,咱们在这分析newHashMap(int initialCapacity, float loadFactor)的构造函数。
咱们都知道在Java中每一个对象都有一个hashcode()方法用来返回该对象的 hash值。HashMap先对hashCode进行hash操做,而后再经过hash值进一步计算下标。
代码以下:
public HashMap(int initialCapacity, float loadFactor) {
// initialCapacity表明初始化HashMap的容量,它的最大容量是MAXIMUM_CAPACITY = 1 << 30。
// loadFactor表明它的负载因子,默认是是DEFAULT_LOAD_FACTOR=0.75,用来计算threshold临界值的。 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);
}
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}复制代码
由上面的代码能够看出,初始化的时候须要知道初始化的容量大小,由于在后面要经过按位与的Hash算法计算Entry数组的索引,那么要求Entry的数组长度是2的N次方。
HashMap怎么存储一个对象呢,代码以下:
public V put(K key, V value) {
//数组为空时建立数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//①key为空单独对待
if (key == null)
return putForNullKey(value);
//②根据key计算hash值
int hash = hash(key);
//②根据hash值和当前数组的长度计算在数组中的索引
int i = indexFor(hash, table.length);
//遍历整条链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//③hash值和key值都相同的状况,替换以前的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//返回被替换的值
return oldValue;
}
}
modCount++;
//③若是没有找到key的hash相同的节点,直接存值或发生hash碰撞都走这
addEntry(hash, key, value, i);
return null;
}
复制代码
从代码中能够看出,步骤以下:
1.首先会判断能够是否为null,若是是null,就调用pullForNullKey(value)处理。代码以下:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}复制代码
若是key为null的值,默认就存储到table[0]开头的链表了。而后遍历table[0]的链表的每一个节点Entry,若是发现其中存在节点Entry的key为null,就替换新的value,而后返回旧的value,若是没发现key等于null的节点Entry,就增长新的节点。
2. 计算key的hashcode,再用计算的结果二次hash,经过indexFor(hash, table.length);找到Entry数组的索引i。
(3) 而后遍历以table[i]为头节点的链表,若是发现有节点的hash,key都相同的节点时,就替换为新的value,而后返回旧的value。
若是没有找到key的hash相同的节点,就增长新的节点addEntry(),代码以下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
//判断数组容量是否足够,不足够扩容
resize(2 * table.length);
}复制代码
(4)若是HashMap大小超过临界值,就要从新设置大小,扩容,稍后讲解。
附上一张流程图,这个图是从别的博主哪里copy的,感受画的不错。
咱们经过hashMap.get(K key) 来获取存入的值,key的取值很简单了。咱们经过数组的index直接找到Entry,而后再遍历Entry,当hashcode和key都同样就是咱们当初存入的值啦。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
复制代码
调用getEntry(key)拿到entry ,而后返回entry的value,来看getEntry(key)方法
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = 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;
}复制代码
相比put,get操做就没这么多套路,只须要根据key值计算hash值,和数组长度取模,而后就能够找到在数组中的位置(key为空一样单独操做),接着就是Entry遍历,hash相等的状况下,若是key相等就知道了咱们想要的值。
再get方法中有null的判断,null取hash值老是0,再getNullKey(K key)方法中,也是按照遍历方法来查找的。
众所周知,HashMap不是线程安全的,但在某些容错能力较好的应用中,若是你不想仅仅由于1%的可能性而去承受hashTable的同步开销,HashMap使用了Fail-Fast机制来处理这个问题,你会发现modCount在源码中是这样声明的。
调用put方法时,当HashMap的大小超过临界值的时候,就须要扩充HashMap的容量了。代码以下:
void resize(int newCapacity) { //传入新的容量
//获取旧数组的引用
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//极端状况,当前键值对数量已经达到最大
if (oldCapacity == MAXIMUM_CAPACITY) {
//修改阀值为最大直接返回
threshold = Integer.MAX_VALUE;
return;
}
//步骤①根据容量建立新的数组
Entry[] newTable = new Entry[newCapacity];
//步骤②将键值对转移到新的数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//步骤③将新数组的引用赋给table
table = newTable;
//步骤④修改阀值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}复制代码
void transfer(Entry[] newTable, boolean rehash) {
//获取新数组的长度
int newCapacity = newTable.length;
//遍历旧数组中的键值对
for (Entry<K,V> e : table) {
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];
newTable[i] = e;
e = next;
}
}
}
复制代码