先来看一个简单的例子:
HashMap map = new HashMap();
map.put("语文", 1);
map.put("数学", 2);
map.put("英语", 3);
map.put("历史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化学", 8);
for(Entry entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
运行结果是:
可以看到:
1. 有一个叫做table大小是16的Entry数组,16即为HashMap初始容量。
2. 这个table数组存储了Entry类的对象。HashMap类有一个叫做Entry的内部类。这个Entry类包含了key-value作为实例变量
3. 每当往hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。示例中创建的Entry对象将会存放的位置(即在table中的精确位置),是根据key的hashcode()方法计算出来的hash值来决定。hash值用来计算key在Entry数组的索引。
4.观察上面的断点视图中数组的索引 0,它有一个叫做HashMap$Entry的Entry对象。
上面的对象的key-value的hash值是如何计算出来的?根据key的hashcode()方法。
put源码:
分析:
1. 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。
2. key的hashcode()方法会被调用,然后计算hash值。hash值用来找到存储Entry对象的数组的索引。
3. indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。
4. 在我们的例子中已经看到,如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。
*如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
*如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
(这也是为什么上面的例子中,放了8个元素,却显示了6个。)
put方法的思路:
1.对 key 的 hashCode()做 hash,然后再计算 index;
2.如果没碰撞直接放到 bucket 里;
3.如果碰撞了,以链表的形式存在 buckets 后;
4.如果碰撞导致链表过长(大于等于 TREEIFY_THRESHOLD),就把链表转换成红黑树;
5.如果节点已经存在就替换 old value(保证 key 的唯一性)
6.如果 bucket 满了(超过 load factor * current capacity),就要 resize。
*如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
get源码:
get工作原理分析:
当你传递一个key从hashmap总获取value的时候:
1. 对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。
2. key的hashcode()方法被调用,然后计算hash值。
3. indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。
4. 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回 Entry对象的value,否则,返回null。
HashMap中两个重要的参数:
容量(Capacity)和负载因子(Load factor)
简单的说,Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor时就需要调整buckets的数目为当前的2倍。