1.概述
1.1官方介绍
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
关键的信息:基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保证序不随时间变化。
1.2简单实体
public class test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
HashMap<String,Integer> map=new HashMap<>();
map.put("春季",1);
map.put("夏季",2);
map.put("秋季",3);
map.put("冬季",4);
for (Map.Entry<String,Integer> entry:map.entrySet()) {
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
}
春季 1
秋季 3
冬季 4
夏季 2
2.原理
2.1原理图
HashMap采用<Key,Value>键值对的形式存储数据,在提取数据时可以通过对key的比较得到对应的Value的值,默认桶树是16,HashMap中存储数据是通过存入key的hash值进行存储的。
2.2源码分析
(1)构造方法
Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity
设置过大,也不要把load factor
设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor
时就需要调整buckets的数目为当前的2倍。
//在HashMap的构造器中初始化桶的数量和装载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//桶数量的默认值(16),而不是桶内可以装的元素的数量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大桶数,必须为2的n次方,所以最大为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
/*
默认负载系数为0.75,当表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行
再散列(rehashed),可以通过构造函数初始化
*/
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
/*
* 内部static类,对外部不可见,并且保证每个HashMap对象的内部类相同
*/
static class Node<K,V> implements Map.Entry<K,V> {
//存入元素的hash值
final int hash;
//存入元素的key值
final K key;
//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;
}
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;
}
}
(2)Put方法
hashmap底层结构就是数组+链表的结构,如果发生冲突,即hashcode相同key也相同,但是value不同的话,那么就会放在底层数组的同一个下标处,官方话叫同一个桶内,以链表的形式保存。
但是在jdk1.8后就修改这么个存储方式,变成了数组+链表+红黑树的结构。
public V put(K key, V 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;
//hashmap对象中 tabel属性为空--->第一次put---->resize()
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//找到应该保存的位置tab[i],如果没有值,直接存入即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果有值,定义两个变量
Node<K,V> e; K k;
//如果key重复了,即hash值相同,key也相同,直接赋值e=p
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;
}
(3)resize()方法(扩容兼初始化)
扩容机制:当向容器添加元素的时候,会判断容器的元素个数,如果元素个数大于等于阈值的时候(即当前数组的长度×加载因子的值),就需要自动扩容了。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//数组长度
int oldThr = threshold;//临界值
int newCap, newThr = 0;
if (oldCap > 0) {//扩容
if (oldCap >= MAXIMUM_CAPACITY) {
// 原数组长度大于最大容量(1073741824) 则将threshold设为Integer.MAX_VALUE=2147483647
// 接近MAXIMUM_CAPACITY的两倍
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新数组长度 是原来的2倍,
// 临界值也扩大为原来2倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
// 如果原来的thredshold大于0则将容量设为原来的thredshold
// 在第一次带参数初始化时候会有这种情况
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//第一次扩容初始化阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//更新扩容临界值
//创建HASH表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {//已存在hash表
for (int j = 0; j < oldCap; ++j) {//遍历每一个桶
Node<K,V> e;//临时节点遍历,指向旧桶中节点元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//旧桶复制为空
if (e.next == null)//如果取出的节点没有下一个元素,则重新计算对应新hash桶的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//如果是红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //如果是链表
Node<K,V> loHead = null, loTail = null;//旧桶位置
Node<K,V> hiHead = null, hiTail = null;//新桶位置
Node<K,V> next;//下一个节点
do {
next = e.next;//指向下一个节点
//判断当前节点的hash值的比hash表容量高一位的二进制位是否为1,如果为0,则节点保持原桶,如果为1,到新桶
if ((e.hash & oldCap) == 0) {
if (loTail == null)//如果是旧桶的链表尾
loHead = e;//将当前节点设置成链表头
else
loTail.next = e;//当前节点追加到为节点的下一个节点
loTail = e;//将当前节点设置成尾节点
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//新桶位置存在链表
hiTail.next = null;
newTab[j + oldCap] = hiHead;//当前桶位置+旧的hash表容量
}
}
}
}
}
return newTab;
}
常见问题