HashMap 数据结构
数据结构中有数组与链表两种模式,但是这两种模式都存在一定的缺陷
数组
数组:数组的存储区域是连续的,占用内存比较严重,空间复杂度比较高,但是数组在查找时是比较简单的。数组特点:查找容易,删除、插入比较复杂
链表
链表:链表的存储区域是不连续的,占用内存比较宽松,但是当在链表中查找某一个元素时是比较复杂的。链表特点:删除、插入比较简单,查找比较复杂。
HashMap是一种数据存储的容器,并且很好的将数组与链表结合使用
到这里读者不免有些疑惑,HashMap在存储过程中是如何将数组与链表结合使用的?
HashMap存取实现
在看HashMap存储室如何实现之前,我们需要先看一下HashMap的一个内部类:HashMapEntry
HashMapEntry
static class HashMapEntry implements Entry {
final K key;
V value;
final int hash;
HashMapEntry next;
HashMapEntry(K key, V value, int hash, HashMapEntry next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
//获取HashMao存储的key值
public final K getKey() {
return key;
}
//获取HashMap存储的value值
public final V getValue() {
return value;
}
//设置 value 值 并将 oldValue值返回
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
...
}
HashMapEntry内部含有四个变量分别是:
K key:HashMap数据存储时 key值
V value:HashMap数据存储时value值
int hash:key的hashCode通过位运算生成的int值
HashMapEntry
put
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
在put时首先判断key是否为null
当key值为null时,直接执行 putValueForNullKey(value);
private V putValueForNullKey(V value) {
HashMapEntry entry = entryForNullKey;
if (entry == null) {
addNewEntryForNullKey(value);
size++;
modCount++;
return null;
} else {
preModify(entry);
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
直接将 entryForNullKey 赋值给 entry ,对于 entryForNullKey
transient HashMapEntry entryForNullKey;
赋值是在 addNewEntryForNullKey(value) 方法内进行赋值,所以第一次执行 putValueForNullKey 方法时,entry 为null,直接执行addNewEntryForNullKey 方法,在改方法内;
void addNewEntryForNullKey(V value) {
entryForNullKey = new HashMapEntry(null, value, 0, null);
}
初始化 entryForNullKey ,此时 传入的key、HashMapEntry 为null,hash值为 0;同时 size++、modCount++;
当entry不为null时,执行 preModify(entry) 方法,将value值赋值给当前entry 并 获取oldValue 将oldValue返回。
关于preModify(entry) 方法内是如何执行,稍后再看。
当key不为null时:
直接从put方法的第四行代码开始看,先来关注一下三行代码:
int hash = Collections.secondaryHash(key);
HashMapEntry[] tab = table;
int index = hash & (tab.length - 1);
第一行 将key值通过secondaryHash 得到一个hash值,该值是如何计算:
public static int secondaryHash(Object key) {
return secondaryHash(key.hashCode());
}
private static int secondaryHash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
第二行 将 table值赋值给tab数据,table值具体是多少,在HashMap初始化时,
private static final Entry[] EMPTY_TABLE
= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
private static final int MINIMUM_CAPACITY = 4;
public HashMap() {
table = (HashMapEntry[]) EMPTY_TABLE;
threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}
通过 上述代码得到tab时一个长度为2 的数组
第三行代码 通过 hash & (tab.length - 1) 获得index 值,本人通过测试得到 index = 0;
接下来分析put方法 之后执行的代码:
for (HashMapEntry e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
在for循环中获取数组中的每一个 HashMapEntry对象,当该对象不为null的情况下,获取该HashMapEntry对象中的存储的下一个HashMapEntry对象。在循环内部存在一个判断,当HashMapEntry对象与存储的HashMapEntry对象 hash值以及key值想等的情况下 执行preModify(e); 方法,该方法稍后分析,在执行完 preModify(e); 方法之后,赋值value值,并返回oldValue
当for循环完成或者 e为null之后 执行下面代码:
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
第六行 通过addNewEntry方法将 HashMapEntry放到 数组 table[index]位置 具体实现:
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry(key, value, hash, table[index]);
}
在addNewEntry 方法内有两种情况:
1、当该数组位置为null情况下,直接将HashMapEntry存放在该位置,同时在该HashMapEntry中存放一个null HashMapEntry;
2、当该数组位置不为null情况下,将该HashMapEntry存放在该位置,同时在该HashMapEntry中存放之前该位置的HashMapEntry
这样在数组同一个位置就形成了一条链式结构。
具体的存储如上图所示。
当e.hash == hash && key.equals(e.key) 或者entry != null时
代码中执行了preModify(entry);方法,
HashMap中该方法是一个空实现:
void preModify(HashMapEntry e) { }
具体实现是在 LinkedHashMap
@Override void preModify(HashMapEntry e) {
if (accessOrder) {
makeTail((LinkedEntry) e);
}
}
只有当accessOrder 为true的情况下 执行 makeTail 该方法
在LinkedHashMap中 当 accessOrder false: 基于插入顺序 为 true: 基于访问顺序
而accessOrder 在默认情况下为false,这也就导致默认情况下preModify 该方法中并没有执行makeTail 方法。所有在HashMap put方法中并不需要太关注 preModify 方法。
那么当e.hash == hash && key.equals(e.key) 或者entry != null 时HashMap是如何存放数据的,则需要重点关注一下三行代码:
V oldValue = e.value;
e.value = value;
return oldValue;
着重看第二行,当出现hash以及key相等情况下,则用新的value值覆盖oldValue值,并将oldValue值返回。
到这里可以看到HashMap的存储是将HashMapEntry存放到数组中,存放位置与HashMapEntry中key值的hashCode值相关,当两个HashMapEntry的hashCode值相同时,会将该两个HashMaEntry以链表形式存储。
那么现在出现一个问题:数组的长度是固定的,HashMap在存储时是不知道多少HashMaoEntry需要存储的。
针对上述问题,可以有两种方式:
1、定义一个最大长度的数组
2、随着HashMapEntry的数量动态改变数组长度
针对第一种方案显然是不合适的,因为定义最大长度数组需要占用很大的内存,google 显然不会这么做。
通过源代码可以看出google 显然是采取的第二种动态改变数组的方案解决存储数组大小问题的。
private transient int threshold;
transient int size;
public HashMap() {
table = (HashMapEntry[]) EMPTY_TABLE;
threshold = -1;
}
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
在构造HashMap时,threshold 默认为 -1;
当调用put方法时,第七行 size > threshold 为true,接下来执行
doubleCapacity方法。
private HashMapEntry[] doubleCapacity() {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
HashMapEntry e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry broken = null;
newTable[j | highBit] = e;
for (HashMapEntry n = e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash & oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
第六行可以看出数组的长度变化每次增加时 进行翻倍。且数组长度有一个最大值:MAXIMUM_CAPACITY 该值为:1 << 30
第八行 执行了一个方法 makeTable 在该方法中可以看出:
private HashMapEntry[] makeTable(int newCapacity) {
@SuppressWarnings("unchecked") HashMapEntry[] newTable
= (HashMapEntry[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}
内部new出一个新的数组,并对threshold 进行赋值。经测试当 newCapacity = 4、8、16、32…. 时, 该threshold值为 newCapacity *3/4;
到这里结合 HashMap put方法中 当 size++ > threshold 时对数组进行扩充,可以想到,HashMap在存储时并非是当此时数组已经存储满之后在扩充,而是当数组中存储的数据达到当前数组的3/4时 进行数组扩充。
在这里的到一个 3/4值,该值在HashMap中实质是默认加载因子;
在HashMap变量中其实已经提到:
/** * The default load factor. Note that this implementation ignores the * load factor, but cannot do away with it entirely because it's * mentioned in the API. * *
Note that this constant has no impact on the behavior of the program, * but it is emitted as part of the serialized form. The load factor of * .75 is hardwired into the program, which uses cheap shifts in place of * expensive division. */ static final float DEFAULT_LOAD_FACTOR = .75F;
ok 到这里 HashMap是如何存储数据的相信大家已经明白了,接下来看一下HashMap中是如何get数据的。
get
public V get(Object key) {
if (key == null) {
HashMapEntry e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry[] tab = table;
for (HashMapEntry e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
get方法很简单
当传入key为null时,获取到entryForNullKey 该对象,并判断该对象是否为null 如果为null 则直接返回null,否则 返回该 HashMaEntry的value值。
当key不为null时,可以看到与put方法中一样,通过secondaryHash 方法得到hash值,可以看到该值与put时该hash值是一样的,得到该值之后,然后遍历数组table 当eKey == key || (e.hash == hash && key.equals(eKey))时,将该key值对应的value值返回,否则返回一个null。
到这里 HashMap中常用的get以及put方法都以解析完成,如果文章中有什么不正确的地方还请各位看官指出。