HashMap具体实现
HashMap默认的初始容积大小为16,加载因子默认0.75,threshold阈值为【容积*加载因子】
HashMap采用的是链表法解决哈希冲突问题,同时引入红黑树可以避免单个链表长度过长的问题
默认8将单向链表转换为红黑树,注意这里还有一个条件,默认64,只有集合中的结点数大于64时才可能进行树化处理
默认6将红黑树退化成链表
hash函数的涉及需要考虑简单高效和分布均匀两个方面,所以首先获取对象的hashCode值,然后要将hash值的高位和低位进行与运算后再针对数组长度进行求余。
HashMap线程不安全,进行多线程操作时可能会出现扩容时执行rehash操作的死循环问题、脏读问题和size值不精确的问题
类定义
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable, Serializable{
具体内部存储方式
transient Node<K,V>[] table; //哈希表的本质就是一个数组,数组中每个元素称为一个桶,桶中存放的是键值对构成的单向链表或者红黑树
transient定义数据的存储,HashMap类中提供了序列化与反序列化操作的方法writeObject和readObject,以提高序列化处理的执行效率
private void writeObject(java.io.ObjectOutputStream s)throws IOException{}
private void readObject(java.io.ObjectInputStream s) throws IOException,ClassNotFoundException{}
重要的阈值
static final int TREEIFY_THRESHOLD = 8;//树化阈值,就是链表转换为红黑树的阈值,当一个链表中存储的元素个数大于8时,会自动将链表转换为红黑树
static final int UNTREEIFY_THRESHOLD = 6;//桶的红黑树蜕化为链表的还原阈值。当红黑树中结点数小于6时会自动将红黑树转换为链表。注意:如果结点数较小时,红黑树的总体性能不一定优于单向链表
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化处理的容积阈值。当哈希表中的容量>64时才允许进行树化处理
内部存储的实现
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //缓存的hash值
final K key; //具体存放的key值
V value; //具体存放数据的value值
Node<K,V> next; //单向链
构造器
HashMap采用的是懒加载lazy-load机制,也就是在构建HashMap对象时,并不直接构建数组,而是第一次添加元素时才进行数组的初始操作。HashMap的无参构造器采用所有的默认配置值,容积默认值为16,加载因子默认0.75,扩容阈值threshold此时实际上是12。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
允许在构建hashMap对象时,手工设置其中的初始化容积值initialCapacity
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
tableSizeFor方法用于获取合理的数组的初始化容积值,返回的是一个大于等于初始化容积值的2的n次方值。
输入10返回的是16,返回值必须是2的多少次方
输入7返回的是8
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n +1;
}
HashMap的存储结构
每个Node[]数组中的元素被称为桶bucket,一个桶对应一个hash映射的值,例如0、1、2、3..,注意相同的hash映射值可能会出现不同的key值【鸽巢理论】,所以针对桶采用链表结构存储hash映射值相同的所有数据【JDK1.8在单个链表长度大于阈值8时会自动转换位红黑树;当删除结点使某个红黑树中节点数小于阈值6时会自动从红黑树蜕化成链表结构】
相关参数
capacity容积值标识当前输入的容量,始终保持是2的n次方,可以扩容,扩容后的数组大小为原始数组大小的2倍。
loadFactor负载因子,默认是0.75,注意这个值在使用链表法的实现中可以大于1。
threshold扩容阈值,等于capacity*loadFactor。
JDK1.7版本:
hashmap底层采用的是Entry数组和链表实现的。
向一个链表中添加数据时采用的是头插法,在多线程并发操作中会有环形链的问题。
JDK1.8版本
hashmap使用Node类型的数组和链表以及红黑树实现的,Node用于链表,红黑树使用的是
TreeNode。
树化处理
向一个链表中插入数据采用的是尾插法,可以避免环形链问题,但是并不是解决了线程安全问题,仍旧会有扩容处理的rehash时的死循环问题、脏读丢失数据问题、size不准确问题
HashMap中存储的是key-value,其中key不允许重复(keySet():Set),但是value可以重复
(values():Collection),允许null的key和value,但是key只能有一个,value没有限制。HashMap线程不安全,如果多线程使用时可以考虑使用ConcurrentHashMap或者使用
Collections.synchronizedMap(map)将HashMap转换为线程安全的map
注意:作为key值出现的类型必须实现equals和hashCode两个方法,要求equals为true时则hashCode码值必须相等。建议可以考虑使用IDE工具自动生成equals和hashCode两个方法
put方法的具体实现流程
public V put(K key, V value) { // 向hashmap集合中添加一个key/value对数据
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// 1、如果table为空或者长度为0,那么调用resize方法扩容数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、计算插入数据存储到数组的对应索引值,如果数组为空则不存在hash冲突,则直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0;; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize()方法的说明:将table大小初始化或加倍。如果为null,则按照默认值初始数组(16、0.75)。否则,使用2倍扩容处理,因为每个容器中的元素必须保持在相同的索引中,或者移动在新表中具有二次方偏移