HashMap?面试?我是谁?我在哪

本文深入剖析HashMap的工作原理,包括其散列表结构、hash函数实现、碰撞处理策略及红黑树优化等关键技术点,揭示HashMap在Java集合框架中的核心作用。

点击上方“芋道源码”,选择“设为星标

做积极的人,而不是积极废人!

源码精品专栏

 

来源:http://rrd.me/eqCsw

  • 1、为什么用HashMap?

  • 2、HashMap的工作原理是什么?

  • 3、有什么方法可以减少碰撞?

  • 4、HashMap中hash函数怎么是是实现的?

  • 5、拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

  • 6、说说你对红黑树的见解?

  • 7、解决hash 碰撞还有那些办法?

  • 8、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

  • 9、重新调整HashMap大小存在什么问题吗?

  • 10、HashTable

  • 11、HashMap ,HashTable 区别

  • 12、ConcurrentHashMap 原理

  • 13、我们可以使用CocurrentHashMap来代替Hashtable吗?


现在是晚上11点了,学校屠猪馆的自习室因为太晚要关闭了,勤奋且疲惫的小鲁班也从屠猪馆出来了,正准备回宿舍洗洗睡,由于自习室位置比较偏僻所以是接收不到手机网络信号的,因此小鲁班从兜里掏出手机的时候,信息可真是炸了呀,小鲁班心想,微信群平时都没什么人聊天,今晚肯定是发生了什么大事,仔细一看,才发现原来是小鲁班的室友达摩(光头)拿到了阿里巴巴JAVA开发实习生的offer,此时小鲁班真替他室友感到高兴的同时,心里也难免会产生一丝丝的失落感,那是因为自己投了很多份简历,别说拿不拿得到offer,就连给面试邀的公司也都寥寥无几,小鲁班这会可真是受到了一万点真实暴击,不过小鲁班还是很乐观的,很快调整了心态,带上耳机,慢慢的走回了宿舍,正打算准备向他那神室友达摩取取经。

片刻后~

小鲁班:666,听说你拿到了阿里的offer,能透露一下面试内容和技巧吗

达摩:嘿嘿嘿,没问题鸭,叫声爸爸我就告诉你

小鲁班:baba(表面笑嘻嘻,心里MMP)

达摩:其实我也不是很记得了(请继续装),但我还是记得那么一些,如果你是面的JAVA,首先当然是

  • JAVA的基础知识:数据结构(Map,List,Set等),设计模式,算法,线程相关,IO/NIO,序列化等等

  • 其次是高级特征:反射机制,并发与锁,JVM(GC策略,类加载机制,内存模型)等等

小鲁班:问这么多内容,那岂不是一个人都面试很久吗?

达摩:不是的,面试官一般都会用连环炮的方式提问的。

小鲁班:你说的连环炮是什么意思鸭?

达摩:那我举个例子

就比如问你HashMap是不是有序的?

你回答不是有序的。那面试官就会可能继续问你,有没有有序的Map实现类呢?

你如果这个时候说不知道的话,那这块问题就到此结束了。如果你说有TreeMap和LinkedHashMap。

那么面试官接下来就可能会问你,TreeMap和LinkedHashMap是如何保证它的顺序的?

如果你回答不上来,那么到此为止。如果你说TreeMap是通过实现SortMap接口,能够把它保存的键值对根据key排序,基于红黑树,从而保证TreeMap中所有键值对处于有序状 态。LinkedHashMap则是通过插入排序(就是你put的时候的顺序是什么,取出来的时候就是什么样子)和访问排序(改变排序把访问过的放到底部)让键值有序。

那么面试官还会继续问你,你觉得它们两个哪个的有序实现比较好?

如果你依然可以回答的话,那么面试官会继续问你,你觉得还有没有比它更好或者更高效的实现方式。。无穷无尽深入,直到你回答不出来或者面试官认为问题到底了

小鲁班捏了一把汗,我去。。。这是魔鬼吧,那我们来试试呗(因为小鲁班刚刚在自习室才看了这章的知识,想趁机装一波逼,毕竟刚刚叫了声爸爸~~)

于是达摩and小鲁班就开始了对决:

