全面解析HashMap(哈希碰撞,哈希扩容,“死锁”解决,手写HashMap)

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<String,String> hashMap=new HashMap<String,String>();
        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<K,V>{

    private Entry<K,V>[] 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<K,V> 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<K,V> 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<K,V>{
        public K k;
        public V v;
        public Entry<K,V> next;
        public Entry(K k,V v,Entry<K,V> next){
            this.k=k;
            this.v=v;
            this.next=next;
        }
    }


    public static void main(String[] args){
        MyHashMap<String,String> myHashMap=new MyHashMap<String,String>();
        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<String,String> hashMap=new HashMap<String,String>();
        hashMap.put("张三","张三");
        hashMap.put("李四","李四");
        hashMap.put("王五","王五");
        hashMap.put("赵六","赵六");
        hashMap.put("谢七","谢七");
        //modCount=5  modCount代表操作次数,get和put方法中执行一次他都会++

        Iterator<String> 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<String, String> map = new ConcurrentHashMap<String, String>();			
map.putAll(param);
for (Map.Entry<String, String> entry : map.entrySet()) {
	String key = entry.getKey();
	map.remove(key);

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值