一. HashMap概述
HashMap是Java程序员用于映射(键-值对)处理的最常用数据类型。随着JDK(Java Developmet Kit)的更新,JDK 1.8使用数组+链表+红黑树优化了HashMap底层的实现。当链表的长度超过阈值(8)时,链表将转换为红黑树,从而大大减少了搜索时间。
二. HashMap继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
您可以看到HashMap继承自父类AbstractMap, 实现了Map<K,V>,Cloneable, Serializable接口。Map接口定义了一组常规操作;Cloneable接口意味着可以复制它;在HashMap中,它实现了浅表副本,即对复制对象的更改将影响复制对象;Serializable接口意味着HashMap已被序列化,即HashMap对象可以在本地保存然后还原。
三. 类的属性
默认的初始容量-必须为2的幂。初始容量为16。
注意:HashMap可以指定初始容量,如果指定的初始容量不是2的幂,则会自动转换为2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
例如:
new HashMap<>(20, 0.8); //实际为 new HashMap<>(32, 0.8);
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当存储桶上的节点大于等于此值时,它将变成一颗红黑树
static final int TREEIFY_THRESHOLD = 8;
当存储桶上的节点小于等于此值时,将由树变回链表
static final int UNTREEIFY_THRESHOLD = 6;
从存储桶中的结构转换对应于红黑树的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
存储元素的数组,始终为2的幂
transient Node<K,V>[] table;
存储一组特定的元素,以迭代元素
transient Set<Map.Entry<K,V>> entrySet;
存储元素的数量,请注意,这不等于数组的长度
transient int size;
针对地图结构的每次扩展和更改进行计数的计数器
transient int modCount;
当实际容量(初始容量*负载因子)超过此阈值,容量将扩大
int threshold;
负载因子
final float loadFactor;
四. 链表节点Node
static class Node<K,V> implements Map.Entry<K,V> {
//存放元素key的hash值
final int hash;
//存放元素的key
final K key;
//存放元素的value
V value;
//指向链表中下一个Node
Node<K,V> next;
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; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//覆盖链表中元素的时候返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法,只有当key和value都相等时返回true
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
其中Node节点中存放一个hash字段来记录hash值而不是每次使用的时候再计算是因为每个Node的hash值都需要经过扰动函数的扰动,并且扩容的时候需要计算每个Node的hash值,所以出于空间换时间的想法,在Node节点中加入了hash字段。
五. 构造函数
- public HashMap(int , float )构造函数
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不应小于0,否则将报错。
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不应大于最大值,否则为最大值。
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不应小于或等于0。
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化负载因子
this.loadFactor = loadFactor;
//初始化阈值大小
this.threshold = tableSizeFor(initialCapacity);
}
注意:tableSizeFor(初始容量)将返回大于初始容量的最小二次幂值。
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;
}
这里涉及到位运算,用于计算给定大小的cap大于或等于cap的最小2的幂。乍看之下,五个连续的右移操作毫无意义,但是当您仔细考虑二进制系统为0和1时,就会出现问题。
第一个右移意味着1右边的每个位置都变为1,第二个右移意味着1的最后位置已经变成两个连续的位置。接下来的五个操作仅使int成为32位大写,是否会感到惊讶?
翻转之后,所有的数字都根据最大的1的位置变成1,然后是n+1,否则就是2的幂。这里需要注意的另一点是第一行中的cap-1,因为如果cap本身是2的幂,它将导致两倍的cap,并浪费空间。
- public HashMap(int )构造函数
//构造一个空的HashMap,具有指定的初始容量和默认的加载因子(0.75)。
public HashMap(int initialCapacity) {
//调用HashMap(int,float)构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- public HashMap()构造函数
//构造一个空的HashMap,具有默认的初始容量(16)和默认的负载因子(0.75)。
public HashMap() {
//初始化负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- public HashMap(Map<? extends K, ? extends V> m)构造函数
//构造一个新的HashMap与指定的Map相同的映射。HashMap是用默认的负载因子(0.75)创建的,
//初始容量足以容纳指定的Map中的映射。
public HashMap(Map<? extends K, ? extends V> m) {
//初始化负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m中的所有元素添加到HashMap
putMapEntries(m, false);
}
说明:putMapEntries(Map, m, evict) 函数将m的所有元素存储到此HashMap实例中。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//确定表是否已初始化
if (table == null) { // pre-size
//未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果计算出的t大于阈值,则获取初始化阈值。
if (t > threshold)
threshold = tableSizeFor(t);
}
//初始化,并且m个元素的数量大于阈值,则执行扩容方法。
else if (s > threshold)
resize();
//将m中的所有元素添加到HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
这里将不再展开,为避免篇幅过长,将hashmap的resize(),get(),put()等常用方法,写到下面这篇↓
链接: HashMap源码 二
参考链接:
链接: HashMap源码分析,基于1.8对比1.7
链接: JDK1.8 HashMap源代码分析