1、为什么用HashMap?

  • HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射

  • HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改

  • HashMap是非synchronized,所以HashMap很快

  • HashMap可以接受null键和值,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)

2、HashMap的工作原理是什么?

  • HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。

     

  • 以下是HashMap初始化 ,简单模拟数据结构

Node[] table=new Node[16]  散列桶初始化,table
class Node {
 hash;//hash值
      key;//键
 value;//值
 node next;//用于指向链表的下一层(产生冲突,用拉链法)

}
  • 以下是具体的put过程(JDK1.8版)

1、对Key求Hash值,然后再计算下标

2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)

3、如果碰撞了,以链表的方式链接到后面

4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

5、如果节点已经存在就替换旧值

6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

  • 以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

  

3、有什么方法可以减少碰撞?

  • 扰动函数可以减少碰撞,原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode。)

  • 使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

4、HashMap中hash函数怎么是是实现的?

我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式,我们来看看JDK1.8的源码是怎么做的(被楼主修饰了一下)

static final int hash(Object key) {
    if (key == null){
        return 0;
    }
     int h;
     h=key.hashCode();返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      //其中n是数组的长度,即Map的数组部分初始化长度
     return  (n-1)&(h ^ (h >>> 16));
}

简单来说就是

1、高16bt不变,低16bit和高16bit做了一个异或(得到的HASHCODE转化为32位的二进制,前16位和后16位低16bit和高16bit做了一个异或)

2、(n·1)&hash=->得到下标

5、拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

6、说说你对红黑树的见解?

1、每个节点非红即黑

2、根节点总是黑色的

3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

4、每个叶子节点都是黑色的空节点(NIL节点)

5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

7、解决hash 碰撞还有那些办法?

开放定址法。

当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。

按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

下面给一个线性探查法的例子  

问题:已知一组关键字为(26,36,41,38,44,15,68,12,06,51),用除余法构造散列函数,用线性探查法解决冲突构造这组关键字的散列表。

解答:为了减少冲突,通常令装填因子α由除余法因子是13的散列函数计算出的上述关键字序列的散列地址为(0,10,2,12,5,2,3,12,6,12)。

前5个关键字插入时,其相应的地址均为开放地址,故将它们直接插入T[0],T[10),T[2],T[12]和T[5]中。

当插入第6个关键字15时,其散列地址2(即h(15)=15%13=2)已被关键字41(15和41互为同义词)占用。故探查h1=(2+1)%13=3,此地址开放,所以将15放入T[3]中。

当插入第7个关键字68时,其散列地址3已被非同义词15先占用,故将其插入到T[4]中。

当插入第8个关键字12时,散列地址12已被同义词38占用,故探查hl=(12+1)%13=0,而T[0]亦被26占用,再探查h2=(12+2)%13=1,此地址开放,可将12插入其中。

类似地,第9个关键字06直接插入T[6]中;而最后一个关键字51插人时,因探查的地址12,0,1,…,6均非空,故51插入T[7]中。

8、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置  

9、重新调整HashMap大小存在什么问题吗?

  • 当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。(多线程的环境下不使用HashMap)

  • 为什么多线程会导致死循环,它是怎么发生的?

HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

(这个过程比较烧脑,暂不作流程图演示,有兴趣去看看我的另一篇博文"HashMap扩容全过程")

达摩:哎呦,小老弟不错嘛~~意料之外呀

小鲁班:嘿嘿,优秀吧,中场休息一波,我先喝口水

达摩:不仅仅是这些哦,面试官还会问你相关的集合类对比,比如:

10、HashTable

  • 数组 + 链表方式存储

  • 默认容量:11(质数 为宜)

  • put:

  • - 索引计算 : (key.hashCode() & 0x7FFFFFFF)% table.length

  • 若在链表中找到了,则替换旧值,若未找到则继续

  • 当总元素个数超过容量*加载因子时,扩容为原来 2 倍并重新散列。

  • 将新元素加到链表头部

  • 对修改 Hashtable 内部共享数据的方法添加了 synchronized,保证线程安全。

11、HashMap ,HashTable 区别

  • 默认容量不同。扩容不同

  • 线程安全性,HashTable 安全

  • 效率不同 HashTable 要慢因为加锁

