目录
8. 如何决定使用 HashMap 还是 TreeMap 的区别
1. HashMap的底层实现,HashSet的底层实现
HashMap在JDK1.8之前是数组+链表,JDK1.8之后是数组+链表/红黑树
HashSet的底层是HashMap
2. HashMap的put方法的底层原理
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;
// 计算索引,并把当前索引数据赋值给p,如果为空就代表当前位置不存在冲突,可以直接存放值(此时,这个节点放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
// 直接赋值
tab[i] = newNode(hash, key, value, null);
else {
// 如果不为空说明存在冲突,需要链表处理或者链表升级
Node<K,V> e; K k;
// 如果当前索引数据和传入的hash相同并且key也一样,说明这是map的覆盖数据操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将引用地址赋值给e,后续对e做值覆盖操作
e = p;
else if (p instanceof TreeNode)
// 如果p已经进化成红黑树,就插入树节点操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 这里就是链表操作,既不是书结构也不是值覆盖,如果p的后继是空
if ((e = p.next) == null) {
// 我们就将值赋值给p的后继
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果添加一个数据之后刚好大于7的最大链表长度就升级成红黑树
treeifyBin(tab, hash);
break;
}
//判断链表中节点的key值与插入的元素key值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//用于遍历桶链表
p = e;
}
}
// 值覆盖
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;
}
1.根据key的hashCode计算出数组index
2. 落槽时:
1.如果数组中节点为null,创建新的节点对象,把k,v存储在节点对象中,把节点对象存储在数组中2.如果数组的节点不为null,判断节点的key与插入元素的key是否相等
1.相等,直接用新的k ,v覆盖原节点中的k,v
2.不相等,判断此时节点是否为红黑树
1.是红黑树,创建红黑树节点对象存储k,v,插入到红黑树中
2.不是红黑树,创建链表节点对象存储k,v,插入到链表中,判断链表长度是否大于阈值8
1.大于阈值8,链表转换为红黑树
3.判断++size是否大于阈值,是就扩容
3. HashMap的resize()扩容方法的底层原理
final Node<K,V>[] resize() {
// 申明对象,指向Hash桶数组
Node<K,V>[] oldTab = table;
// 记录当前node数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 记录负载值,就是临界值
int oldThr = threshold;
int newCap, newThr = 0;
// 不为空,表示之前的数组容量不为空
if (oldCap > 0) {
// 如果这个存放的数据大于int最大值,就赋值为整数的最大的阈值,不扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果当前hash桶数组在扩容后小于int的最大值并且oldcap大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 负载扩大一倍、负载就是负载因子 * 最大长度
newThr = oldThr << 1; // double threshold 双倍扩容阈值threshold
}
// 表示之前的容量是0,但是之前的阙值却大于零, 此时新的hash表长度等于此时的阙值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 初始化,无参构造创建的map,给出默认容量和threshold 16,16*0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//此时表示若新的阙值为0 就得用 新容量* 加载因子重新进行计算。
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 扩容实际操作,遍历旧的hash表,将之内的元素移到新的hash表中。
for (int j = 0; j < oldCap; ++j) {
// 申明对象
Node<K,V> e;
// 如果当前索引数据有值,赋值给e
if ((e = oldTab[j]) != null) {
// 清空旧node[]当前索引数据
oldTab[j] = null;
if (e.next == null)
// 如果当前节点就一个,不存在冲突,重新计算索引,再分配
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果在旧哈希表中,这个位置是树形的结果,就要把新hash表中也变成树形结构
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//保留 旧hash表中是链表的顺序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {// 遍历当前Table内的Node 赋值给新的Table。
next = e.next;
if ((e.hash & oldCap) == 0) {
// 原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 原索引+oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//遍历结束,讲tail指向null,并把链表头放入新数组的相应下标,形成新的映射
if (loTail != null) {
//原索引放到bucket里面
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 原索引+oldCap 放到bucket里面
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap 的扩容实现机制是将老table数组中所有的Entry取出来,重新对其Hashcode做Hash
散列到新的Table中,可以看到注解Initializes or doubles table size.
resize表示的是对数组进行初始化或进行Double处理。
HashMap默认初始容量是16,resize()方法是在hashmap中的size大于阈值时或者初始化时,就调用resize方法进行扩容每次扩容的时候始终是原数组长度的2倍,即长度永远是2的n次方
扩容后节点对象的位置要么在原位置,要么偏移到两倍的位置
4. HashMap 的长度为什么是2的幂次方
HashMap的长度为什么是2的幂次方
为了能让HashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),hash%length==hash&(length-1)的前提是length是2的n次方;
举个例子:
长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
因为在hashMap的length等于2的n次方的时候,才会有hash%length==hash&(length-1);哈希算法的目的是为了加快哈希计算以及减少哈希冲突,所以此时&操作更合适,所以在length等于2的幂次方的时候,可以使用&操作加快操作且减少冲突,所以hashMap长度是2的幂次方
5. 什么是哈希码和哈希函数
哈希函数:就是一个方方
哈希值=(hashcode)^(hashcode >>> 16)
同一个字符串使用同样的哈希函数计算出来的哈希码必是一样
不同的字符串使用同样的哈希函数计算出来的哈希码大概率是不一样的,小概率是一样的;下图是哈希函数的转换示意图。
6.什么是哈希碰撞/哈希冲突
如果有两个字符串通过同样哈希算法计算出来的哈希码是一样的,则称他们发生了哈希碰撞,哈希冲突,哈希冲突是不可避免的,我们常用解决哈希冲突的方法有两种开地址法和拉链法。
开地址法
就是发生冲突时在散列表(也就是数组里)里去寻找合适的位置存取对应的元素。
线性探测法:
就是当前位置冲突了,那我就去找相邻的下一个位置。
举个例子:当<c,3>也需要落入<a,1>这个位置的时候,但是这个位置已经有元素了,那么就依次加一寻找合适的位置,把<c,3>放入。
hash(key) = (hash1(key)+i)%9, i = 0,1,2,......7,8
所以关键字要进行查找或者插入,首先看(hash1(key)+0)%9 位置是自己最终的位置吗?如果有冲突,就探测(查看)下一个位置:(hash1(key)+1)%9,依次进行。
平方探测法:
但是这样会有一个问题,就是随着键值对的增多,会在哈希表里形成连续的键值对,这样的话,当插入元素时,任意一个落入这个区间的元素都要一直探测到区间末尾,并且最终将自己加入到这个区间内。这样就会导致落在区间内的关键字Key要进行多次探测才能找到合适的位置,并且还会继续增大这个连续区间,使探测时间变得更长,这样的现象被称为“一次聚集”。
hash(key) = (hash1(key)+i^2)%9, i = 0,1,2,......7,8
双散列:
可以再弄另外一个Hash函数,对落在同一个位置的关键字进行再次的Hash,探测的时候就用依赖这个Hash值去探测
hash(key) = (hash1(key)+hash2(key)*i)%9, i = 0,1,2,......7,8
拉链法
把哈希表每个单元中的存储方式都设置为链表,某个数据项的关键字值还是像之前一样通过哈希函数映射到哈希表,但是这个数据插入到哈希表指定下标单元的链表中,当有其他元素映射到同一个单元的时候,就往链表后面挂就可以了.
7. HashMap和TreeMap的区别
相同点
- HashMap和LinkHashMap,TreeMap都属于Map;
- Map主要用于存储键值(key)(value)对,根据键得到值,因此键不允许重复,但值允许重复;
不同点
- HashMap里面存入的键值对取值的时候是随机的,根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap是最好的选择;
- TreeMap取出来的是排序后的键值对如果按照顺序遍历,TreepMap会更好;
- LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现
8. 如何决定使用 HashMap 还是 TreeMap 的区别
如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap。
9. HashMap和HashSet的常用方法
HashMap的常用方法:
方法名 | 参数说明 | 返回值说明 | 方法功能说明 |
put(K key, V value) | 需要输入的key值,需要输入的value | 返回true | 将键和值射映射存放到Map集合中。 |
size() | 无参 | 返回Map集合中数据数量 | 返回Map集合中数据数量 |
isEmpty() | 无参 | 返回true或者false | 判断Map集合中是否有数据,如果没有则返回true,否则返回false |
get(Object key) | 指定的键值 | 返回对应的key或者null | 返回指定键所映射的值,没有该key对应的值则返回 null。 |
clear() | 无参 | 无返回值 | 清空Map集合 |
HashSet的常用方法:
方法名 | 参数说明 | 返回值说明 | 方法功能说明 |
add(Object obj) | 需要添加的元素 | 返回true或false | 向Set集合中添加元素,添加成功返回true,否则返回false。 |
size() | 无参 | 返回Set集合中数据数量 | 返回Set集合中的元素个数。 |
isEmpty() | 无参 | 返回true或者false | 如果Set不包含元素,则返回 true ,否则返回false。 |
remove(Object obj) | 需要删除的元素 | 返回true或者false | 删除Set集合中的元素,删除成功返回true,否则返回false。 |
clear() | 无参 | 无返回值 | 移除此Set中的所有元素。 |