Java——HashMap的基础知识和常见的面试题

1、HashMap的特性?

  • HashMap存储键值对实现快速存取,允许为null。key 值不可重复,若key值重复则覆盖。
  • 非同步,线程不安全
  • 3.底层是hash表,不保证有序(比如插入的顺序)

2、HashMap 的底层原理是什么?

答:基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过putget存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。

3、HashMap put 是如何实现的?

答:

  • 计算关于keyhashcode值(与Key.hashCode的高16位做异或运算
  • 如果散列表为空时,调用resize()初始化散列表
  • 如果没有发生碰撞,直接添加元素到散列表中去
  • 如果发生了碰撞(hashCode值相同),进行三种判断
    • key地址相同或者equals后内容相同,则替换旧值
    • 如果是链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
    • 如果是红黑树结构,就调用树的插入方法
  • 如果桶满了大于阀值,则resizz()进行扩容

4、hashMap 中什么时候需要进行扩容?

  • 初始化数组 table
  • 当数组 table 的元素个数 (size) 达到阙值时即 ++size > load factor (默认0.75)* capacity 时,也是在 putVal 函数中

5、扩容 resize() 又是如何实现的?

答:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。

详细来说是:通过判断旧数组的容量是否大于0来判断数组是否初始化过。

  • :进行初始化

    • 判断是否调用无参构造器

      • 是:使用默认的大小和阙值(16);
      • 否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数。
    • :进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中。

6、HashMapget是如何实现的?

答:keyhashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有 hash 冲突,则利用 equals 方法去遍历链表查找节点。

7、HashMap hash 函数是怎么实现的?还有哪些hash函数的实现方式?

答:keyhashCodehash 操作,并与高16位做异或运算。还有平方取中法除留余数法伪随机数法

# 计算方法
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

8、为什么不直接将 key 作为哈希值而是与高16位做异或运算?

答:因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16为做异或运算使得在做 &运算确定数组的插入位置时,此时的低位实际是高位与低位的结合增加了随机性减少了哈希碰撞的次数

HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂

9、为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

  • 为了数据的均匀分布减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
  • 输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

10、当两个对象的 hashCode 相等时会怎么样?

答:会产生哈希碰撞,若key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储

11、如果两个键的 hashcode 相同,你如何获取值对象?

答: HashCode 相同,通过 equals 比较内容获取值对象

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

答:超过阙值会进行扩容操作,概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去。

13、HashMap HashTable 的区别

  • 相同点:都是存储key-value键值对的。
  • 不同点:
    • HashMap允许Key-valuenullHashTable不允许;
    • HashMap 没有考虑同步,是线程不安全的, HashTable是线程安全的HashTable 给api套上了一层synchronized修饰;
    • HashMap 继承于 AbstractMap 类, HashTable 继承于 Dictionary 类。
    • 迭代器(Iterator)HashMap的迭代器(Iterator)fail-fast迭代器,而Hashtableenumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException
    • 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为 “原始容量x2”Hashtable默认的容量大小是11;增加容量时,每次将容量变为 “原始容量x2 + 1”
    • 添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()

14、请解释一下 HashMap 的参数 loadFactor ,它的作用是什么?

答:loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor

15、传统HashMap的缺点(为什么引入红黑树?)

答:JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。

16、平时在使用 HashMap 时一般使用什么类型的元素作为Key?

答:选择IntegerString这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的。

17、为什么不使用AVL树而使用红黑树?

AVL(平衡二叉查找树)树和红黑树有几点比较和区别:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

总结

(1)AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。

(2)两种实现都缩放为 O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。

(3)在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。

(4)两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。

18、HashMap在多线程条件下会出现什么问题?

  • 多线程putget时出现死循环,导致CPU利用率过高;
  • 多线程put,可能导致元素丢失:主要问题出在addEntry()方法的new Entry<K,V>(hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失;
  • putnull元素后get出来的却是null

19、HashMapConCurrentHashMap的区别?

  • HashMap不支持并发操作,没有同步方法,ConcurrentHashMap支持并发操作,通过继承 ReentrantLock(JDK1.7重入锁)/CASsynchronized(JDK1.8内置锁)来进行加锁(分段锁),每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
  • JDK1.8之前 HashMap 的结构为数组+链表,JDK1.8之后HashMap的结构为数组+链表+红黑树;JDK1.8之前ConcurrentHashMap的结构为segment数组+数组+链表,JDK1.8之后ConcurrentHashMap的结构为数组+链表+红黑树。

20、HashMapHashSet的区别

  • HashSet实现了Set接口,仅存储对象;HashMap实现了 Map接口,存储的是键值对;
  • HashSet 底层其实是用 HashMap 实现存储的HashSet封装了一系列HashMap的方法。依靠HashMap来存储元素值,(利用hashMapkey键进行存储),而value值默认为Object对象。所以HashSet也不允许出现重复值,判断标准和HashMap判断标准相同,两个元素的hashCode相等并且通过equals()方法返回true
  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值