12、ConcurrentHashMap 原理

1、最大特点是引入了 CAS(借助 Unsafe 来实现【native code】)

  • CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

  • Unsafe 借助 CPU 指令 cmpxchg 来实现

  • 使用实例:

1、对 sizeCtl 的控制都是用 CAS 来实现的

1、sizeCtl :默认为0,用来控制 table 的初始化和扩容操作。

  • -1 代表table正在初始化

  • N 表示有 -N-1 个线程正在进行扩容操作

  • 如果table未初始化,表示table需要初始化的大小。

  • 如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。

4、CAS 会出现的问题:ABA

  • 对变量增加一个版本号,每次修改,版本号加 1,比较的时候比较版本号。

13、我们可以使用CocurrentHashMap来代替Hashtable吗?

  • 我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

此时躺着床上的张飞哄了一声:睡觉了睡觉了~

见此不太妙:小鲁班立马回到床上(泉水),把被子盖过头,心里有一丝丝愉悦感,不对。好像还没洗澡。。。

by the way

CocurrentHashMap在JAVA8中存在一个bug,会进入死循环,原因是递归创建ConcurrentHashMap 对象,但是在1.9已经修复了,场景重现如下

public class ConcurrentHashMapDemo{

    private Map<Integer,Integer> cache =new ConcurrentHashMap<>(15);

    public static void main(String[]args){
        ConcurrentHashMapDemo ch =    new ConcurrentHashMapDemo();
        System.out.println(ch.fibonaacci(80));
    }

    public int fibonaacci(Integer i){
        if(i==0||i ==1) {
            return i;
    }

    return cache.computeIfAbsent(i,(key) -> {
        System.out.println("fibonaacci : "+key);
        return fibonaacci(key -1)+fibonaacci(key - 2);
    });

    }
}


欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:


如果你喜欢这篇文章,喜欢,转发。

生活很美好,明天见(。・ω・。)ノ♡

