底层数据结构:数组 链表
key—》hashcode&key --》寻址
新元素进来插入到链表头部,新元素的引用存在数组里,next里边存着老元素的地址。
初始化
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始容量 16
* 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
* 加载因子 3/4
static final float DEFAULT_LOAD_FACTOR = 0.75f;
* 当表没有膨胀时,要共享的空表实例
static final Entry<?, ?>[] EMPTY_TABLE = {};
* 根据需要调整表的大小。长度必须是2的次幂。数组
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
* 此映射中包含的键-值映射的数目。
transient int size;
* 调整大小的下一个大小值(容量*加载因子)。阈值
int threshold;
//默认负载因子,是0.75的原因主要是“哈希冲突”和“空间利用率”矛盾的一个折中
//加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
//从源码中的注释可以知道hash桶中元素个数遵循泊松分布,在负载因子为0.75的时候
//桶中元素个数超过8个几乎是不可能的,所以0.75是解决“哈希冲突”和“空间利用率”矛盾比较优的一个值。是一个最优解
final float loadFactor;
*构造方法
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;
threshold = initialCapacity;
init();
}
初始化时只是初始了各种数据,但是没创建数组
添加元素
//添加元素
public V put(K key, V value) {
//第一次向map中添加元素
if (table == EMPTY_TABLE) {
//初始化
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
//保存null值,放入链表首位
return putForNullKey(value);
//将key计算hash值,尽量散列均匀分布
int hash = hash(key);
//计算key的位置,保证下标不会越界
int i = indexFor(hash, table.length);
//hash冲突解决
for (Entry<K, V> e = table[i]; e != null; e = e.next) {//遍历链表
Object k;
//判断hash和equals
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
//后传进来的覆盖之前的节点
e.value = value;
//调用value的回调函数,其实这个函数也为空实现 linkedHashMap里面实现了此方法
e.recordAccess(this);
return oldValue;//返回旧的值
}
}
//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
modCount++;
//插入链表的头部
addEntry(hash, key, value, i);
return null;
}
/**
* Inflates the table.
* 开始添加
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//求2的幂>= toSize, 如果是10,则返回16
int capacity = roundUpToPowerOf2(toSize);
//计算扩容值 12 阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化数组长度 16
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
求2的幂>= toSize
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY
//1 number - 1 先将原有的数字-1,目的是防止16/32/64..经过计算返回32/64/128
//2 << 1右移1位相当于乘以2 目的是经过计算得到的是>=number
//3 highestOneBit方法计算
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
/**
* 将空值放入链表首位 也会去遍历 数组存放的位置 可以有其他元素
*/
private V putForNullKey(V value) {
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容原先的两倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
把当前数组下标上的那个元素 换成新的元素 这个e是之前元素的地址 在新的元素中 作为next
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K, V> e = table[bucketIndex];//获取数组中的entry
table[bucketIndex] = new Entry<>(hash, key, value, e);//重新new 个entry,将旧的e作为next
size++;//size +1
}
对key的计算
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 可以保证在数组大小范围内 而且可以保证平均
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//位与运算,保证当前数组不会越界,减1是为了让高位全是0,低位全是1,不会越界
return h & (length - 1);
}
HashMap 会在 put 元素时,通过元素的 hash 值以及当前数组的长度,来确定一个下标来存放元素。
h & (length - 1); 是控制hash值长度在数组范围内
h ^= k.hashCode(); 是让数值更加平均
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4); 加上这个会更加散列
扩容
void resize(int newCapacity) {
Entry[] oldTable = table;//旧的数组
int oldCapacity = oldTable.length;//旧的数组长度
if (oldCapacity == MAXIMUM_CAPACITY) {//判断是否等于Integer.max
threshold = Integer.MAX_VALUE;//不再扩容
return;
}
Entry[] newTable = new Entry[newCapacity];//创建一个新的数组,是原来的两倍
//将旧的集合放入新的集合,会重新计算hash值
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 将老的数组转移到新的数组
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;//新的数组大小
for (Entry<K, V> e : table) {//双重循环
while (null != e) {//加入链表顺序为1->2->3
Entry<K, V> next = e.next;//next=2
if (rehash) {//是否计算hash,一般是false
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//计算数组下标,两种结果,要么在原有位置,要么原有的下标+oldTable.length
e.next = newTable[i];//= null 或=2
newTable[i] = e; // =1
e = next;
}
}
}
1、resize发生在table初始化, 或者table中的节点数超过threshold值的时候, threshold的值一般为负载因子乘以容量大小.
2、每次扩容都会新建一个table, 新建的table的大小为原大小的2倍.
3、扩容时,会将原table中的节点re-hash到新的table中, 但节点在新旧table中的位置存在一定联系: 要么下标相同, 要么相差一个原table的大小.
4 由于HashMap的扩容需要遍历整个map重新调整每个Entry的位置,比较消耗性能,所以在开发中使用HashMap时,如果已知需要存储的总的Entry数量,我们可以在实例化HashMap时,给定一个合理的初始容量,从而减少map的resize和rehash操作,从而提高程序的运行效率和map的安全性;
5 旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上
6 ModCount作用