HashMap
基本特性
特性:
hashmap 是一个散列表,存储的内容是以键值对(key-value)进行存储的,key仅允许一条为null的的数据,无序,根据key的hashcode存储数据,不支持线程同步。
数据结构:
JDK1.7:Table数组+Entry链表
JDK1.8:Table数组+Entry链表/红黑树
常用方法
-
初始化
//不给定初始化容量 Map<String, String> hashMap1 = new HashMap<>(); //给定初始容量(最好给定合适初始容量,避免扩容消耗资源) Map<String, String> hashMap2 = new HashMap<>(2); //给定集合 Map<String, String> hashMap3 = new HashMap<>(hashMap1); //给定初始容量和负载因子(当容量百分比超过因子的进行扩容) Map<String, String> hashMap4 = new HashMap<>(16,0.8f);
-
添加数据
HashMap<String, String> map = new HashMap<>(2); map.put("B","456"); map.put("C","789"); Map<String, String> hashMap1 = new HashMap<>(3); //添加键值对,存在修改value值 hashMap1.put("A","123"); //将map中的键值对插入hashmap1中 hashMap1.putAll(map); //如果 hashMap1中不存在指定的键,则将指定的键/值对插入到hashMap1中。 hashMap1.putIfAbsent("A","666");
-
删除元素
//删除 hashMap1 中指定键 key 的映射关系 hashMap1.remove("A"); //删除 hashMap1 中指定键值对 key-value,不存在该键值对则不删除 hashMap1.remove("B","4567"
-
获取元素
//获取key=B的value值 String bValue = hashMap1.get("B"); //获取key=A的value值,若不存在返回默认值"555" String aValue = hashMap1.getOrDefault("A","555");
-
遍历
//lambda遍历 hashMap1.forEach((key,value)->{ System.out.println(key+"-"+value); }); //keySet遍历 for (String key : hashMap1.keySet()) { System.out.println(key+"-"+hashMap1.get(key)); } //entrySet 迭代器遍历 Iterator<Map.Entry<String, String>> iterator = hashMap1.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); System.out.println(entry.getKey()); System.out.println(entry.getValue()); } //KeySet 迭代器遍历 Iterator<String> iterator2 = map.keySet().iterator(); while (iterator2.hasNext()) { String key = iterator2.next(); System.out.println(key); System.out.println(map.get(key)); } //stream单线程遍历 map.entrySet().stream().forEach((entry) -> { System.out.println(entry.getKey()); System.out.println(entry.getValue()); }); //stream多线程遍历 map.entrySet().parallelStream().forEach((entry) -> { System.out.println(entry.getKey()); System.out.println(entry.getValue()); });
其他常用的方法
方法 | 描述 |
---|---|
size() | 计算 hashMap 中键/值对的数量 |
containsKey() | 检查 hashMap 中是否存在指定的 key 对应的映射关系。 |
containsValue() | 检查 hashMap 中是否存在指定的 value 对应的映射关系。 |
values() | 返回 hashMap 中存在的所有 value 值。 |
replace() | 替换 hashMap 中是指定的 key 对应的 value。 |
replaceAll() | 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。 |
源码分析
public class HashMap<K,V> extends AbstractMap implements Map{
//Hash数组初始化容量 -- 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//Hash数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//转红黑树阈值
static final int TREEIFY_THRESHOLD = 8
//红黑树元素节点小于等于6转链表
static final int UNTREEIFY_THRESHOLD = 6;
//操作次数
transient int modCount;
//默认初始容量
int threshold
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断数组是否为空
if ((tab = table) == null || (n = tab.length) == 0)
//初始化
n = (tab = resize()).length;
//判断下标位置是否有值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//有值
Node<K,V> e; K k;
//判断key是否已经存在存在直接替换
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否是树结构,是则直接插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历插入
for (int binCount = 0; ; ++binCount) {
//插入并判断是否需要转红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表长度是否大于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//判断key是否存在,存在即替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//操作次数
++modCount;
//判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
核心在于put方法
hashmap的put过程:
1.计算key的hashcode值(hashcode是object基类的方法,如果是String类型,他已经重写了hashcode方法,所以保证了同样的值,hashcode值相等;如果是其他类型,需要自己重写hashcode方法,因为,object的hashcode方法只是返回对象的内存地址,相同值的对象,由于在堆中存放的位置不同,hashcode也不同,这就违反了hashmap的key唯一性的定义),
2.根据hashcode计算key的hash值。(使用hash散列算法,目前采用的是:key==null?0:(h=key.hashCode())^(h>>>16);)
3.根据hash值计算数组index。(Entry[]数组的长度在初始化的时候会被指定,index需要尽可能的分布均匀;两种算法:取模运算(index=hash%length)和位运算(index=hash&(length-1);但是位运算有个前置条件:length的值必须是2的n次幂,因为只有这样,hash%length=hash&(length-1)才成立,实践证明,这样可以降低hash碰撞,就减少存放在链表的可能性,相对的,查询的时候就不用遍历某个 位置上的链表,这样查询效率也就较高了。 );两者都能保证key分布在Entry[0,length-1]上,但是取模运算效率低,所以选用位运算进行映射。)
4.插入,得到index之后,判断Entry[index]当前数组位置上是否已经有值,如果没有,直接插入;如果有值,需要根据equals方法判断key是否已经存在,存在的话直接覆盖;不存在的话,判断Entry[index]是否是treeNode,是的话直接红黑树插入value;如果不是的话,开始遍历链表准备插入,插入后判断此时链表长度是否大于8,大于的话转换为红黑树,插入,不大于的话,根据equals方法判断key是否已经存在,存在的话直接覆盖,不存在的话,直接插入到最前面,其他value后移。 插入之后判断此时size是否大于阈值,大于的话需要resize进行扩容,重新映射。
总结:
1.底层存储数据的数组,在第一次put元素的时候初始化,同时发生第一次扩容;
2.相比较JDK 1.8之前的版本,JDK 1.8在链表长度大于8的时候,会转化为红黑树处理,主要是基于效率的考量;
常见问题
-
HashMap容量为什么必须是2的幂?
因为计算数组中的下标是由key的hash值和数组长度-1进行位运算(&),如果数组长度不是2的幂,数组长度的二进制有可能出现0,导致数组中的某些位置有可能永远存储不到值
-
HashMap默认的负载因子为什么是0.75?
通过统计,取得一个时间和空间的平衡
-
什么是Hash碰撞?
两个对象的hash值一样,也就是key的计算出来的下标相同
-
HasHMap存储null键的位置?
在table数组第0个下标的位置
-
HashMap再次扩容的数量?
原来数组的两倍
-
JDK1.7版本和JDK1.8版本的HashMap有什么区别?
JDK1.7:数组+链表
JDK.18:数组+链表+红黑树,链表中的元素个数大于8并且数组大于64转换为红黑树,提高了查询的效率
-
为什么链表大于8才转换为红黑树?
泊松分布(了解即可)