<think>首先,用户的问题是关于HashMap的定义。用户说:“我想了解HashMap的定义 请问HashMap的定义”。所以,我需要专注于回答HashMap的定义。 我有几个引用,但这些引用是关于HashMap的讨论,我需要从中提取HashMap的定义。用户指定了系统级指令,包括使用中文回答、格式化数学表达式等,但HashMap定义不涉及数学,所以不需要数学格式。 关键指令: - 回答结构清晰,逐步解决问题。 - 尽量保证回答真实可靠。 - 生成相关问题。 - 在引用的段落末尾自然地添加引用标识。 从引用中提取HashMap定义: - 引用[1]:提到“HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现”。引用[1]还讨论了构造函数等。 - 引用[2]:解释了Map和HashMap的区别,“Map是一个interface, HashMap是一个class 是Map的一个实现”。 - 引用[4]:类似地,“HashMap实现了Map接口,继承AbstractMap”。 - 引用[5]:展示了HashMap的用法示例。 所以,HashMap的定义是:HashMap是Java中的一个类,它实现了Map接口,用于存储键值对(key-value pairs),允许键和值都为null(基于引用[3]),并且提供高效的查找、插入和删除操作。 结构化回答: - 先简明扼要地定义HashMap。 - 然后解释其关键特征,如实现Map接口、继承AbstractMap、线程不安全、允许null键值等。 - 引用相关信息时添加引用标识。 例如: - HashMap是Java集合框架中的一个类,它实现了Map接口,用于存储键值对映射[^1]。 - 详细信息:[^1]提到HashMap继承自AbstractMap类。 最后,生成相关问题: - 基于用户的问题,可能的后续问题包括: 1. HashMap和HashTable的区别? 2. HashMap如何处理冲突? 3. HashMap的性能如何? 4. HashMap的常用方法? 5. 为什么HashMap允许null键值? 用户指令说“生成相关问题”,所以我需要在回答末尾添加相关问题部分。 完整回答结构: 1. 清晰定义HashMap。 2. 逐步解释关键点。 3. 自然地添加引用。 4. §§相关问题§§部分。 回答: HashMap是Java中的一个类,位于java.util包中。它实现了Map接口,继承自AbstractMap类,用于存储键值对(key-value pairs)。HashMap允许键(key)和值(value)为null,并且提供快速的查找、插入和删除操作[^1][^4]。 关键特征: - 实现Map接口:这意味着HashMap遵循Map的契约,支持键值对的存储和操作[^2]。 - 继承AbstractMap:AbstractMap提供了Map接口的默认实现,减少了实现工作[^4]。 - 线程不安全:HashMap不是线程安全的类,如果在多线程环境中使用,可能导致并发问题[^3]。 - 允许null键值:HashMap允许键和值为null,而HashTable不允许[^3][^5]。 HashMap通过哈希表(hash table)实现,使用数组和链表(或红黑树在JDK 1.8及之后)来处理哈希冲突[^1]。 相关问题: 1. HashMap和HashTable的主要区别是什么? 2. HashMap如何处理哈希冲突? 3. 为什么HashMap在多线程环境下不安全? 4. HashMap的常用方法有哪些? 5. HashMap的加载因子和扩容机制是怎样的? 确保引用正确: - 定义部分引用[1][^1] and [4][^4]. - 特征部分引用[2][^2], [3][^3], [5][^5]. 最终输出。 </think>### HashMap的定义 HashMap是Java集合框架中的一个核心类,用于存储键值对(key-value pairs)的数据结构。它位于`java.util`包中,实现了`Map`接口并继承自`AbstractMap`类。HashMap的核心功能是提供高效的键值映射,允许键(key)和值(value)为`null`,并支持快速的插入、删除和查询操作[^1][^4]。 #### 关键特征详解 1. **实现Map接口**: HashMap遵循`Map`接口的规范,定义了键值对的存储规则。这意味着它支持基本操作如`put(key, value)`(添加键值对)、`get(key)`(获取值)和`remove(key)`(删除键值对)。Map接口定义了键值映射的逻辑,HashMap作为具体实现提供了高效的执行[^2][^4]。 2. **继承AbstractMap类**: AbstractMap提供了Map接口的骨干实现,简化了HashMap的开发。例如,它默认实现了部分Map方法(如`equals()`和`hashCode()`),减少重复代码。但HashMap覆盖了这些方法以实现特定优化[^1][^4]。 3. **数据结构与哈希机制**: HashMap底层使用数组(称为Entry数组或Node数组)和链表(或红黑树,在JDK 1.8+)来处理哈希冲突。每个键通过哈希函数(hash function)计算索引位置,如果多个键哈希到相同索引,它们会被存储在链表结构中(JDK 1.8后当链表过长时转为红黑树,提升查询效率)[^1][^3]。 4. **允许null键值**: 与HashTable不同,HashMap允许键或值为`null`。HashMap对`null`键特殊处理(其哈希码为0),不会抛出异常,这增加了灵活性[^3][^5]。 5. **线程不安全**: HashMap不是线程安全的类(如引用[3]所述),在多线程环境中未加锁的`put()`或`get()`操作可能导致数据不一致。推荐在并发场景使用`ConcurrentHashMap`或`HashTable`[^3]。 6. **初始化与扩容**: - 默认初始容量为16,加载因子为0.75(表示当容量达到75%时触发扩容)。 - 扩容时大小翻倍(例如16变为32),以保持高效的操作性能。扩容临界点通过`capacity * loadFactor`计算[^1][^3]。 #### 简单示例 以下代码展示了HashMap的基本定义和使用: ```java import java.util.HashMap; import java.util.Map; public class HashMapExample { public static void main(String[] args) { // 定义HashMap:键为String类型,值为Object类型 Map<String, Object> map = new HashMap<>(); // 添加键值对:允许null键和null值 map.put("name", "zhangsan"); map.put("age", 20); map.put(null, "who"); // null键 map.put("info", null); // null值 // 获取值 System.out.println(map.get("name")); // 输出: zhangsan } } ``` 此示例体现了HashMap的灵活性和易用性:定义时推荐使用接口类型`Map`(面向接口编程),但实现基于`HashMap`类[^2][^5]。 HashMap因其高效性(平均时间复杂度为$O(1)$)被广泛应用于缓存、数据索引等场景[^1][^3]。如果您有更多具体问题,欢迎继续提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值