HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减小实现此接口所需的工做node
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable面试
HashMap基于哈希表的 Map 接口的实现。此实现提供全部可选的映射操做,并容许使用 null 值和 null 键。(除了不一样步和容许使用 null 以外,HashMap 类与 Hashtable 大体相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。数组
值得注意的是HashMap不是线程安全的,若是想要线程安全的HashMap,能够经过Collections类的静态方法synchronizedMap得到线程安全的HashMap。安全
Map map = Collections.synchronizedMap(new HashMap());数据结构
HashMap提供了三个构造函数:app
HashMap():构造一个具备默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。ide
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。函数
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。源码分析
在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是建立哈希表时的容量,加载因子是哈希表在其容量自动增长以前能够达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来讲,查找一个元素的平均时间是O(1+a),所以若是负载因子越大,对空间的利用更充分,然然后果是查找效率的下降;若是负载因子过小,那么散列表的数据将过于稀疏,对空间形成严重浪费。系统默认负载因子为0.75,通常状况下咱们是无需修改的。性能
HashMap的底层主要是基于数组和链表来实现的,它之因此有至关快的查询速度主要是由于它是经过计算散列码来决定存储的位置。HashMap中主要是经过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就同样。若是存储的对象对多了,就有可能不一样的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同窗都知道,解决hash冲突的方法有不少,HashMap底层是经过链表来解决hash冲突的。
HashMap是基于哈希表的 Map 接口的实现。既然是基于哈希表的那么他的数据结构天然也就是由数组和链表组成的喽。数组的特色是:寻址容易,插入和删除困难;而链表的特色是:寻址困难,插入和删除容易。因此两者结合就是哈希表了。下面借用一张图片看看它的内部构造。
从图上能够看出左侧是一个长度为16的数组,右侧则是存储数据的链表。那数组的下标是按什么顺序存储的呢?其实存储规则是这样的,当拿到Map的key之后须要调用hashcode()方法计算他的hash值,而后用hash%len(数组长度)获得的结果就是它对应的顺序了。看看上面的十二、2八、10八、140对16作模运算都等于12 因此他们都在12上面。那咱们的key value去哪里了呢?HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value咱们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,咱们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
结构:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
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;
}
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;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
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);
}
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,造成一个链表。
有两点须要注意:
一是链的产生。这是一个很是优雅的设计。系统老是将新的Entry对象添加到bucketIndex处。若是bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,造成一条Entry链,可是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
2、扩容问题。
随着HashMap中元素的数量愈来愈多,发生碰撞的几率就愈来愈大,所产生的链表长度就会愈来愈长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必需要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。可是扩容是一个很是耗时的过程,由于它须要从新计算这些数据在新table数组中的位置并进行复制处理。因此若是咱们已经预知HashMap中元素的个数,那么预设元素的个数可以有效的提升HashMap的性能。
1.关键属性
先看看HashMap类中的一些关键属性:
1 transient Entry[] table;//存储元素的实体数组 2
3 transient int size;//存放元素的个数 4
5 int threshold; //临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量 6 7 final float loadFactor; //加载因子 8
9 transient int modCount;//被修改的次数
其中loadFactor加载因子是表示Hsah表中元素的填满的程度.
若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会愈来愈长,查找效率下降。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减少了,但:空间浪费多了.表中的数据将过于稀疏(不少空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高.
所以,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
若是机器内存足够,而且想要提升查询速度的话能够将加载因子设置小一点;相反若是机器内存紧张,而且对查询速度没有什么要求的话能够将加载因子设置大一点。不过通常咱们都不用去设置它,让它取默认值0.75就行了。
二、构造方法
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)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
咱们能够看到在构造HashMap的时候若是咱们指定了加载因子和初始容量的话就调用第一个构造方法,不然的话就是用默认的。默认初始容量为16,默认加载因子为0.75。咱们能够看到上面代码中13-15行,这段代码的做用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为何要把容量设置为2的n次幂,咱们等下再看。
3.数据存储
put 过程图解
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
第2和3行的做用就是处理key值为null的状况,咱们看看putForNullKey(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的话,hash值为0,对象存储在数组中索引为0的位置。即table[0]
计算hash码的函数 这个咱们要重点说下,咱们通常对哈希表的散列很天然地会想到用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值发生碰撞的几率较小,这样就能使元素在哈希表中均匀地散列。
这看上去很简单,其实比较有玄机的,咱们举个例子来讲明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果以下:
从上面的例子中能够看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上造成链表,那么查询的时候就须要遍历这个链 表,获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费至关大,更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1获得的二进制数的每一个位上的值都为1,这使得在低位上&时,获得的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上造成链表。
因此说,当数组长度为2的n次幂的时候,不一样的key算得得index相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面 put 方法的源代码能够看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:若是两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。若是这两个 Entry 的 key 经过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。若是这两个 Entry 的 key 经过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 造成 Entry 链,并且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
四、调整大小
resize()方法以下:
从新调整HashMap的大小,newCapacity是调整后的单位
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);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的所有元素添加到新的HashMap中,并从新计算元素在新的数组中的索引位置
当HashMap中的元素愈来愈多的时候,hash冲突的概率也就愈来愈高,由于数组的长度是固定的。因此为了提升查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,这是一个经常使用的操做,而在HashMap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap何时进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认状况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,扩容是须要进行数组复制的,复制数组是很是消耗性能的操做,因此若是咱们已经预知HashMap中元素的个数,那么预设元素的个数可以有效的提升HashMap的性能。
五、数据读取
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
六、HashMap的性能参数:
HashMap 包含以下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子建立一个 HashMap。
HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。
initialCapacity:HashMap的最大容量,即为底层数组的长度。
loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来讲,查找一个元素的平均时间是O(1+a),所以若是负载因子越大,对空间的利用更充分,然然后果是查找效率的下降;若是负载因子过小,那么散列表的数据将过于稀疏,对空间形成严重浪费。
HashMap的实现中,经过threshold字段来判断HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下容许的最大元素数目,超过这个数目就从新resize,以下降实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍.
从新调整HashMap大小存在什么问题?
当从新调整HashMap大小的时候,确实存在条件竞争,由于若是两个线程都发现HashMap须要从新调整大小了,它们会同时试着调整大小。在调整大小的过程当中,存储在链表中的元素的次序会反过来,由于移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了不尾部遍历(tail traversing)。若是条件竞争发生了,那么就死循环了。
为何String, Interger这样的wrapper类适合做为键? String, Interger这样的wrapper类做为HashMap的键是再适合不过了,并且String最为经常使用。由于String是不可变的,也是final的,并且已经重写了equals()和hashCode()方法了。其余的wrapper类也有这个特色。不可变性是必要的,由于为了要计算hashCode(),就要防止键值改变,若是键值在放入时和获取时返回不一样的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其余的优势如线程安全。若是你能够仅仅经过将某个field声明成final就能保证hashCode是不变的,那么请这么作吧。由于获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是很是重要的。若是两个不相等的对象返回不一样的hashcode的话,那么碰撞的概率就会小些,这样就能提升HashMap的性能
咱们可使用自定义的对象做为键吗? 这是前一个问题的延伸。固然你可能使用任何对象做为键,只要它遵照了equals()和hashCode()方法的定义规则,而且当对象插入到Map中以后将不会再改变了。若是这个自定义对象时不可变的,那么它已经知足了做为键的条件,由于当它建立以后就已经不能改变了。
咱们可使用CocurrentHashMap来代替Hashtable吗?这是另一个很热门的面试题,由于ConcurrentHashMap愈来愈多人用了。咱们知道Hashtable是synchronized的,可是ConcurrentHashMap同步性能更好,由于它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap固然能够代替HashTable,可是HashTable提供更强的线程安全性
下面一张图能够看得很清楚。
JDK1.6中HashMap采用的是位桶+链表的方式,即咱们常说的散列链表的方式,而JDK1.8中采用的是位桶+链表/红黑树的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。
//链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//省略
}
//红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//省略
}
// HashMap的主要属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 槽数组,Node<K,V>类型,TreeNode extends LinkedHashMap.Entry<K,V>,因此能够存放TreeNode来实现Tree bins
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
// 去掉了volatile的修饰符
transient int modCount;
int threshold;
final float loadFactor;
...
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
//hash & length-1 定位数组下标
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) {
/*第一个节点是TreeNode,则采用位桶+红黑树结构,
* 调用TreeNode.getTreeNode(hash,key),
*遍历红黑树,获得节点的value
*/
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
final TreeNode<K,V> getTreeNode(int h, Object k) {
//找到红黑树的根节点并遍历红黑树
return ((parent != null) ? root() : this).find(h, k, null);
}
/*
*经过hash值的比较,递归的去遍历红黑树,这里要提的是compareableClassFor(Class k)这个函数的做用,在某些时候
*若是红黑树节点的元素are of the same "class C implements Comparable<C>" type
*利用他们的compareTo()方法来比较大小,这里须要经过反射机制来check他们究竟是不是属于同一个类,是否是具备可比较性.
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> 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;
}
//put(K key,V value)函数 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<K,V>[] tab; Node<K,V> p; int n, i; //若是table为空或者长度为0,则resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //找到key值对应的槽而且是第一个,直接加入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //第一个node的hash值即为要加入元素的hash if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){ e = p; }else if (p instanceof TreeNode)//第一个节点是TreeNode,即tree-bin /*Tree version of putVal. *final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) */ e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //不是TreeNode,即为链表,遍历链表 for (int binCount = 0; ; ++binCount) { /*到达链表的尾端也没有找到key值相同的节点, *则生成一个新的Node,而且判断链表的节点个数是否是到达转换成红黑树的上界 *达到,则转换成红黑树 */ if ((e = p.next) == null) { p.next = newNode(hash, key, value, 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); //返回旧的value值 return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }