1.类结构
继承了AbstractMap抽象类。
2.数据结构
常用的数据结构:
-数组:占用连续的内存空间,通过索引来查询数组数据,故时间复杂度为O(1),但是在非头尾元素的新增、删除操作都会在内存中进行元素的移位(复制,清除),效率低。
-链表:占用非连续内存空间,由一系列节点组成。节点中分为两部分:数据域,下一个节点的地址Next指针。时间复杂度为O(N),需要从头尾节点开始逐个查找。对于新增、删除操作具有很高的效率,只需要更改节点的Next指针
-二叉树:对一颗相对平衡的有序二叉树,对其的拆入、查找、删除操作,平均复杂度都为O(logn)
HashMap采用哈希表(数组+链表)数据结构存储Entry类。Entry类是HashMap中的一个静态内部类,是实现存储K-V的基本。
3.如何计算存储位置
先熟悉一下HashMap中的主要变量
/** 默认初始容量大小/16
* 必须为2个幂次方,在下面会说明*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/** HashMap最大容量2^30*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 默认加载因子(HashMap扩容的一个系数)*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** HashMap 大小*/
transient int size;
/** 一个临界值(当HashMap的size达到该值时,会触发ReHash/扩容*/
int threshold;
/**加载因子*/
final float loadFactor;
/**ReHash次数*/
transient int modCount;
在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);
// 进行位运算判断,确保初始容量为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();
}
确保初始容量为2的幂次方,这对HashMap的hash算法实现很重要。
Hashmap的存储算法:
/**HashMap键hash算法*/
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 得到在Hash表中的存储索引
* h: hash(K) length: 当前hash表的长度(2的幂次方)
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
为什么需要容量一定是2^n呢?当length为2的幂次方时:h & (length - 1) == h % length,这个是等价的,但是位预算更加的快速。
那么当俩个K值得hash值相等时,会形成hash碰撞,产生一个链表,后存储的K值将会作为链表的头结点存储在数组中,同时该节点Next指针指向另一个K值。
那么问题来了?:
大量的hash碰撞会形成大量长的链表,对于hashmap的查询效率产生很大影响?
- 确实会有很大的影响,那么HashMap是怎么解决的呢!
- ReHash操作:当hashmap中存储的数据个数>=当前容量大小*加 载因子 。会触发ReHash操作,将容量进行翻倍,重新对集合中的数据进行hash定位存储操作。减少了hash碰撞,但产生了重新定位的开销。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果rehash会超过最大值容量,直接取最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//设置新的临界点(rehash点)
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**将当前的所有Entry对象存储到新的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;
}
}
}
- 所以初始化时选择一个合适的初始容量很重要。
在Java8的HashMap结构有些改变,当链表长度达到默认值8时,会使用红黑树进行存储。有兴趣可以自己结合看看源码。