HashMap源码解析
// 默认初始容量为16,必须为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// Entry数组,长度必须为2的n次幂
transient Entry[] table;
// 已存储元素的数量
transient int size ;
// 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor
int threshold;
// 加载因子
final float loadFactor ;
HashMap底层是用Entry数组存储数据,同时定义了初始容量,最大容量,加载因子
Entry是HashMap的内部类,它继承了Map中的Entry接口,它定义了键(key),值(value),和下一个节点的引用(next),以及hash值。
Entry是单线链表的一个节点。也就是说HashMap的底层结构是一个数组,而数组的元素是一个单向链表。
自
自己实现的map存在一个问题就是查询时需要遍历所有的key,为了解决这个问题HashMap采用hash算法将key散列为一个int值,这个int 值对应到数组的下标,再做查询操作的时候,拿到key的散列值,根据数组下标就能直接找到存储在数组的元素。但是由于hash可能会出现相同的散列值,为 了解决冲突,HashMap采用将相同的散列值存储到一个链表中,也就
是说在一个链表中的元素他们的散列值绝对是相同的。(hash值相同)
public V put(K key, V value) {
// 如果key为null,调用putForNullKey方法进行存储
if (key == null)
return putForNullKey(value);
// 使用key的hashCode计算key对应的hash值
int hash = hash(key.hashCode());
// 通过key的hash值查找在数组中的index位置
int i = indexFor(hash, table.length );
// 取出数组index位置的链表,遍历链表找查看是有已经存在相同的key
for (Entry<K,V> e = table [i]; e != null; e = e. next) {
Object k;
// 通过对比hash值、key判断是否已经存在相同的key
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 如果存在,取出当前key对应的value,供返回
V oldValue = e. value;
// 用新value替换之旧的value
e. value = value;
e.recordAccess( this);
// 返回旧value,退出方法
return oldValue;
}
}
// 如果不存在相同的key
// 修改版本+1
modCount++;
// 在数组i位置处添加一个新的链表节点
addEntry(hash, key, value, i);
// 没有相同key的情况,返回null
return null;
}
通过将key做hash取得一个散列值,将散列值对应到数组下标,然后将k-v组成链表节点存进数组中。
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);
}
Map中的元素越多,hash冲突的几率也就越大,数组长度是固定的,所以导致链表越来越长,那么查询的效率当然也 就越低下了。还记不记得同时数组容器的ArrayList怎么做的,扩容!而HashMap的扩容resize,需要将所有的元素重新计算后,一个个重新 排列到新的数组中去(copy到新的数组),这是非常低效的,和ArrayList一样,在可以预知容量大小的情况下,提前预设容量会减少HashMap的扩容,提高性能。
再来看看加载因子的作用,如果加载因子越大,数组填充的越满,这样可以有效的利用空间,但是有一个弊端就是可能会导致冲突的加大,链表过长,反过来却又会造成内存空间的浪费。所以只能需要在空间和时间中找一个平衡点,那就是设置有效的加载因子
查找:
public V get(Object key) {
// 如果key等于null,则调通getForNullKey方法
if (key == null)
return getForNullKey();
// 计算key对应的hash值
int hash = hash(key.hashCode());
// 通过hash值找到key对应数组的索引位置,遍历该数组位置的链表
for (Entry<K,V> e = table [indexFor (hash, table .length)];e != null;e = e. next) {
Object k;
// 如果hash值和key都相等,则认为相等
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
// 返回value
return e.value ;
}
return null;
}
在hash值相同的时候,继续判断(k = e.key) == key || key.equals(k),equals方法判断,如果hash方法得出的hash值相同,并且equals方法判断也相同,则返回value。===>引申出问题hashcode()与equals()方法重写的问题。
private V getForNullKey() {
// 遍历数组第一个位置处的链表
for (Entry<K,V> e = table [0]; e != null; e = e. next) {
if (e.key == null)
return e.value ;
}
return null;
}
从删除和查找可以看出,在根据key查找元素的时候,还是需要通过遍历,但是由于已经通过hash对key散列,要遍历的只是发生冲突后生成的链表,这样遍历的结果就已经少很多了,比我们自己写的完全遍历效率提升了n倍。
HashSet解析
// 底层使用HashMap来保存HashSet的元素
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();
HashSet是用HashMap来保存数据,而主要使用到的就是HashMap的key。
一个静态的常量Object类PRESENT来充当HashMap的value,将new出来的Object分配到堆空间(一个空的Object对象占用8byte)从根源上避免NullPointerException的出现,在代码中再也不会写这样的代码if (xxx == null) { … } else {….}
遍历HashSet是基于迭代器,基于HashMap内部类HashIterator
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 取得HashMap底层数组中链表的一个节点
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
// 将next指向下一个节点,并判断是否为null
if ((next = e.next) == null) {
Entry[] t = table;
// 如果为null,则遍历真个数组,知道取得一个不为null的节点
while (index < t.length && ( next = t[index ++]) == null)
;
}
current = e;
// 返回当前节点
return e;
}
该方法主要思路是,首选拿去HashMap低层数组中第一个不为null的节点,每次调用迭代器的next()方法,就用该节点next一下,当当前节点 next到最后为null,就拿数组中下一个不为null的节点继续遍历。什么意思呢,就是循环从数组第一个索引开始,遍历整个Hash表。