JDK1.7里的HashMap采用的是数组和链表的数据结构
首先,该类继承于AbstractMap实现规定的一些方法,Cloneable支持克隆,Serializable序列化
先从类的一些成员说起:
-
loadFactor
装载因子,是用来衡量 HashMap 满的程度,计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是 table 的长度length。 -
threshold
计算公式:capacity * loadFactor。这个值是当前已占用数组长度的最大值。过这个数目就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 -
modCount 修改次数 保证数据一致性 防止写冲突
-
DEFAULT_INITIAL_CAPACITY 1<<4 aka 16
构造函数
提供四种构造方法
构造时,提供capacity和loadFactor双参构造,无参构造时,capacity默认是16,loadFactor是0.75f.且capacity有个最大值1<<30
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();
}
要注意,这时的构造并没有产生Entry[]数组,而是在第一次执行put元素时才产生Entry[]数组
put方法
public V put(K key, V value) {
//如果数组为空,会执行对数组的初始化操作
//为空的原因是该数组并不会在new HashMap()时就产生,而是在第一次
//执行put操作时才会产生 inflateTable()方法后面会讲
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//hashmap支持key为null的put操作
if (key == null)
return putForNullKey(value);
//根据key计算出hash值 该hash()方法后面也会讲到
int hash = hash(key);
//根据hash值和数组长度取得一个下标 indexFor()方法后面也会讲到
int i = indexFor(hash, table.length);
//该循环目的是遍历数组第i个下标的链表
//目的是找到与key相同的Entry并覆盖旧的value 并且返回旧value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果是个新key则采用头插法加入到该链表中,该方法后面会讲到
addEntry(hash, key, value, i);
return null;
}
inflateTable(threshold)
该方法用来初始化Entry数组
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//找到大于该值的最小的2次幂方数 即12 -->16 31-->32 64-->64
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化阈值
//根据前面算出来的capacity初始化Entry[]数组
//而且前面算出来的capacity必须是2的次方数,所以Entry数组必须是2的次方数即2,4,8,16,32......1024......1<<30 为什么必须是这么大后面也会讲
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
//用来找到大于等于number的最小2的次方数
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
//如果number大于最大容量了,就只用最大的容量就行
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//Integer.highestOneBit(n)该方法用来找到小于等于n的最小2次方数
//源码如下
public static int highestOneBit(int i) {
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
//这个是怎么找到的呢,举例分析 17应该找到的是16
//int在java中32位这里只写低8位
//17 0001 0001
// >>1 0000 1000
// |= 0001 1001
// >>2 0000 0110
// |= 0001 1111
// >>4 0000 0001
// |= 0001 1111
// >>8 0000 0000
// |= 0001 1111
//右移16位依然为0001 1111
//再将其>>>1 0000 1111
//用i减去该值 0001 0000即16 then retrun.
//分析原理 该方法将形如001* ****这种数据转换成0010 0000这种数据
//因为2进制中2的次方数只有一位为1其余位全为0 保留最高位的1其余位为0
//则找到该最小值
// roundUpToPowerOf2((number - 1) << 1))
//先不管减1 如果num为15 <<1 翻倍为30找到小于或等于其的2次方数则为16
// 16 <<1 为32 小于或等于其2次方数则为32但是num为16 要求得到的值应该为16 所以number - 1 then << 1 16 - 1 = 15 << 1 = 30 得到的值就为16
//为什么要 >> 1 >> 2 >> 4 ..... >> 16的原因是
// 1 + 2 + 4 + 8 + 16正好是32 而int值为32位 保证了较大值也能计算出结果
hash()
先说indexFor()方法
static int indexFor(int h, int length) {
return h & (length-1);
//该方法时根据hash值找到数组中的下标
//正常思路就是将hash值与数组的长度取余%就会得到0 - (length - 1)的值,但是%运算比较耗时,就用了&运算 原理分析如下
// 例如hash 值为 18 0001 0010 数组长度为8
// 0001 0010
//& 0000 0111
//= 0000 0111 7
// 例如hash 值为 9 0000 1001 数组长度为8
// 0000 1001
//& 0000 0111
//= 0000 0001 1
//但是这种方法 上面这个例子来说 0000 1001得到的是1 而**** 1001得到的都是0000 0001这样hash碰撞就比较严重,因为高位并没有参与到运算中。所以为了避免这种碰撞,该类提供了hash()方法重新计算了一个hashcode,增加了散列性,而不仅仅只用key.hashcode()
}
final int hash(Object k) {
//hashSeed初始值为0
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//将h 异或 key.hashcode()
//假设key.hashcode() 为 0001 0110 与0000 0000异或
// ^= 仍为0001 0110
//后面的操作是将高位都参与到低位的运算中,增加hash的散列性,为indexFor()提供支持
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
addEntry()
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);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
因为Entry对象里有key value next
头插法即 先取出Entry e = table[index]
table[index] = new Entry[key , value , e] 即可。
采用头插法时,插入是 O(1)的,遍历是O(n)的。
尾插法 插入O(n) 遍历也为O(n)
但是在put时会先遍历这个链表看这个链表里是否有相同的key的Entry,
所以其实无论头插法尾插法效率都差不多。
resize()
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);
}
void resize(int newCapacity) {
//检查数组的大小是否是最大值,如果是,不支持扩容
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建二倍大小的新数组
Entry[] newTable = new Entry[newCapacity];
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) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
按照代码所示
先将rehash当作false处理
最终复制完成,不难发现新数组中链表的顺序与原链表顺序相反。
在多线程下,假设两个线程操作时,都需要扩容,假设线程A扩容完成,线程B走到创建好e和next指针时停止
这种情况下B继续操作
最终形成了循环链表,也就是说当你对其链表再遍历的时候就会形成死循环。
那hashMap多线程下会出现死循环就无法使用了吗?
答案是否定的。避免死循环就是避免hashmap进行扩容。也就是说如果你需要30个数据去存储,那么你初始化数组的时候就先设定好容量,从而避免出现扩容。当然,jdk1.7的hashMap是没有解决这个问题的。