jdk1.7中hashmap源码分析
常量定义:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认初始长度
static final int MAXIMUM_CAPACITY = 1 << 30; //最大值
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//数组
transient int size;//当前hashmap的大小
int threshold;//扩容阈值
final float loadFactor;//扩容因子
transient int modCount;//代表对hashmap的操作次数 提供一种fast-fail快速失败机制
构造方法:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
put
public V put(K key, V value) {//PUT时,如果key相同,会进行覆盖操作,返回的是被覆盖的value
//第一次进来就是空的,进行初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
//从这一点可以看出hashmap是支持key为null的,如果不支持,这里可能会直接抛出一个异常
return putForNullKey(value);
int hash = hash(key);//计算key的hash值
int i = indexFor(hash, table.length);//用hash值和数组长度算出一个数组下标
//遍历第i个位置上的链表
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);//hashmap中没用到这个方法
return oldValue;
}
}
modCount++;
//添加
addEntry(hash, key, value, i);
return null;
}
inflateTable
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//对toSize做了处理后才新建Entry
//找到一个>=toSize的最近的2的幂次方数
int capacity = roundUpToPowerOf2(toSize);
//
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];//初始化
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
//如何取>=number的最小的2次幂呢
//规律:2的幂运算的二进制数,都只含一个1,例如0000 0001
//Integer.highestOneBit这里返回的是<=number的最近一个2的幂运算,原理是通过>>和|运算,一步一步把二进制最高位为1的后面的数,都替换为0
//例如0010 1100 替换结果就是0010 0000 如果在以后代码中遇到类似场景,可以直接用这个方法
//计算最小2的幂之后,左移一位
int rounded = number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0
? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
return rounded;
}
hash
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();//异或 相同为1 不相同为0
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//&操作 都为1 则为1 这里指两个数的二进制位对比
//例如
//h:0101 0101 length-1:0000 1111
//&之后的结果就是: 0000 0101
//位运算效率高
return h & (length-1);
}
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容
//threshold=数组长度*0.75
if ((size >= threshold) && (null != table[bucketIndex])) {
//2倍
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];
//新new一个entry,把当前数组作为新数组的next
//再把新entry赋值给当前索引位置,这就是jdk1.7的头插法
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
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) {//rehash先默认为false,大多数情况他都是false,jdk8当中都没有这个rehash了
int newCapacity = newTable.length;
//双重循环,把老数组的内容分配到新数组中
for (Entry<K,V> e : table) {
while(null != e) {
//如果是多线程环境,线程A线程B,A和B会各自创建一个新数组
//A会正常执行如下代码 B走到这一步就会卡住,假设指针指向第一个元素1
//A循环后所有的数据都转移到了新数组中,元素由1,2,3变为3,2,1 这个时候线程B的next指向的是链表最后端的1
//此时线程B继续执行
//B执行完之后,会覆盖A循环出来的新数组,此时B循环出来的数组是有大问题的,
// 后果是后续在put元素时,如果恰巧循环到了这个位置的链表,就会出现无限循环的问题
//归根结底还是因为头插法把链表顺序搞反了,导致第二个线程循环分配链表元素时,乱套了,形成一个无限循环的链表,这是bug
Entry<K,V> next = e.next;
if (rehash) {
//rehash大多数情况都是false,因为很少有人去配置这个启动参数,不配置的话,默认要比整数最大值大才有可能为true
e.hash = null == e.key ? 0 : hash(e.key);
}
//分配到新数组中的位置规律:和就数组一样或者就数组的索引位置加上新数组的长度
int i = indexFor(e.hash, newCapacity);
//此处仍然是用头插法将链表的数据移动到新数组中
//存在问题:例如原链表中数据从上到下是1,2,3
//因为遍历是按1,2,3的顺序遍历,那么到了新链表中,由于头插法特性,链表数据正好相反了,变为3,2,1
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;//哈希种子为0的情况下,currentAltHashing为false
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//这里提供一个可配置的参数
boolean switching = currentAltHashing ^ useAltHashing;//异或,不同则为true
if (switching) {
//更新哈希种子
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
多线程情况下transfer:
线程A正常执行,线程B会卡住,线程A运行前线程B的指向效果是:
线程A正常执行完,分配旧元素到新数组中了,此时线程B的指向情况为:
线程B执行第一次:
线程B执行第二次:
执行到e.next为null时,不再继续执行,此时会形成一个循环链表:
新数组会再赋值给table,问题就大了.
形成了一个无限循环的链表.
源码看起来会乱乱的,第一次看建议下载jdk源码对照注释进行逻辑分析.