HashMap与ArrayMap的区别1
Posted 灰灰的Rom笔记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap与ArrayMap的区别1相关的知识,希望对你有一定的参考价值。
看了一下午的 HashMap 的实现原理,感觉很有必要记录下来,防止之后忘记。
HashMap的构成原理
1、HashMap结构
HashMap 当中,存储最终数据的其实是一个 HashMapEntry 类型的数组:
HashMapEntry<K, V>[] table。
而 HashMapEntry 对象其实是属于一种单向链表结构。这样最终其实是构造了一种二维的结构。
我们看一下 HashMapEntry 类:
里面有四个元素,key,value,hash值以及指向的下一个节点的对象。
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry<?, ?> e = (Entry<?, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override public final String toString() {
return key + "=" + value;
}
}
2、查找key对应的对象
知道了 HashMap 是由数组构成的,那么再来看一看 HashMap 根据 key 值查找 value 的时候是如何去实现的。
public V get(Object key) {
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> 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;
}
首先根据 key 值来计算出对应的 hashcode 值。
然后用 hashcode 值去和(数组长度-1)且操作。最终的结果肯定就是一个小于数组长度的某个数字。
比如 hashcode 为 0x0007(16进制),数组长度为 16,这样计算 7&15, 这样计算出来的 index 值就是 7,该对象的值就位于该数组的第7个链表对象当中。
这时候很自然的一个问题就出来了,那么如果我是 0x0017(16进制)呢,这样计算的出来的 index 肯定也是7,那么也就是位于该数组的第7个链表对象中。
这时候为什么使用数组和链表结合的优势就出来了,如果不是链表的话,根据 hashcode 得出相同 index 值的对象就无处存放了。
根据上面的代码
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];e != null; e = e.next)
的可知道,顺序的遍历链表,直到找到直到 key 相同返回。同时我们也可以得出结论,最极端的情况下,找到对应 key 值的元素需要遍历 map.size() 个的链接对象。
同样的原理,在不考虑数组扩容的情况下,添加对象也是一样的流程,根据 hash 值计算出 index 值,然后插入 index 对应的链表当中。
3、添加key对应的对象
首先先看源代码:
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> 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;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
// No entry for (non-null) key is present; create one
之前的代码基本上就是上面所说的,根据 hashcode 计算出 index 然后找到对应的链表。如果存在对应的 key 就直接重新赋值返回。
下面重点讲的就是数组的扩容。
这里顺便提一下 threshold(临界值),初始创建的时候 threshold 为 -1,当首次添加元素扩容之后 threshold 的值为数组长度的 3/4。当 size 大于临界值的时候,数组扩容。
数组扩容,doubleCapacity() 方法。
private HashMapEntry<K, V>[] doubleCapacity() {
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
HashMapEntry<K, V> e = oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> 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;
}
上面这些代码我们一行一行理解。
扩容的时候,首先 new 一个新的数组,数组长度是原来的两倍,下一步就是把老的数组的每一条链表对象添加到新的数组当中去。
这个过程看代码挺复杂的,但是实际理解上还是蛮简单的。
就是一个和数组长度的且操作,然后放到数组对应的位置上。
举例:
如果 hashcode 为 0x0013(16进制),原数组长度 16 的时候就放到第3个,扩容之后数组长度变成了 32。
新数组的 index 位置:(19&15)|(19&16) = 19
。这其实就是newTable[j | highBit] = e;
这一行代码。
那么就会放到新数组的第19位。
4、何时扩容
首先如果初始化的时候没有设置容量 capacity,并且这个数值是大于4小于2的31次方的。则会对这个值进行处理,找到大于该值并且最接近该值的2的N次方,就比如19的话则会设置容量为32。
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
看了这个,自然也就想到了刚开始就设置容量的好处了。
如果我需要一个容纳96个元素的 map,那么只要我把 capacity 初始值设置为128,那么就不会经历16到32到64再到128的三次扩容,这样来说是节省内存和运算成本的。
当然如果需要容纳97个元素的话,因为超过了 capacity 值的3/4,所以就需要设置为256了,否则也会经历一次扩容的。
未完,请看下一篇:HashMap与ArrayMap的区别2
以上是关于HashMap与ArrayMap的区别1的主要内容,如果未能解决你的问题,请参考以下文章
HashMap,ArrayMap,SparseArray源码分析及性能对比