面试最多的HashMap

前一段时间去面试,被面试官问到了很多关于HashMap的知识点,当时懵的一逼.回去之后仔细去网上看了一下资料.也是看的不怎么懂.还好最近公司大神进行了一次关于HashMap的技术分享,学到了很多东西!

面试碰到的问题
  1. 简单介绍一下HashMap?
  2. HashMap是如何进行数据的存取的?
  3. HashMap如何进行扩容的?
  4. HashMap的底层是什么结构?
  5. HashMap的底层为什么选用这种结构?
首先我们先针对上面的问题进行详细的解答
什么是HashMap呢?

想必大家肯定基本都清楚,如果这点东西不清楚的话还是需要巩固一下基础的java知识…
我们都清楚HashMap是一个key-value形式的集合,1.8JDK版本底层是由数组加(单向)链表以及红黑树(暂不讲解)组成
在这里插入图片描述
从图中可以看出来,数组中都有一个角标,那么此时我们产生了疑问,这个是做什么的哪?
此时我们可以去HashMap中的源码一探究竟

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

首先我们可以看到HashMap继承了AbstractMap<K,V>,实现了Map<K,V>, Cloneable, Serializable这三种接口.
然后我们观察HashMap的构造函数:
一共有四种构造函数,我们常用的基本就是默认的构造函数,此时可以看到默认参数中有一个默认的初始容量大小(16),也就是我们最常用的是默认为容量大小为16的HashMap.
那么为什么默认容量为16呢?
我们可以看一下默认容量的计算公式:

initialCapacity=(需要存储的元素个数/加载因子)+1
loadFactor即为加载因子,默认为0.75

在jdk中,当我们new HashMap并且制定初始化容量capacity时.jdk会帮我们取第一个大于capacity的2次幂.
具体实现是(参照阿里的建议):

  1. 先把capacity - 1
  2. 进行多次无符号右移和或运算
  3. 最后再 + 1
    比如,我们new HashMap(3),会生成一个4容量的map,5->8。10->16。

但是,hashmap在我们存放的数据大于初始化容量*负载因子(默认0.75)时就会自动扩容,自动扩容是非常消耗性能的。因为元素要重新hash分配。

那么当我们生成了一个7容量的map,jdk会生成一个8容量的map,那么存放到8 * 0.75 = 6个元素时就会扩容了,跟我们预想放7个有偏差,所以阿里就推出了这个建议。

initialCapacity = (int) ((float) expectedSize / 0.75F + 1.0F)

这样我们想放7个元素,就设置 7 / 0.75 + 1 = 10,经过jdk会生成16的容量,这样我们存放7个元素就不会因为扩容而损失性能了,当然会消耗一部分内存。

总结:所以当我们不知道我们要存取的数据有多少个时,我们直接用默认的容量大小的构造参数既可.
如果我们知晓了我们要存取的key-value个数时,我们可以根据上面的公式进行计算推算出所需要的容量大小.这样可以有效的提高性能

//默认的构造函数
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定"容量大小"的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定"容量大小"和"加载因子"的构造函数
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
            .......
}
//包含"子Map"的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

我们来查看一下源码中的各种参数

