目录
1. HashMap简介
2. HashMap的底层结构
3. HashMap源码分析
4. HashMap的扩容机制
5. HashMap的性能
1、HashMap简介
- HashMap是一个用于存储(key-value)结构的散列表
- ,继承了AbstractMap,实现了Map、Cloneable、java.io.Serializable 3个接口
- HashMap的
key
和value
都是可以为null
的 - HashMap是线程不安全的,如果在高并发情况下想要保证线程安全,可以考虑使用HashTable或者currentHashMap。
- currentHashMap效率会比HashTable高。
2、HashMap的底层结构
- HashMap底层数据结构是数组+链表
- 其能够有相当快的查询速度(时间复杂度为O(1) )是因为对
key
的hashcode
使用hash算法进行运算,得出存储在数组中的下标 - 如果储存的对象一旦多起来了,就有可能导致hash冲突,即数组下标重复 。HashMap为了解决这个问题,采用了链表的结构
- 如果最新插入的Entry 的数组下标中已经存有数据了,则把该位置让出来给新插入的Entry并让其指向上一个Entry节点。
3、HashMap源码(jdk1.7)
3.1主要属性
DEFAULT_LOAD_FACTOR
(加载因子),当加载因子越小的时候,数组利用率会越低,HashMap的hash冲突就会越低,即entry链表长度越短,查找效率越高。反之,加载因子越大,数组利用率会越高,HashMap的hash冲突就会越多,即entry链表长度越长,查找效率越低。
//存储entry数组的默认初始容量 ,为2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认的最大容量 为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子,数组使用率到达75%的时候就扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当数组表还没扩容的时候,一个共享的空表对象
static final Entry<?,?>[] EMPTY_TABLE = {};
//内部数组,用来装entry,大小只能是2的n次方。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//存储Entry<K,V>的个数
transient int size;
/**
* 扩容的临界点(加载因子*数组容量),如果当前容量达到该值,则需要扩容了。
* 如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
*/
int threshold;
//构造函数传进来的加载因子
final float loadFactor;
//Hash被修改的次数
transient int modCount;
//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//计算hash值时候用,初始是0
transient int hashSeed = 0;
//含有所有entry节点的一个set集合
private transient Set<Map.Entry<K,V>> entrySet = null;
3.2 Entry类分析
Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//多个Entry是构成单向链表结构,数组中存储的是一个个entry所构成的链表,next是指向下一个entry节点
Entry<K,V> next;
//用于记录本entry节点的hash值
int hash;
//初始化节点
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//获取节点的key
public final K getKey() {
return key;
}
//获取节点的value
public final V getValue() {
return value;
}
//设置新value,并返回旧的value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断传入节点与此结点是否相等,如果相等则返回true,反之返回false
public final boolean equals(Object o) {
//传入对象不是Entry,就返回false
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;
}
//根据key和value生成hashCode
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//每当相同key的value被覆盖时被调用一次,在HashMap的子类LinkedHashMap中实现了这个方法
void recordAccess(HashMap<K,V> m) {
}
//每移除一个entry就被调用一次,在HashMap的子类LinkedHashMap中实现了这个方法;
void recordRemoval(HashMap<K,V> m) {
}
}
3.3 构造器分析
HashMap调用构造方法的时候,参数为非具体值并不会创建容器。当传具体值的时候,会创建一个内部数组(table数组)。默认初始容量是16,在3.1主要属性
中的代码块中有所体现,字段为DEFAULT_INITIAL_CAPACITY
。
/**
* 生成一个空HashMap,传入容量与加载因子
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//初始容量不能大于默认的最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能小于0,且不能为NaN(Not a Number)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//把加载因子赋值给属性
this.loadFactor = loadFactor;
//设置临界值
threshold = initialCapacity;
//该方法只在LinkedHashMap中有实现,主要在构造函数初始化和clone、readObject中有调用。
init();
}
/**
* 生成一个空hashmap,传入初始容量,加载因子使用默认值(0.75)
* @param initialCapacity 初始容量
*/
public HashMap(int initialCapacity) {
//生成空数组,并指定扩容值
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 根据已有map对象生成一个hashmap,初始容量与传入的map相关,加载因子使用默认值
* @param m Map对象
*/
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);
//将传入map的键值对添加到初始数组中
putAllForCreate(m);
}
3.4 存储分析(put)
put方法和get方法是HashMap中最常用的方法,下面对put方法进行分析。
如果key为null的话,hash值为0,对象存储在table中index为0的位置(table[0])。当K不为null时,会先计算出K的hash值,然后通过hash值与table的length映射出一个下标i,这个i就是Entry存放在数组的位置。然后遍历table[i]的Entry,如果有相同K的,就替换新的value,返回oldvalue,结束。反之把新的Entry插入链表头部。
put算法流程图如下
put源码
/**
* 存入一个键值对,如果key重复,则更新value
* @param key 键值名
* @param value 键值
* @return 如果存的是新key则返回null,如果覆盖了旧键值对,则返回旧value
*/
public V put(K key, V value) {
//如果数组为空,则新建数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,则把value放在table[0]中
if (key == null)
return putForNullKey(value);
//生成key所对应的hash值
int hash = hash(key);
//根据hash值和数组的长度找到:该key所属entry在table中的位置i
int i = indexFor(hash, table.length);
/**
* 先找到i位置,然后遍历entry,
* 如果发现存在key与传入key相等,则替换其value。返回oldvalue
* 如果没有找到相同的key,则继续执行下一条指令,将此键值对存入链表头
*/
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;
}
}
//map操作次数加一
modCount++;
//扩容检测和添加entry
addEntry(hash, key, value, i);
return null;
}
3.5获取源码分析(get)
下面是put的分析
get方法用于获取出入的参数Key所对应的value,get方法会先对key进行判断,如果为null就执行getForNullKey(),否则就通过getEntry()来找到entry,再通过entry获取value。其中getForNullKey()和getEntry()的算法流程如下
/**
* 根据key找到对应value
* @param key 键值名
* @return 键值value
*/
public V get(Object key) {
//如果key为null,则从table[0]中取value
if (key == null)
return getForNullKey();
//如果key不为null,则先根据key,找到其entry
Entry<K,V> entry = getEntry(key);
//返回entry节点里的value值
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
//查找table[0]处的链表,如果找到entry的key为null,就返回其value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
/**
* 根据key值查找所属entry节点
* @param key 键值名
* @return entry节点
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//如果key为null,则其hash值为0,否则计算hash值
int hash = (key == null) ? 0 : hash(key);
//根据hash值找到table下标,然后迭代该下标中的链表里的每一个entry节点
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;
}
4、HashMap的扩容机制
当HashMap存储的元素越来越多的时候,hash冲突就会越来越多,查询效率就会越来越低。因为定位到table[i]的时候,需要遍历table[i]中的链表,冲突越多,链表的长度就会越长,所以查询的效率就越慢。为了提高效率,就需要对table进行扩容处理,也就是进行resize(),resize()这个操作也会出现在ArrayList中。
言归正传,resize是HashMap中最消耗性能的一个操作,在resize中,原数组中的所有数据会重新计算一次其在新数组中的位置,并存放进去。当HashMap在进行put操作的时候,检测到table的使用量达到扩容的临界threshold
的时候,就会进行扩容操作(threshold= table.length * DEFAULT_LOAD_FACTOR)
/**
* 对数组扩容,即创建一个新数组,并将旧数组里的东西重新存入新数组
* @param newCapacity 新数组容量
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果当前数组容量已经达到最大值了,则将扩容的临界值设置为Integer.MAX_VALUE(Integer.MAX_VALUE是容量的临界点)
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);
}
/**
* 将现有数组中的内容重新通过hash计算存入新数组
* @param newTable 新数组
* @param rehash
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历现有数组中的链表
for (Entry<K,V> e : table) {
//查找链表里的每一个entry
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//根据新的数组长度,重新计算此entry所在下标i
int i = indexFor(e.hash, newCapacity);
//将新的entry插入链表头部
e.next = newTable[i];
newTable[i] = e;
//查看下一个entry
e = next;
}
}
}
5、HashMap性能
HashMap中,加载因子是衡量的是一个散列表的空间的使用程度,加载因子越大表示散列表的使用率越高,HashMap的hash冲突就会越多,即entry链表长度越长,查找效率越低。当加载因子越小的时候,数组利用率会越低,HashMap的hash冲突就会越少,即entry链表长度越短,查找效率越高。HashMap查找一个元素的效率为O(1+avg(entry.length)),即O(1)。经研究发现,加载因子选取0.75是比较合适的
PS:博文中如有什么不对的地方恳请大家指出,谢谢~