HashMap在工作中是最常用的一个集合,也是面试中最常问的知识点,现在就让我带你走进HashMap,揭开HashMap的真实面目。
先看几个HashMap的面试题,看你是否能回答上来:
初级面试题:
1、JDK8中的HashMap有哪些改动?
(红黑树,哈希值,链表结点的添加,扩容的机制)。
2、JDK8中为什么要使用红黑树?
3、为什么重写对象的Equals方法时,要重写HashCode方法,跟HashMap有关系吗?为什么?
4、HashMap是线程安全的吗?遇到ConcurrentModificationException异常吗?为什么会出现?如何解决?
5、在使用HashMap的过程中我们应该注意哪些问题?
高级面试题:
1.笔试中要求你手写HashMap
2.你知道HashMap的工作原理吗?
3.HashMap中的“死锁”是怎么回事?
4.HashMap中能put两个相同的Key吗?为什么能或为什么不能?
5.HashMap中的键值可以为Null吗?能简单说一下原理吗?
6.HashMap的扩容机制是怎么样的?JDK7与JDK8有什么不同吗?
HashMap底层是怎么实现的?
JDK7 是数组加链表,而
JDK8是数组加链表/红黑树。当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。
现在用一个例子看一下JDK7的底层实现:
package com.ycc.hashmap;
import java.util.HashMap;
/**
* @author lenovo
*/
public class HashMapTest {
public static void main(String[] args){
HashMap hashMap=new HashMap();
hashMap.put("张三","张三");
hashMap.put("李四","李四");
hashMap.put("王五","王五");
hashMap.put("赵六","赵六");
hashMap.put("谢七","谢七");
System.out.println(hashMap.get("张三"));
for (String key:hashMap.keySet()) {
Integer hash=key.hashCode();
Integer index=hash%8;
System.out.println(String.format("%s的哈希值是%s,取模后index is %s",key,hash,index));
}
}
}
打印结果:
具体的如图所示:在数组索引值相同的情况下,通过链表将所有的数组索引值相同的元素存起来。
解决这种冲突的方法还有散列法,就是发生冲突的时候,再通过一个公式操作,取得没有与别的的下标重复的位置,放进去。HashMap采取是链表法。
那现在有新的元素进来(比如现在一个“程八”进来,数组下标也是1),是放在链表的头部好还是链表的尾部好呢?
站在性能的角度上考虑,对于一个链表肯定放在最前面好,你放在尾部,你都需要去遍历,顺着链表一个一个找下去,直到找到你要的元素。那放在最前面会导致什么问题?当你要找程八的时候,他会找到数组下标为1的位置,但是它是一个链表,你可以往下面找,但是你不能往上面找到程八。那怎么办呢?
JDK7的解决办法实现:就是你往链表的头部加完之后,他会移动一下。其实就是移动他的头结点,移完之后,他下面的值都可以get到了。这就是JDK1.7中put的思路。
现在我们试着写一个HashMap:
package com.ycc.hashmap;
/**
* @author lenovo
* 仿JDK7的HashMap
*/
public class MyHashMap{
private Entry[] table;
private static Integer CAPACITRY=8;
private Integer size=0;
public MyHashMap(){
//初始化数组,容量刚开始为8
this.table=new Entry[CAPACITRY];
}
public int size() {
//计算size值我们可能会想到遍历数组,但是这样性能不高
//HashMap的这边是定义一个属性size,每次增加元素,删除元素,对size加减就行,然后给他返回就行
return size;
}
public V get(K key) {
//获取到传进来的key的hash值
Integer hash=key.hashCode();
//用哈希值对数组的容量取模,获得数组的下标值。
Integer index=hash%table.length;
//相同key,value覆盖的操纵,他返回的是老的value值
for (Entry entry=table[index];entry!=null;entry=entry.next) {
if(key.equals(entry.k)){
return entry.v;
}
}
return null;
}
public V put(K key, V value) {
//获取到传进来的key的hash值
//HashMap哈希值的获取进行了很多右移和异或的操作(目的:让高四位参与进来运算,让元素分散得更散列,链表的缺点就是查找慢,数组更散列那么get就更方便)
Integer hash=key.hashCode();
//用哈希值对数组的容量取模,获得数组的下标值。
//HashMap在这边是用与操作来获得索引值(二次方数和与操作配合使用)
Integer index=hash%table.length;
//相同key,value覆盖的操纵,他返回的是老的value值
for (Entry entry=table[index];entry!=null;entry=entry.next) {
if(key.equals(entry.k)){
V oldValue=entry.v;
entry.v=value;
return oldValue;
}
}
//添加元素
addEntry(key, value, index);
return null;
}
private void addEntry(K key, V value, Integer index) {
//元素进来的时候,让他先指向原来的数组上的值,然后再
//把当前数组赋值给我们新的元素,这样就达到了插在头部的操作。
table[index]=new Entry(key,value,table[index]);
size++;
}
class Entry{
public K k;
public V v;
public Entry next;
public Entry(K k,V v,Entry next){
this.k=k;
this.v=v;
this.next=next;
}
}
public static void main(String[] args){
MyHashMap myHashMap=new MyHashMap();
for(int i=0;i<10;i++){
String put = myHashMap.put("1" + i, "周" + i);
}
System.out.println(myHashMap.get("1"));
}
}
这时候,你开Debug去运行,查看是否有运用到链表,我们初始的容量是8,所以这时候,put10个数据进去,肯定会运用到数组。可以看到:数组下标为0的位置存放着周9,而他next的元素就是周1,就是链表实现的。
HashMap扩容:
JDK7的时候,Hashmap扩容的时候,当有新的元素进来的时候,他不仅仅会判断是否大于阈值,还会看当前的数组位置是否为空,就算这时候已经大于阈值了,但是当前数组位置为空的时候,他也不会扩容。
数组扩容只有一个办法:只能把元素存入一个新的数组
现在讲一讲JDK8的实现:
jdk1.8中在计算新位置的时候并没有跟1.7中一样重新进行hash运算,而是用了原位置+原数组长度这样一种很巧妙的方式,而这个结果与hash运算得到的结果是一致的,只是会更块。rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
Jdk8是元素在加在链表的尾部,原因是他必须遍历链表,要知道阈值的大小,变化红黑树,还能解决JDK1.7的死循环的情况,一举两得。
ConcurrentModificationException异常,为什么会出现?如何解决?
package com.ycc.hashmap;
import java.util.HashMap;
import java.util.Iterator;
public class ExceptionTest {
public static void main(String[] args){
HashMap hashMap=new HashMap();
hashMap.put("张三","张三");
hashMap.put("李四","李四");
hashMap.put("王五","王五");
hashMap.put("赵六","赵六");
hashMap.put("谢七","谢七");
//modCount=5 modCount代表操作次数,get和put方法中执行一次他都会++
Iterator iterator=hashMap.keySet().iterator();//iterator迭代器初始化,这时候在父类中expectedModCount=5也初始化好了,不会变。
while(iterator.hasNext()){
String key=iterator.next();//第二次循环的时候,next方法中,modCoun就不等于expectedModCount,就会报错
//其实当你多线程的时候,也会出现这个情况,两个值不相等报错。
//这个其实是一个容错机制,当我这个线程在读的过程中,如果有别的线程修改我的值,我就报错,不让你用了。
if(key.equals("张三")){
hashMap.remove(key);//执行完代码之后,modCount=6
}
}
System.out.println(hashMap);
}
}
解决方案:
使用ConcurrentHashMap代替HashMap,ConcurrentHashMap会自己检查修改操作,对其加锁,也可针对插入操作。
Map map = new ConcurrentHashMap();
map.putAll(param);
for (Map.Entry entry : map.entrySet()) {
String key = entry.getKey();
map.remove(key);