/**
 * The load factor used when none specified in constructor.
* HashMap默认加载因子 0.75
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * The default initial capacity - MUST be a power of two.
 * HashMap的默认初始化大小16,必须是2的幂次
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 * HashMap的最大容量
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

我们继续刚才的话题,数组中有角标,每个数组下面都有单向链表(是分散的并不像图中那么紧密).

这样我们就需要讨论HashMap是如何进行put数据的?

在这里插入图片描述

// 求Key的Hash值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以简化为: 
index = HashCode(Key) & (Length - 1)

上面的公式就是进行key的Hash值得获取,他会根据你传入的key进行计算并返回一个int值也就是数组的角标索引值.
代码中的h = key.hashCode()为你所传入的key的HashCode值
h >>> 16为高位参与运算
我们观察简化后的公式index = HashCode(Key) & (Length - 1)
length为我们创建HashMap时的初始容量大小值
举一个例子:

 public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>(16);
        //进行put值得模拟运算
        System.out.println("a".hashCode());  //97
        //算法
        System.out.println("a".hashCode() & (16 - 1)); //1
    }

我们可以获取到经过计算后的索引值为1. 也就是说 基本可以确认上述例子中的put值时存储的数组位置为1
虽然说我们确定了数组索引的位置,但是我们存储的key-value到底去了哪里?
首先上面我们确定了key-value的数组位置,我们需要知道具体存储的链表位置.
我们继续分析链表结构:一个entity包含key,value,hash,next四个区域.
key就是我们存储的key
value就是我们存储的value
hash就是我们经过计算获取到的hashcode值
next就是下一个entity的地址
这样假设我们刚才存储的是第一个key-value,那么数组角标为1的就会直接在下面产生了一个链表(此时只有一个entity),那么我们如果再次存储一个key-value会发生什么情况哪?
再次存储key-value会有两种情况

  1. 数组角标还是1 : 这种情况又分为两种
  • 第一种是key相同:  如果我们又存储了key为"a"的key-value,但是此时value不相同或者不相同,由于hashcode值是相同的,那么此时会直接将之前的相同hashcode值所在的entity进行覆盖.  
    
  • 第二种是key不相同:但是此时如果key不是"a",虽然此时数组角标还是在1(我们要明白并不是key="a"才是1)下面,但是此时的hashcode与"a"的不相同,那么会出现entity追加的情况.那么追加是以什么形式追加的哪?  
      自上而下,会将之前的entity挤压到下面.(貌似是开发者认为越后面存储的key-value会比之前存储得更容易被使用)
    
  1. 数组角标不是1 : 这种情况就跟刚才说的是一个道理.不用再次阐述了
我们讨论了put数据,现在我们讨论如何get数据?

在这里插入图片描述我们在hashMap中获取数据时 我们需要key.经过上面的讲解我们已经清楚了key的hashcode的重要性,大家应该能想出是如何获取value的.
我们通过get(“a”),计算key="a"的数组索引值.能知道获取的目标在哪个数组索引处, 紧接着我们可以根据计算出hashcode值,自上而下根据hashcode值进行匹配如果不相同继续根据next进行搜索,如果相同那么直接返回value.

下面我们思考hashcode有没有重写的必要
下面是对象没有重写HashMap和equals方法

当我们利用HashMap存储对象时.假设student对象并没有进行equals和hashcode的重写会发生什么情况?
我们都知道比较两个对象,没有重写的equals方法比较的是地址值

 public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>(16);
        //创建student1对象
        student student1 = new student();
        //创建student2对象
        student student2 = new student();
        //比较两者equals
        System.out.println(student1.equals(student2));//false
    }

我们紧接着查看没有重写hashcod方法的两个对象的hashcode大小

   public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>(16);
        //创建student1对象
        student student1 = new student();
        //创建student2对象
        student student2 = new student();
        //比较两者equals
        System.out.println(student1.equals(student2));//false
        //计算两个对象的hashcode值
        System.out.println(student1.hashCode());//109961541
        System.out.println(student2.hashCode());//670700378
        System.out.println(student1.hashCode()==student2.hashCode());//false
    }

我们继续下面的操作,我们将对象存储到map中查看到底存储了几个对象

    public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>(16);
        //创建student1对象
        student student1 = new student();
        //创建student2对象
        student student2 = new student();
        //比较两者equals
        System.out.println(student1.equals(student2));//false
        //计算两个对象的hashcode值
        System.out.println(student1.hashCode());//109961541
        System.out.println(student2.hashCode());//670700378
        System.out.println(student1.hashCode()==student2.hashCode());//false

        //下面进行没有重写hashcode和equals方法的hashMap存储
        map.put(student1,student1);
        map.put(student2,student2);
        //打印map的size
        System.out.println(map.size());//2
        //我们对其中一个对象进行赋值操作,虽然是赋值但是其对象的hashcode值是没有变化的
        student1.setName("小哲");
        map.put(student1,student1);
        System.out.println(map.size());//2
    }

从上面可以看出来,我们put了三次但是实际上map的大小只有2个.这是因为我们虽然进行了赋值,但是实际的hashcode 是没有变化的.
但是这样的结果是我们用hashmap存储对象时所期望的吗?

下面我们重写HashMap和equals方法看看有什么结果
 public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>(16);
        //创建student1对象
        student student1 = new student();
        //创建student2对象
        student student2 = new student();
        //比较两者equals
        System.out.println(student1.equals(student2));//true
        //计算两个对象的hashcode值
        System.out.println(student1.hashCode());//961
        System.out.println(student2.hashCode());//961
        System.out.println(student1.hashCode()==student2.hashCode());//true

        //下面进行没有重写hashcode和equals方法的hashMap存储
        map.put(student1,student1);
        map.put(student2,student2);
        //打印map的size
        System.out.println(map.size());//1
        //我们对其中一个对象进行赋值操作,虽然是赋值但是其对象的hashcode值是没有变化的
        student1.setName("小哲");
        map.put(student1,student1);
        System.out.println(map.size());//2
    }

我们可以看到当我们重写了HashMap和equals方法后.没有赋值的对象put了两次但是map.size只有1.此时说明两个对象的hashcode值是一样的 ,后put的把上一个put的覆盖了
但是当我们对其中一个对象进行赋值的操作时,此时的hahscode有不一样了.所以最后put了三次,map.size为2
这才是我们利用HashMap存储对象时所期望的把应该?~

上面我们讲解了HashMap 的存取,下面我们想一下为什么HashMap是数据加链表的结构哪?

我们都清楚数组和链表的特点:

  1. 数组: 查询速度快,增删速度慢
  2. 链表: 增删速度快,查询速度慢
    也正是因为这两者的特性,所以HashMap才用了数组加链表的结构(1.8前),之后加入了红黑树.当链表数量大于一定值时(默认为8)会自动转化为红黑树,当链表结构又小于8时,又会自动转化为链表.
    那么为什么引入了红黑树哪?
    刚才已经说明链表查询慢,如果链表数量太多了,我们获取值得时候那么会非常的慢影响效率.
    这样把链表限制到一定值 ,会相对的提高性能
下面我们讲解HashMap的扩容机制

前面提到过HashMap的默认初始容量initialCapacity为16.加载因子loadFactor为0.75
当我们存储的元素个数 hashMap的size>initialCapacity * loadFactor时就会进行扩容
假如初始容量大小为16 16*0.75 = 12 即当元素个数大于12个的时候会发生一次扩容.

一次扩容操作可划分为两个步骤:
  1. resize : 也就是创建一个新的数组,长度为原来的两倍
  2. rehash : 将之前的HashMap中的数组进行遍历,把所有的entity重新Hash到新的数组中
    此时我们需要考虑一下.这个扩容机制明显是很消耗性能的.
    假如我们进行HashMap创建之前知道了我们要存储的数据的个数,我们完全可以直接指定容量进行创建,就算后期超过了预算,他也会扩容.但是次数肯定要比我们使用默认的容量进行创建HashMap扩容次数要少.能提高不少性能~

上述理解仅限个人哦~观点冲突不要打我!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值