HashMap的详细介绍
—–本文只针对1.7版本的HashMap所讲解.
HashMap是java里面以Key-value存储的一种集合对象,它使用的是数组加上链表的结构,它允许key和value为null,是一种无序并且线程不安全的集合对象。现在我们开始详细介绍下它的工作流程.
HashMap它有四种构造方法,如下图所示:
//两个参数的构造方法,传入的是初始容量,加载因子
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) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//无参构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//这个传入的是一个Map集合
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
我们看见不管传入一个参数还是无参的构造都是去调用下面这个两个参数的构造方法进行构造HashMap对象,那我们就来研究研究这个两个参数的构造方法.
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();
}
这个构造方法需要传入两个参数,一般我们在使用的时候都是使用无参的构造方法进行创建对象,那我们就来看看这个无参的构造方法传入了什么值给这个构造方法.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
我们看见传入的是两个常量值,就是我们所知道的两个HashMap的初始容量和加载因子的默认值,16和0.75,16怎么来的呢?1<<4 是什么意思呢?这里是意思是把1转化为2进制在向右移动4位,1的二进制为..{28个0}..0001(int是32位的),右移4位就变为..{27个0}..10000 这里转为10进制就为16了,所以我们就知道了传入的参数为16和0.75.
传入了以后,接下来它又做了什么事情呢?观看代码如下:
static final int MAXIMUM_CAPACITY = 1 << 30;
final float loadFactor;
int threshold;
public HashMap(int initialCapacity, float loadFactor) {
//判断初始容量是否小于0,如果为true就会抛异常
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);
//下面两段代码就是给HashMap的属性初始化
this.loadFactor = loadFactor;
threshold = initialCapacity;
//这个是一个空方法,我们不需要关注它
init();
}
void init() {
}
到这里一个HashMap对象就已经构造出来了.那有人肯定会疑问.哎为什么传入了容量没有初始化数组呢? 这里HashMap做了一个优化,如果你创建出来对象,却不使用它,它如果给你创建数组岂不是浪费内存空间呢。还有一个问题就是我们传入的容量大小,它一定就会创建我们传入容量大小的数组吗?答案是不一定,为什么,我们下面来慢慢讲解.
我们现在就来解决上面有两个问题.
1: 什么时候它才会创建数组?
2:为什么我们传入的容量,它不一定给我们生成指定的容量大小?
第一个问题很简单,当我们在第一次put元素的时候它会触发数组的初始化,我们来看代码:
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
public V put(K key, V value) {
//这里为true ,所以put之前执行inflateTable
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;
}
我们都知道在创建对象的时候,首先会给成员变量实现初始化,
在我们构造方法中,也没有发现table重新赋值了,所以这里table还是初始化的值,table == EMPTY_TABLE 这个条件是为true的,所以我们会进入这个方法,但是这个方法有一个参数,我们找找这个参数的值,在构造方法中,可以看见我们把初始容量的值赋值给它了,所以传入的值为16,知道了这个条件,我们在进去看看它里面做了什么,
private void inflateTable(int toSize) {
//toSize为16
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
我们看见它把我们传入的值又继续传给了roundUpToPowerOf2这个方法,我们在进去看看,
private static int roundUpToPowerOf2(int number) {
// 它这里做了一些判断,判断我们传入的值是否大于
//HashMap的最大容量,如果大于就返回最大容量
//如果不大于则会继续判断是否大于1,如果小于1就返回1,如果大于1呢?它会继续执行Integer.highestOneBit这方法,我们继续跟踪
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
跟踪的代码如下:
//这里传入的值为((number-1) <<1),我们上面number为16
//这里就是15 << 1 15为1111 ,左移一位为 11110,所以我们传入的二进制为11110(十进制为30)
public static int highestOneBit(int i) {
// 这里是一大堆的位运算,我们一个一个来算
// |= 是什么意思呢?就是两个二进制相比较,如 101 和111比 最后结果为111 ,只要其中有一个1 它就为1
// 11110 >>1 为:1111 ,然后 11110 |= 1111 结果为 11111
i |= (i >> 1);
// 11111 >>2 为:111 ,然后 11111 |= 111 结果为 11111
i |= (i >> 2);
// 11111 >>4 为:1 ,然后 11111 |= 1 结果为 11111
i |= (i >> 4);
//因为后面再怎么移动 它前面的为11111 所以你后面就算全是0 它还是11111 所以我们就看最后一步吧
i |= (i >> 8);
i |= (i >> 16);
//这里返回的时候 i>>>1 是什么意思呢? 就是无符号右移一位,不管正负前面都补0
//所以我们这里移动一位 得出 11111 - 1111 最后得到 10000
//10000 的十进制位16 还是我们传入的值。最后返回。
return i - (i >>> 1);
}
上面这里就引发了我们的第二个问题 我们传入的容量,它不一定给我们生成指定的容量大小,是因为这里的位运算 它会给我们传入的值进行一些操作然后返回.如果我们初始容量为输入5,最后会创建出一个数组为8大小的数组对象.我们这里可以模拟测试一下
所以我们在初始化容量的时候传入的大小不一定就会生成你所传入的大小,(这里说一下它会生成你传入大于你传入数值的最近于2的次幂的一个数)那有人会说这里只是返回一个数字,又不是生成数组,那我们就可以继续往下面看了.
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//我们知道了这里返回了16赋值给capacity
int capacity = roundUpToPowerOf2(toSize);
//Math.min是取两个数之间小的那个把它赋值给threshold
//threshold是一个阀值,如果我们put的元素个数大于阈值就会扩容?
//这里有个问题,如果元素大于阈值 一定会扩容吗?这个问题留着下一章介绍
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//看,同学们 这里是不是就新创建了数组给table了 ,
//好了到这里我们的数组对象已经创建出来了。第一个问题也随即解决。
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
本章的内容就到这里先告一段落,上面有个问题,同学们好好思考一下,下回咱们在来分解这个问题.
书山有路勤为径,学海无涯苦作舟