一、HashMap概述:
1.HashMap主要是是用来存储键值对,实现了Serializable(可被序列化),Cloneable(可被克隆),Map接口,继承了AbstractMap,其key和value都可以为null,但是key的位置只能存在一个null。
2.HashMap的底层数据结构在1.8之前采用数组+链表实现,遇到哈希冲突时通过拉链法解决。在1.8之后采用数组+链表+红黑树实现。当链表长度大于8时,会先进行数组扩容(减短链表长度),当数组长度大于64时,会将链表转换为红黑树。
二、HashMap存储数据的过程(put方法)
public void test01() {
HashMap<String, Integer> hashMap = new HashMap();
hashMap.put("a", 3);
hashMap.put("b", 4);
hashMap.put("a", 88888);// 修改
System.out.println(hashMap);
}
1.在1.8之前,创建HashMap对象的时候就会从构造方法生成一个默认大小为16的Entry[] table数组。而在1.8之后,当第一次调用put(putVal)方法时才会创建一个Node[] table数组。
HashMap<String, Integer> hashMap = new HashMap();
2.假如向表中存储"a"-3这个键值对,那么首先会调用String中重写的hashCode()方法计算出"a"的hash值,然后通过这个值与数组长度进行一定的运算(index=hash&(cap-1),cap为数组容量),得出该数据在数组中的索引值(下标),如果该位置为空,则存储到数组中。如果该位置不为空,例如存在"aa"-3这个键值对,那么此时分为两种情况:
1).二者的hashcode值不相等,则在此空间上划出一个节点来存储插入的数据(拉链法)。
2).二者的hashcode值相等,此时会先调用重写后的equeals()方法判断二者的key内容是否相等,如果相等则为同一对象,value值覆盖;如果不相等则不为同一对象,此时会依次向下比较(如果有链表的话),都不相等则插入链表(尾插法)。
获取数组下标步骤:
获得key的hashcode->通过hash函数获得hash值->hash&(cap-1)得到数组下标。
&相当于对数组长度取模,不过&效率更高在二进制中,cap-1是为了保证随机性。
hash函数的实现方式:
1.对 key 的 hashCode 做 hash 操作,如果key为null则直接赋哈希值为0,否则,无符号右移 16 位然后做异或位运算(高16位和低16位),如,代码所示:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
2.除上面的方法外,还有平方取中法,伪随机数法 和 取余数法。这三种效率都比较低,而无符号右移 16 位异或运算效率是最高的。
注:两个对象的hashcode值相等不一定为同一对象(hash碰撞),但如果两个对象的hashcode值不相等则一定为不同对象!
说明:
1.size表示此时hashmap中存储的数据数量,不等于数组容量!
2.threshold(临界值)= capacity(容量)* loadFactor(负载因子)。size 超过这个值就重新 resize(扩容),扩容后的 HashMap 容量是之前容量的2倍。
三、HashMap的基本属性
//序列化版本号
private static final long serialVersionUID = 362498820763181265L;
//HashMap的初始化容量(必须是 2 的 n 次幂)默认的初始容量为16
// 1 << 4 相当于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的装载因子
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值,当一个桶中的元素个数大于等于8时进行树化
static final int TREEIFY_THRESHOLD = 8;
//树降级为链表的阈值,当一个桶中的元素个数小于等于6时把树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 当桶的个数达到64的时候才进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
// Node数组,又叫作桶(bucket)
transient Node<K,V>[] table;
// 作为entrySet()的缓存
transient Set<Map.Entry<K,V>> entrySet;
//元素的数量
transient int size;
//修改次数,用于在迭代的时候执行快速失败策略
transient int modCount;
//扩容阈值,threshold = capacity * loadFactor
int threshold;
//装载因子
final float loadFactor;
提问:为什么必须是 2 的 n 次幂?如果输入值不是 2 的幂比如 10 会怎么样?
1.为了获取下标,我们需要对数组进行取模运算,为了提高效率将hash % length 替换为了 hash & ( length - 1) ,而这个替换的前提就是length 是 2 的 n 次幂。另外,这样还可以使数据均匀分布,减少hash冲突。
2.如果将长度初始化为10:
HashMap<String, Integer> hashMap = new HashMap(10);
此时HashMap双参构造函数会通过**tableSizeFor(initialCapacity)**方法,得到一个最接近length且大于length的2的n次幂数(比如最接近10且大于10的2的n次幂数是16)。因为当在实例化 HashMap 实例时,如果给定了 initialCapacity,由于 HashMap 的 capacity 必须是 2 的幂,因此这个方法用于找到大于等于 initialCapacity 的最小的 2 的幂。
static final int tableSizeFor(int cap) {
int n = cap - 1;
//cap-1是为了防止 cap 已经是 2 的幂。如果 cap 已经是 2 的幂,又没有这个减 1 操作,则执行完后面的几条无符号操作之后,返回的 capacity 将是这个cap的2倍
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;
//为什么最后n+1?
//如果 n 这时为 0 了(经过了cap - 1后),则经过后面的几次无符号右移依然是 0,返回0是肯定不行的,所以最后返回n+1最终得到的 capacity 是1。
}
四、遍历HashMap的方式
1.分别遍历Key和Values
for (String key : map.keySet()) {
System.out.println(key);
}
for (Object vlaue : map.values() {
System.out.println(value);
}
2.使用迭代器
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> mapEntry = iterator.next();
System.out.println(mapEntry.getKey() + "---" + mapEntry.getValue());
}
3.for-each迭代Entries
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue())
}
4.通过get方式(二次迭代不建议使用)
Set<String> keySet = map.keySet();
for (String str : keySet) {
System.out.println(str + "---" + map.get(str));
}
5.jdk8以后Map接口中的默认方法
default void forEach(BiConsumer<? super K,? super V> action)
// BiConsumer接口中的方法:
void accept(T t, U u) 对给定的参数执行此操作。
参数
t - 第一个输入参数
u - 第二个输入参数
HashMap<String,String> map = new HashMap();
map.put("001", "zhangsan");
map.put("002", "lisi");
map.forEach((key, value) -> {
System.out.println(key + "---" + value);
});
部分参考:
https://csp1999.blog.csdn.net/article/details/109442223