一.Map
Java集合中,List和set存储的都是单个值,而Map中存储的却是key-value对。
实现Map接口的类可以分为两种:基于Hash表的和基于RB-Tree的。
基于Hash表的主要是HashMap,该类具有Hash表的一些特点
1)集合中元素是无序的
2)操作时间复杂度为O(1)
3)可能要考虑hash冲突、hash表扩容的情况
4)允许key或value为null,但只能存在一个这样的元素
基于RB-Tree的主要是TreeMap,具有RB-Tree的一些特点:
1)由于插入到RB-Tree中会进行排序,因此集合中的元素都是有序的
2)操作的时间复杂度是O(lgn)
3)不允许null元素
下面将主要对HashMap的实现进行分析
二、HashMap的实现
Hash表有多种不同的实现方法,HashMap是采用“拉链法”实现的,即先是通过数组来实现。不同的元素经过hash()函数得到的值可能相同,因此可能还会产生冲突,冲突时则采用链表的形式解决。示意图如下:
1、基本属性
HashMap的基本属性如下所示:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final float loadFactor;
transient Entry<K,V>[] table; //存储元素的Hash表
transient int size;
int threshold;
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
}
可以发现HashMap的容量为[16,2^30]
当未指定负载因子时,默认的loadfactor = DEFAULT_LOAD_FACTOR = 0.75f;
负载因子 = 已有元素个数 / HashMap的总容量 ,当负载因子过大时,则会导致一个数组元素的链表非常长,从而导致查找的效率非常低,此时则需要对HashMap进行扩容。
threshold为可存储的元素最大个数
table是存储所有key-value对的Hash数组,是真正存储数据的地方。
该数组中的每一个元素都是一个Entry,而Entry的基本结构如下
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
每一个Entry都包含一个key和value,以及计算后得到的hash值,并通过next引用来解决hash冲突。
2、构造与清除
如果对hash表有过了解的话,可以发现HashMap的实现与hash表基本相同
HashMap的构造有如下几种方式,可以发现最终实现的都是构造一个capacity、loadfactor确定的hash表,注意capacity的大小会自动调整为2的整数倍。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; //capacity只能为2的整数倍
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; //创建hash表数组
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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中的元素时,只需要将各个桶中的首个元素置为null即可,而不用管链表后面的元素。这是由于JVM的GC机制中对于无用元素的判断是根据根搜索法的,当引用链上的对象都与GC Roots没有联系时,则这些对象都是可以删除的。只将链表首元素置为null,则整条链都是可以释放的。
public void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
3、辅助方法
Hash表最主要的特点是时间复杂度为O(1),主要的原理首先根据元素及相应的hash算法求出元素的hash值,然后根据hash值得到元素在hash表中的桶下标,再直接在hash数组中利用下标访问。
因此hash表一般都会有两个基本的内部方法用于支持其它操作,即hash()用于计算元素hash值,和indexFor()获得元素在hash表中的位置。其中hash值只与key的hashcode有关。
static int indexFor(int h, int length) { //h为hash值,length为数组长度
return h & (length-1); //数组下标为[0,length-1],二者相与即可得到元素在hash表的哪个桶中
}
4、加入一个元素
由于HashMap是基于hash表的,因此所有元素的key必须唯一。
当key不存在时,则直接向hash表中加入元素,否则将原有的value替换新元素的value
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //key为null时,单独处理
int hash = hash(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))) { //当元素相等时,则替换原有元素
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); //否则直接加入新元素
return null;
}
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) { //如果已有key为null的元素,则将原有元素替换为新元素
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0); //否则向hash表中加入一个新的元素,key为null时hash值为0
return null;
}
从上面的实现中可以发现,每当向HashMap中加入一个新元素时,最终会调用到的都是addEntry(),addEntry()的具体实现如下,首先判断是否需要扩充hash表的容量,然后找到对应的hash桶,并将新元素插入到链表的头部。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //扩充hash表的容量,为原来的2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //e为相应的hash桶中单链表的头部节点
table[bucketIndex] = new Entry<>(hash, key, value, e); //把新加入的节点作为链表的头部加入
size++;
}
5、删除一个元素
移除一个元素的操作与加入一个元素的操作基本相同:
1)首先通过key得到hash值
2)然后根据hash值找到对应的hash桶
3)再遍历该hash桶对应的链表,找到待删除的元素
4)然后更新链表的前、后节点。
这样就讲一个元素从链表中移除了。
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key); //根据key得到hash值
int i = indexFor(hash, table.length); //根据hash值找到对应hash桶的索引
Entry<K,V> prev = table[i]; //hash桶中链表的头节点
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next; //保存每个元素的next节点
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) { //当找到待删除元素时
modCount++;
size--;
if (prev == e) // prev == e ,则说明e为链表头节点,则直接将table[i]更新为e.next即可
table[i] = next;
else //否则将e.prev.next更新为e.next
prev.next = next;
e.recordRemoval(this);
return e; //然后返回该节点
}
prev = e;
e = next;
}
return e;
}
6、查找&修改
get()与set()操作与上面的put、remove也基本类似,首先都是根据key计算hash值,然后根据hash值找到对应的hash桶,再遍历该链表找到目标元素,然后再对目标元素进行相应操作。
7、扩容
当向HashMap中加入一个新元素,导致HashMap中元素个数超过阈值时,就会对Hash表进行扩容,且新容量为原来的2倍,即会调用resize(2*table.length); 从而可以保证hash表的容量始终为2的整数倍。
resize()的具体实现如下:
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]; //创建一个新的hash表,容量为newCapacity
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash); //将原来hash表中的内容全部搬运到新建的hash表中
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
可以发现其中最重要的操作就是transfer(),将原来hash表中的内容全部搬运到新建的hash表中。
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;
}
}
}
可以发现transfer()的基本思想是将原来链表从头到尾遍历,然后依次将头部节点作为新的hash表的头部节点,因此这会导致链表逆序。即若原来链表为1 —> 2 —-> 3 —-> 4,经过transfer之后会变为 4 —> 3 —-> 2 —-> 1.
8、线程不安全
我们都知道HashMap是线程不安全的,而这是怎么体现的呢?
会带来线程不安全问题的主要原因就是当两个线程都在加入新元素时需要扩容,那么在扩容的时候可能会导致链表成环
假设当前有HashMap的hash表如下所示
假设此时hash表已满,则再向其中加入一个元素时,则会导致扩容,会调用resize()方法。
若此时有两个线程线程A和线程B都在调用put()加入新元素,则两个线程如果几乎同时进行,则很可能都会调用resize()。resize()传递的newCapacity参数也会相同,因此可能两个线程同时都在执行transfer搬运数据。
分析transfer的源码可以发现具体的操作是将原来链表的头节点作为新的hash表中新链表的头节点。
如果线程A、线程B执行了以下操作:
1)A:取出节点1,作为新链表的头节点,则原来链表为 2 —-> 3 —-> 4,新链表为 1
2)B:取出节点2
3)A:取出节点2,作为新链表的头节点,则原来链表为 3 —-> 4,新链表为 2 —-> 1
4)A:取出节点3,作为新链表的头节点,则原来链表为 4,新链表为 3 —-> 2 —-> 1
则得到的结果示意图如下:
此时B取出的头节点为2,然后再将其插入到新链表的头节点,则得到的结果为:
原本3.next = 2,现在 2.next = 3,即e.next.next=e ,导致链表形成环。
最后再将4加入到链表头部,得到的最终结果为
若要在给HashMap中查找节点1,则首先会找到该链表然后遍历,但是由于链表已经形成环,因此会陷入死循环。
因此HashMap是线程不安全的。
三、对比
下面对HashMap与其它一些Map进行对比。
HashMap与HashTable:
1)Hashtable是基于陈旧的Dictionary类的,HashMap是Map接口的一个实现
2)Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的
3)由于HashMap是非线程安全的,因此执行时效率会更高
4)HashTable不允许null作为元素的key或者value,而HashMap则允许一个元素的key或value为null
HashMap与TreeMap
1)HashMap是基于hash表的,TreeMap是基于RB-Tree的
2)HashMap中的元素是无序的,TreeMap中的元素是有序的
3)HashMap的时间复杂度为O(1),TreeMap的时间复杂度为O(lgn)
4)HashMap允许一个元素的key或value为null,TreeMap则不允许