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方法都以解析完成,如果文章中有什么不正确的地方还请各位看官指出。