概述
HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
继承关系、接口实现
//继承关系
java.lang.Object
java.util.AbstractMap<K,V>
java.util.Hash<K,V>
//接口实现
Map<K,V>, Serializable, Cloneable
底层依赖
HashMap底层依赖数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算key的hashCode来决定存储的位置。
哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组叫做哈希表。
//该表根据需要调整大小,长度必须始终是2的次幂
transient Entry<K,V>[] table;
由源码可知,HashMap封装了一个Entry数组,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向链表结构(是用来解决Hash冲突的),具有next指针属性,用来指向下一个Entry实体。
HashMap底层采用这种结构,不仅在查找、增删方面有较好的效率,而且有效的解决了Hash冲突问题(后面讲解)。
重要参数
//默认初始容量,必须是2的次幂
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;
//临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
int threshold;
其中,loadFactor就是集合扩容的判断,集合中可存储的元素个数为(容量 * 加载因子)。
若加载因子越大,集合中存储的元素越多,空间利用率提高了;但是,增大了冲突的机率,导致链表长度变长,降低了查找效率。
若加载因子越小,集合中存储的元素越少,减少了冲突的机率;但是,不仅浪费了空间,还增加了扩容的频率。
一般,不去给loadFactor设置,取它的默认值0.75。
构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
//得到大于initialCapacity的最小2次幂的容量。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //计算临界值,判断是否进行扩容操作。
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
public HashMap(Map<? extends K, ? extends V> m) {
......
}
第一个无参构造:调用第三个构造方法,所传参数是默认容量与加载因子。默认初始化容量大小为16。
第二个带参构造:调用第三个构造方法,所传参数是自定义容量与默认加载因子。
第三个带参构造:指定参数容量与加载因子,完成HashMap的初始化操作。该构造方法保证了集合容量为2的次幂。
第三个带参构造:这里不进行说明。
添加方法
public V put(K key, V value) {
if (key == null) //若key为null,该键值对添加到table[0]中
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length); //根据key的hash值,计算出对应table中的索引
//循环遍历该索引处的Entry对象。判断Entry链中是否有相同的key,如果没有,发生冲突。
//若该索引处没有Entry实体,直接添加
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断key是否与Entry中的相同,若相同,覆盖value;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++; //修改次数加1
addEntry(hash, key, value, i); //将该键值对添加到table[i]处
return null;
}
put(K key,V value)方法中,得到key的hash值后,计算出在数组中的索引位置,该索引处若已有Entry对象,说明存在Entry对象具有相同的hash值。这时用equals()方法判断它们的key是否相同,如果相同,覆盖value值;如果不相同,这时就是产生了hash冲突。
Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。结构如下:
Hashmap里面的bucket出现了单链表的形式,哈希表要解决的一个问题就是哈希值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //将容量扩充为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];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
扩容操作
在执行put()方法时,就对扩容进行判断,若需要扩容,扩充为2倍。
下面看看resize()方法的源码: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]; //以新容量重新创建Entry数组
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash); //对数组中的Entry对象重新确定索引位置
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) { //循环遍历table,对Entry对象重新确定索引位置
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;
}
}
}
查询操作
public V get(Object key) {
if (key == null) //若key为null,返回table[0]处Entry对象的value值
return getForNullKey();
Entry<K,V> entry = getEntry(key); //若不为null,根据key查询其Entry对象
return null == entry ? null : entry.getValue();
}
//根据key查询Entry对象,若有,返回e;否则;返回null
final Entry<K,V> getEntry(Object key) {
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;
}
若HashMap里面没有出现hash冲突时,没有形成单链表时,HashMap查找元素很快,get()方法能够直接定位到元素,但是如果发生冲突,出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能按顺序遍历每个 Entry,直到找到想查询的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素,此时,查询效率较低。
HashMap总结
HashMap底层主要依赖数组和链表实现的,它之所以有相当快的查询速度主要是因为它是通过计算哈希值来决定存储的位置。HashMap结合了ArrayList与LinkedList两个实现的优点,虽然HashMap并不会向List的两种实现那样,在某项操作上性能较高,但是在基本操作(get 和 put)上具有稳定的性能。
HashMap在执行添加操作时,有可能发生hash冲突(不同的对象具有相同的hash值)。解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的(将它们存储在同一个bucket位置的链表上)。
当HashMap的容量大于临界值时,会自动扩容2倍。扩容后,计算集合中原有对象的索引值,重新确定索引位置,所以说,HashMap中存储的元素是无序的。
HashMap是线程不安全的。如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。