一、HashMap结构
1.1 HashMap简介
jdk1.7里面HashMap底层是由数组+链表的结构实现的,jdk1.8就变成了数组+链表+红黑树了;数组是用来存放元素,链表用来解决哈希冲突。
1.2"初始化"
大家都知道数组是通过下标来存放元素的,所以在进行向HashMap里面put元素的时候就需要计算下标。故我们从一个例子出发:
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("1", "韩信");
String value = hashMap.put("1", "李白");
System.out.println(value); //韩信
String value2= hashMap.get(1);
System.out.println(value2); //李白
首先呢我们进入HashMap这个类里面去看看,发现它首先呢是初始化了一个长度为16的数组长度,扩容的最大容量为2^30(一般都用不到这么多),加载因子为0.75,阈值(数组容量乘以加载因子)等等一些初始化属性的事情。
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;
int threshold;
二、put方法
2.1 put方法源码
然后我们进入到里面的put方法里面,这就可以方法上面的例子里第二次put相同的key时会返回一个value,这个值是oldvalue,也就是被覆盖的哪个value值,所以上面的value值为韩信。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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++;
addEntry(hash, key, value, i);
return null;
}
2.1.1ArrayList纯数组
ArrayList里面是采用纯数组的形式存放元素,我们在用add往里面放元素的时候是有两个方法,一个是带下标的,一个是不带下标的。可以发现当我们不带下标的时候这种以此向后存储的方法效率还是挺好的。
那为什么HashMap里面不用这种方法呢?
原因是这样的get效率低了,因为HashMap里面我们要传入一个key值,这个key值你不知道在数组里面的哪个位置,就需要去遍历数组,所以效率就变低了。HashMap是想要做到put和get方法同时效率都很高的!
ArrayList arrayList = new ArrayList();
arrayList.add(0, new Object());
arrayList.add(new Object());
2.2初始化table
通过源码我们可以知道,在put的时候会去首先判断table这个属性是否为空,如果为空会通过方法inflateTable去进行初始化。而这个threshold阈值会在HashMap的构造方法里面去给他赋值。这个值默认是16,当然也可以我们自己在创建HashMap的时候就给定一个容量值以及加载因子,这里就不过多赘述了。
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
然后我们进入到这个inflateTable方法里面去。可以看到一行关键代码就是new了一个Entry数组赋值给table,并且在之前去计算了这个阈值,也就是容量乘以扩容因子。
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
可以看一下这个Entry对象是什么样的,它里面定义了key,value,还有next以及一个hash这四个属性
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
。。。
我们new这个数组的时候并没有把直接传入的值toSzie拿来当数组容量,而是又进行了一个方法。源码里面的注释写到是,去找到一个大于等于toSize的2的幂次方数(也就是1去找1,2去找2, 3至8去找8,9至16去找16)。那接着我们来看它使怎么实现这个操作的呢。
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
它首先是去看number是否大于最大的容量(2^30),如果没有,就会再去判断number是否大于1,如果是0的话就会返回一个1。去走到highestOneBit这个方法里面去。
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
在这个方法里面并不是去找大于等于i的2的幂次方数,而是去找小于等于i的2的幂次方数。
而这个方法就是用到计算机里面通过二进制数的右移和或(有1则为1)运算来实现的。
下面举例详解:
17 | 0001 0001 |
---|---|
>>1 | 0000 1000 |
| | 0001 1001 |
>>2 | 0000 0110 |
| | 0001 1111 |
>>4 | 0000 0001 |
| | 0001 1111 |
接着后面右移8位,右移16位结果都不会改变了,所以我们接着看return里面的。
i | 0001 1111 |
---|---|
>>>1 | 0000 1111 |
i - (i >>> 1) | 0001 0000 |
这样就找到我们需要的值16(小于等于17的2的幂次方数)。
接着我们来看看随机一个数i的变化过程:
i | 001* **** |
---|---|
>>1 | 0001 **** |
| | 0011 **** |
>>2 | 0000 11** |
| | 0011 11** |
>>4 | 0000 0011 |
| | 0011 1111 |
… | |
>>>1 | 0001 1111 |
i - (i >>> 1) | 0010 0000 |
所以,我们可以通过上面这个方法找到任意一个数的比它小的2的幂次方数。但这并不是我们想要到最终结果,所以我们回到这个方法调用的地方。
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
这里可以看到它并不是直接把number的值传入highestOneBit方法里,而是传的(number - 1) << 1。即需要把这个值先进行左移,这样17(0001 0001)就变成了36(0010 0100),在去取小于36的2的幂次方数32,即是大于17的2的幂次方数。至于number-1是为了解决边界值的问题,当传入值是16的时候,最后返回的是32,然而我们就要16就行了,这样16-1=15经过计算最后返回的值就是16了。通过以上一系列步骤就找到了我们用来出初始化数组的容量了(不禁感叹作者大大每一个细节都是用到了极致呀!)。
今天就写到这里吧,后面再续更,写多了估计也看不下去,哈哈哈哈!