源码解析jdk7.0-HashMap底层实现原理
一、HashMap的概述
HashMap是Map接口的实现类,键值对存储(基于哈希表的映射:根据指定的键,可以获取对应
的值),并允许null作为键和值,线程不安全,即方法为非同步方法。
二、HashMap的存储结构
-
Java编程语言中,最基本的两种结构:数组和链表(引用模拟指针),所有的数据结构都可以用这两种基本结构进行构建。数组的特点:寻址容易,插入和删除难;而链表的特点是:寻址困难,插入和删除容易。
-
综合数组和链表两者的特点,HashMap(直译为散列表,音译为哈希表)采用数组+链表的存储方式。
底层结构是一个数组(默认长度为16),而数组元素是一个单向的链表,每一个数组存储的元素代表的是每一个链表的头结点,结构如下:
三、HashMap内部实现原理机制(源码解析)
-
HashMap的基本元素
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ /* 默认的初始容量为:16 1 << 4 代表将1左移4位:2^4 = 16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量为2^30 = 1024*1024*1024 = 1073741824 static final int MAXIMUM_CAPACITY = 1 << 30; /* 默认的负载因子 负载因子表示一个散列空间的使用程度。 当向集合容器中添加元素的时候,会判断当前容器的个数: 如果当前容器的个数 > 阈(yu)值:即底层数组长度*负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; // 空表(空数组) static final Entry<?,?>[] EMPTY_TABLE = { }; /** 以Entry<K,V>为元素的数组,也就是上图HashMap的纵向的长链数组, 起长度必须为2的n次幂 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // Map集合中键值对的个数 transient int size; /* 扩容的临界值(阈值),或者所能容纳的key-value对的极限。 当size>threshold的时候就会扩容 */ int threshold; // 加载因子 final float loadFactor; // 记录对集合修改的次数 transient int modCount;
(1) DEFAULT_INITIAL_CAPACITY 和 MAXIMUM_CAPACITY
通过HashMap的无参数的构造方法创建一个HashMap时,系统会默认使用默认的数组
长度为:DEFAULT_INITIAL_CAPACITY(16),但是如果指定长度时,容量不能超过
MAXIMUM_CAPACITY(2^30)
(2) DEFAULT_LOAD_FACTOR 和 loadFactor
扫描二维码关注公众号,回复: 12198705 查看本文章 创建HashMap对象时。可以指定负载因子(loadFactor),如果通过无参数的构造方法
创建HashMap对象时,则默认的负载因子DEFAULT_LOAD_FACTOR 为 0.75
注意:负载因子表示一个散列表的空间使用程度,如果负载因子越大则代表散列表的
填装度越高,即能容纳的元素越多,元素多,链表相对会长,所以索引的效率
会降低;如果负载因子越小,则代表散列表的填充度越稀疏,此时对空间造成
浪费,此时索引效率相对较高。所以负载因子需要合理设置其大小。
(3) size 和 threshold
size代表HashMap集合中存储Entry<K,V>的个数(即键值对的个数);
threshold代表扩容的临界值,即阈(yu)值,来源:底层数组长度 * 负载因子
注意: threshold的初始数据和底层数组长度相同,在第一次调用put方法时,将
threshold的值重新赋值为:capacity(数组长度) * loadFactor(加载因子)
(4) Entry<K,V>[] table
Entry<K,V>[] table是HashMap的一个重要组成部分,一个Entry中包含了一个键值对的
内容,详见以下源码:
// 此静态内部类和内部的属性和方法都是包级访问权限 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; 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; } .... }
从源码中可以看到Entry<K,V>是HashMap类中的一个静态内部类,它既是HashMap底层数
组的组成元素,又是每一个单向链表的组成元素,并且它包含了元素的key和value,以及链
表所需要的指向下一个节点的地址区next,存储的具体结构如下:
-
HashMap中的构造方法:
//可以指定数组长度(容量)和负载因子 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); this.loadFactor = loadFactor; threshold = initialCapacity; init(); } // 指定数组长度(容量),负载因子为默认的0.75 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 无参数的构造:都采用默认数据(数组长度为16;负载因子为0.75) public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } // 基于一个Map创建一个新的HashMap对象 public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); inflateTable(threshold); putAllForCreate(m); }
-
HashMap中存储:put方法的实现
public V put(K key, V value) { /* 首次添加,才初始化Entry数组; 创建HashMap对象时,并没有完成数组的初始化 */ if (table == EMPTY_TABLE) { inflateTable(threshold); } /* HashMap是允许null作为键和值,如果键为null, 则调用 putForNullKey方法,将null存放在数组 下标0位置上,具体看putForNullKey方法的实现 */ if (key == null) return putForNullKey(value); // 如果键不为null,获取hash值 int hash = hash(key); // 根据hash值和数组长度进行运算获取,对应的存储下标 int i = indexFor(hash, table.length); /* 如果对应下标位置上Entry不为null,则表示此位置已经存在,则 查看当前数组下标和对应的链表中是否存在和存储的key相同的元素, 存在,则新value覆盖原有value,原有value作为返回值返回。 */ 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++; /* 如果当前数组下标位置上Entry为null,或是不存在相同的key, 则在对应数组下标位置上存储该 键值对(Entry) */ addEntry(hash, key, value, i); return null; }
putForNullKey方法的实现源码如下:
// 键为null的存储方法:本类中的访问权限 private V putForNullKey(V value) { /* 将key=null的键值对存储在数组下标0位置上, 如果0位置上已经有元素,则遍历数组0下标及对应链表内容, 查看是否有键为null, 如果存在key为null的数据,则新值覆盖旧值 */ 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++; /* 如果数组0下标对应的所有元素都没有key为null的键, 则将key为null的数据存储在数组0下标的第一个位置 */ addEntry(0, null, value, 0); return null; }
解析:从put方法的源码中可以看出:当调用HashMap的put方法往集合中添加元素时,先根据key的hashCode计算hash码值,根据hash码值和底层数组的长度获取该键值对在数组中存储的下标:
(1) **如果数组该下标位置上已经存储了其他元素:**先检测要存储的元素和已经存储元素
的键是否有相同的,如果有相同的,则新值覆盖旧值,被覆盖的旧值作为返回值进
行返回;如果没有相同的键(对象类型用equals比较的),则数组的这个下标位置将
采用数组+链表的形式进行存放,但是需要注意的是:新加入元素放在链表的头部,
最早加入的放在链表的尾部。
(2) 如果数组该下标位置上没有存储其他元素:直接将键值对存储在数组下标位置上即
可。
但是如果key为null,将此键值对存储在数组的0下标位置上,如果0下标上没有元素直
接存储该键值对,但是如果0下标上已经存在键值对,由于HashMap是要求键不可以
重复,所以先判断数组0下标及对应的链表中是否存在key为null的Entry(键值对),如果
存在,则新值覆盖旧值,被覆盖的原有数组作为返回值进行返回,如果不存在,将此键
值对存储在数组的0下标位置上。
应用层解析:根据put方法的分析,当HashMap中存储键值对时,仅仅考虑key,完全不考虑value,只是根据key来计算并决定每一个键值对(Entry)存储的位置。如果保证键的不重复性,需要让内容相同的键有一个相同的存储位置,这样会让for循环的过程中if条件成立(此过程中需要调用equals方法),则才能让重复的键对应的值,新值替换旧值;但是如果每存储一个键值对时,都获取相同的存储下标,这样数组的同一个下标对应的链表就会很长,并且每一次存储都需要调用equals方法具体比较键的内容是否相同,则会降低存储的效率,所以为了提高效率,尽可能满足内容不同的键给定一个不同的存储下标,这样可以尽量让HashMap中的元素分布均匀即每一个位置上尽可能一个元素。
所以如果自定类型的元素作为HashMap的键时,需要覆盖hashCode方法和equals方法:
(1) 覆盖hashCode方法的原则:
a. 必须保证内容相同的元素返回相同的哈希码值
b. 为了提高效率,尽可能做到内容不同的元素返回不同的哈希码值
(2) equals方法:内容相同的对象返回true。
-
HashMap的读取:get方法的实现
public V get(Object key) { //如果键为null,则调用getForNullKey方法 if (key == null) return getForNullKey(); // 获取键对应的Entry Entry<K,V> entry = getEntry(key); // 通过Entry中getValue方法,获取键对应的value return null == entry ? null : entry.getValue(); } // 被调getForNullKey方法: private V getForNullKey() { //如果HashMap中为空,则返回null if (size == 0) { return null; } /*根据put方法讲解,如果key为null,则将此键值对存储 在数组的0下标位置上,所以get方法时,如果指定的键 为null,则遍历数值的0下标及对应链表中的内容,找到 key为null,将对应的值返回 */ for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } // 如果在数组0下标中不存在键为null的元素,则直接返回null return null; } // 被调getEntry方法 final Entry<K,V> getEntry(Object key) { // 集合为空,则直接返回null if (size == 0) { return null; } // 获取键对应的hash值 int hash = (key == null) ? 0 : hash(key); // 根据hash值计算对应的下标,通过equals方法获取对应的元素 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; } //如果对应下标没有相同的key,则返回null return null; }
解析:从HashMap通过get方法获取元素时,首先计算key的hash码值,找到数组中对应的下标,然后 通过equals方法获取key对应的的Entry,再通过Entry中getValue方法获取键对应的value。如果数组的同一个下标的链表越长,循环次数就会越多,查询的效率相对会低;所以为了尽可能让HashMap存储时,尽可能均匀。
-
HashMap的扩容: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]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } // 每次添加操作时,需要判断是否扩容 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
解析:当HashMap中元素逐步增多时,键值对存储时,获取的下标冲突的几率也逐步增高,因为数组的长度是固定的。所以为了提高效率,需要对HashMap的底层数组进行扩容,而在数组扩容之后,消耗性能的操作为:原数组中的数据必须根据每一个元素的key和新数组的长度获取新的存储位置,并进行存储.
问题:HashMap什么时候进行扩容?
解析:当HashMap中的元素个数超过threshold(阈值:即数组长度*加载因子[loadFactor])时,需要将数组扩展为 2 * 数组长度,即扩大一倍。
四、Fail-Fast机制(快速失败)
-
在使用迭代器遍历集合过程中,对集合中对象的内容进行了修改(增、删、改)会抛出java.util.ConcurrentModificationException(并发修改异常),这就是所谓的Fail-Fast策略。
-
实现原理:在HashMap类中定义了modCount属性,记录了对HashMap的修改次数,只要对HashMap中的内容进行修改,都会让modCount增加;在迭代器初始化时 ,会将modCount的值赋值给 expectedModCount ,每次迭代(nextNode操作)时,都会先判断modCount和expectedModCount 的值是否相等,如果不相等则代表HashMap集合被操作,就会抛出java.util.ConcurrentModificationException异常。
注意:modCount声明为volatile,保证线程之间修改的可见性。
-
源码如下:
abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do{ }while(index<t.length&&(next=t[index++])==null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next=(current=e).next)==null&&(t=table)!=null){ do{ }while(index<t.length&&(next=t[index++])==null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }
解析:在HashMap的API中指出:java.util 包中所有集合的迭代器都是Fail-Fast(快速失败)机制,在迭代器创建之后,如果想对元素内容进行修改,则可以通过迭代器本身的remove方法,其他任何修改方式,迭代器都将会抛出java.util.ConcurrentModificationException异常。
注意:迭代器的Fail-Fast机制不能完全确保,一般来说:存在非同步的并发修改时,不能做出十分保证,快速失败机制尽最大的可能抛出java.util.ConcurrentModificationException,因此开发时不要依赖于此异常进行编写程序,只能作为程序的错误检测。