HashMap源码分析(JDK1.7)
构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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 HashMap(int initialCapacity, float loadFactor) {
//判断传入的初始化大小是否小于0如果小于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);
}
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为38,当然这个数可以是
经过第一行可以得出n为37
第二行:0010 0101>>>1 得到0001 0010 在进行|运算0011 0111
第三行:0011 0111>>>2 得到0000 1101 在进行|运算0011 1111
第四行:0011 1111>>>4 得到0000 0011在进行|运算0011 1111
第五行:0011 1111>>>8 得到0000 0000 在进行|运算0011 1111
第六行:0011 1111>>>16 得到0000 0000 在进行|运算0011 1111
经过上面真么多行的位移我们可以保证最高位1后面的二进制全部为1了
然后(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
如果n小于0那就直接返回1,因为2的最小次幂就是1,如果继续判断n是否大于最高1<<30,如果大于让他使用最大容量,负责让他等于n+1;为什么等于n+1
0011 1111 +1 就等于0100 0000 ,二的次幂有一个特点那就是二进制里面只能出现一个1,=
然后我们可以想想为什么第一次要减1,如果这个数就是二的次幂如整数32,二进制位00100000
不执行第一行,执行第二行到第六行可以得到 0011 1111 在加1
不就是0100 0000了吗得到64,他自己本身就是二的次幂直接拿来用就可以了,无需对他进行操作
所以我们让他减少1抹掉自己最高位的1 得到二进制0001 1111
经过第二行到第六行运算得到 0001 1111 在经过+1运算有得到原来的0100 0000了
如果不传入参数使用的都是默认参数,到此容量和加载因子初始化结束
Put方法分析
public V put(K key, V value) {
//判断是否为空 直接插入到下标为0的数组中
if (key == null)
return putForNullKey(value);
//对key进行计算hash值
int hash = hash(key);
//计算下标 内部做了 h & (length-1); 这样& 只有当2边为1才为1 不管hash值有多长只取length-1位
//计算hash值是 1111111111 length如果是8 0111 最高也就 0111 下标为7不会越界
int i = indexFor(hash, table.length);
//进行for循环
//首相对e 进行复制 获取数组的的计算的i的下标位置 是否为空,如果不为空就进行循环
//Entry是一个链表,所以调用next一直循环,直到找到对应的值,或者循环到最后一个节点
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 首先对hash值进行对比如果相同在对比key的地址是否相同一般做基本数据类型比较,如果是对象调用equals进行对比,如果查询到内部有相同的key就进入
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//保存旧的值
V oldValue = e.value;
//把新的值赋值进入
e.value = value;
e.recordAccess(this);
//返回旧的值
return oldValue;
}
}
//如果没有寻找到
modCount++;
//直接使用头插法插入一条数据
addEntry(hash, key, value, i);
return null;
}
Hash值计算
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
//主要目的是让hashCode的全部二进制参与到计算中
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//threshold 默认16*0.75
//首先会判断size是否大于等于阈值以及table当前下标是不为空 如果符合条件进行扩容
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进行++
size++;
}
当一个新数据要插入的时候 如何计算的下标和数据1是同一个,他会先循环遍历是否有相同的key如果有就替换,如果没有进行插入
首先让一个变量先保存数组[i]的值
在让新的数据的下一个结点等于这个变量
最后把数组中的值替换成新数据的地址值
经过以上几步实现了新数据的插入
resize
void resize(int newCapacity) {
//把旧的数组保存起来
Entry[] oldTable = table;
//把旧的数组长度保存下来
int oldCapacity = oldTable.length;
//判断旧的数组长度是否等于最大阈值 如果等于把阈值增加到整形最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新的数组也就是length*2
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
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) {
//使用next保存下一个元素的地址
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新生成下标,因为计算下标是通过h & (length-1); 数组长度发生了改变所以要存放的下标也会改变
//只不过这个改变是有规律的, length长度无法就是*2,
// 如:0001 0100 & (16-1) 得4
// 如:0001 0100 & (32-1) 的20 可以看出增加了16 也就是扩大数组容量的大小
// 如:0100 0011 & (16-1) 得3
// 如:0100 0011 & (32-1) 的3 可以看到没有发生改变,这是因为参数的hash值和(32-1)的二进制最高位1的位置没有1 所以不会二进制进1操作
//通过以上可以看出转移数据只会出现在他原来下标的位置或者他的 +原长度的下标位置
int i = indexFor(e.hash, newCapacity);
// 然后进行头插法 ,我们可以通过以下几张图片看出插入的变化
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
首先创建一个新的数组长度是原来的2倍
假设已经遍历到下标1的数组的第一个元素,切第一个元素并没有受到长度变化改变下标
可以得到
继续经过上面步骤 next下移 ,直到无法寻找到元素,可以看出因为使用了头插法链表发生了反转的操作.