想必对于一个Java开发程序员来说,HashMap用的不少了,当被人问起你了解HashMap的时候,也许你会说:HashMap是线程不安全的,HashMap采取K,V的形式存储,HashMap是高效,强一点的会说,HashMap通过一个Entry<k,v>的内部类实现的,是的,可你真的了解Map吗?你会说Map是通过数组实现的,你或者也会说是通过链表实现的?可它实际上是通过数组和链表实现的,就算你知道这一点,你又知道为什么采取数组和链表去实现的吗?
HashMap(HashMap继承了AbstractMap父类,而AbstractMap父类实现了Map接口。)
下面介绍Map的存储数据的核心内部类entry:
每一个entry是链表结构,为什么这样呢?因为但key计算的hash值一样的话,通过链表的方式来解决冲突。也就是如果计算的hash一样的话就陆续存储在第一值得下一个节点。这样一来,对于不同的hash值的entry链表就组成了一个entry数组,现在明白了hashMap是数组和链表实现的吧。
我们观察源码可以发现:
(注意jdk1.8的HashMap重写了,比1.6的多了尽2000行代码,笔者只是针对1.6的源码分析,而1.8的实现大有不同)
HashMap的基本属性:
/** Map的初始容量 默认是16 以2的次幂扩增
*首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,
*这样便保证了散列的均匀,同时也提升了效率;
*其次,length为2的整数次幂的话,为偶数,
*这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,
*也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,
*而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,
*即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,
*因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
*
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大的容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 装载因子,越大冲突概率越小,但查找效率越低
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 用于计算阈值 实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 用于计算阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
重要:内部类node实现了Map的entry接口:
//实现了Map里的Entry这个接口
static class Node<K,V> implements Map.Entry<K,V> {
//哈希值
final int hash;
//键
final K key;
//键值
V value;
//节点,实际上也是Entry
Node<K,V> next;
//内部类 Entry的构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//得到hashCode
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等,若两个Entry的“key”和“value”都相等,则返回true。否则,返回false
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {//instanceof关键字用于判断一个引用类型变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例。
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
而Map接口的entry是一个内部类(接口):
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
//返回一个以key自然排序
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
//返回一个以value的自然排序
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
存储数据
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数+1
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold) //如果大于临界值就扩容
resize(2 * table.length); //以2的倍数扩容
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//用来将原先table的元素全部移到newTable里面
table = newTable; //再将newTable赋值给table
threshold = (int)(newCapacity * loadFactor);//重新计算临界值
}
读取数据
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
其他方法,计算hash ,计算索引
//计算hash值的方法 通过键的hashCode来计算
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
return h & (length-1); //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
}
HashMap允许key为null的真正原因:
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) { //如果有key为null的对象存在,则覆盖掉
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
return null;
}