能力要进阶,必然就要多了解了解原理。看了一些Java的数据结构,简单的总结下HashMap原理。
所谓map就是一个个key、value对,value允许重复,key必须唯一;那HashMap从字面上看就是加入了Hash的Map。那HashMap到底是什么类型呢?
HashMap的底层结构是由数组和链表组成;每个对象都是Map.Entry对象,Entry存储着:hash、key、value、next(下一个Entry指针)
数组:寻址快,但是不方便删除和插入; 链表:方便删除和插入,但是遍历速度慢;
两者结合,就有事情发生。
HashMap初始化的时候是分配默认大小为16的数组table,每个数组里存放着Entry对象。最多的操作就是put() 和 get();
说说put(k,v):
1. 拿到key之后,先会计算一下key的hash值 ; 2. 再用这个hash值%(模)数组的长度length-1,这样就可以得到一个不超过数组长度的坐标i; 3. 这个坐标就是这个value要保存在数组上的位置。一般的数组保存都是table[i]=entry(hash,key,value,next);但是不能保证index=hash%length后就一定都不一样,所以这种直接等于的方式就会覆盖,这种写入是不可取的;(index相同就是数据碰撞) 4. 那同一个坐标上的entry就用一个链表list保存,数组上保存最新插入的entry即链表头;先记录oldEntry=table[i],然后将table[i]=newEntry ,然后用newEntry.next指向oldEntry;这里oldEntry与一个个newEntry就形成了一个链表;
当然这只是大流程,这里还有很多细节和逻辑,后面代码再续。
get()就更好理解了,通过key的hash值找到链表,再遍历列表,如果hash和key相等,就返回这个entry的value;
下图:纵列0~15是数组(默认16个),每个横是一个链表,如496是一个entry;
大体了解后,我们就一起看看代码,在此之前再说几点细节:
1. 为了快速遍历,就会尽可能的将entry保存在数组上(每个链表尽可能只有一个节点),所以当所有元素数快达到数组大小的时候,就会扩大数组;
2. 为了优化查找速度,数组大小一般是2的n次幂;……
初始化函数:initialCapacity 指定初始化大小(默认16),loadFactor 实际容纳元素因子默认0.75
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
//注意这个循环,设置一个2的n次幂数,大于指定初始值,后续再讲为何是2的n次幂
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//threshold 即当前hashmap的最大容量
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//table 即初始化的数组
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
通过初始化函数,可以看出本质就是一个数组。
说下存储数据的元素:
static class Entry implements Map.Entry {
final K key; //键
V value;//值
Entry next;//下一个entry指针
final int hash;//hash值
……
}
认识下put(key,value)源码:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
*/
public V put(K key, V value) {
//其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
if (key == null)
return putForNullKey(value);
//通过调用hash方法对key进行哈希,得到哈希之后的数值。该方法实现可以通过看源码,其目的是为了尽可能的让键值对可以分不到不同的桶中
int hash = hash(key);
//根据上一步骤中求出的hash得到在数组中是索引i
int i = indexFor(hash, table.length);//这个很重要,看后面函数说明
//如果i处的Entry不为null,则通过其next指针不断遍历e元素的下一个元素,如果找到相同hash和key则进行数据替换,put结束
for (Entry 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;
}
//hash函数,对k的hashcode进行高位运算,减少hash冲突
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
//得到k的hashcode值
h ^= k.hashCode();
//进行计算
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
//这个函数就非常巧妙的用&代替了%运算,因为数组的大小总是2的n次幂;(详见初始化函数)
//&运算速率高于%运算
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//size记录了当前hashmap的元素数,在createEntry函数有说明;
//threshold是当前hashmap最大容量,由capacity*loadFactor计算来,详见初始化函数;
//当达到上限时进行扩容,扩容是非常耗资源的,所以初始化尽量合适的大小
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);
}
//在bucketIndex位置上创建entry
void createEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry.next 指向原来的 Entry
table[bucketIndex] = new Entry<>(hash, key, value, e);//最新的entry当链表头
size++;
}
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//创建一个新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//将Old Hash Table上的数据迁移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中,原顺序上1234会变成4321,当然因为扩容了,所以不会4个仍在一起。
for (int j = 0; j < src.length; j++) {
Entry e = src[j];
if (e != null) {
src[j] = null;
do {
Entry next = e.next;//
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];// 这个是导致多线程死循环的根源!!!
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
多线程不安全的原因:比如一个链条上有entry1->entry2->entry3,当两个线程都在执行扩容时,假设entry1和entry3又都hash到了同一个链表上,线程一获取了entry1后挂起,线程二顺利完成扩容,即entry1->entry3,当线程一继续执行后,entry1.next=newtable[i] 即entry3;此时就产生了entry1.next=entry2,entry2.next=entry1死循环!!!
参考:https://coolshell.cn/articles/9606.html
//读取比较好理解
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
*
A return value of {@code null} does not necessarily
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry 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;
}
Fast-Fail机制
还记得modCount参数么,每次put都会+1,这里就相当于hashmap的一个版本号,迭代过程中就会检测是否有变化,如果有就会抛出异常。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
final Entry nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
再说下遍历方式:
//第一种比较高效
Map map = new HashMap();
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
//第二种相对较低
Map map = new HashMap();
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
Object key = iter.next();
Object val = map.get(key);
}