HashMap的十个经典问题

整理了一些hashmap的一些经典问题,和大家分享一下…

1、谈谈HashMap的一些特性

* HashMap存储键值对实现快速存取,key值不可重复,但是允许为null【放在table[0]的位置】,如果key值重复则覆盖
* HashMap线程不安全,非同步
* 底层是hash表,不能保证有序

2、HashMap的结构简介

java8之前采用数组(table)【Node[]】 + 链表(Node节点组成的链表)的结构
java8之后采取数组 + 链表 + 红黑树的结构(单条链上数组的长度 > 8 TREEIFY_THRESHOLD 且 数组的长度 >= 64 MIN_TREEIFY_CAPACITY的时候进行树化)

3、HashMap中的一些定位算法

1、hashcode的重载
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

2、hash函数(为什么需要使用hash函数 : 单纯使用hashCode的值大多数情况会小于2的16次方,所以会浪费hashcode的高16位,如果参与进来使得结果更加散列,同时增加了随机性,减少了哈希碰撞的次数)
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3、定位函数
     index = hash & (cap - 1)

4、将数组长度控制为2的幂次?
(为什么要控制为2的幂次 : 
    * 如果控制为2的幂次,则保证了cap - 1二进制位为11111...1 hash & 当前值的结果 和 hash % cap一致
    * 使用连续的 1.....1,保证了不同的key算出相等结果的概率较小,分布均匀,减少了hash碰撞
)
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

4、说一下hashmap中的get是怎么得到结果的?

* 通过hash & (table.length - 1) 获取该key对应的数据节点的槽 tab[?]
* 如果首节点为null 直接返回null
* 判断首节点的key是否和目标值相同
    如果相同则直接返回首节点
    如果不相同的话,判断首节点的状态
        * 如果是链表的话,链表查找,返回结果
        * 如果是红黑树的话,红黑树查找,返回结果

    final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

5、说一下hashmap中的put是怎么得到结果的?

源码太多了 这里就不放详细内容了。放一下方法名: public V put(K key, V value) {...} => putVal(hash(key), key, value, false, true);
说一下过程:
    * 计算关于key的hashcode值(key的高16位 异或 key的低16位)
    * 如果散列表为空,进行resize()
    * 是否发生了碰撞
        * 如果发生了碰撞
            key的地址相同,或者equals相同,直接进行替换旧值
            如果不相同,判断状态进行(链表尾插 or 红黑数插入)
        * 如果没有发生碰撞
            * 直接加入元素
        * 加入数据之后还需要判断是否需要转换为红黑树
        	* 如果当前的链表长度超过8 而且数组的长度超过64的时候,需要把当前哈希冲突的链表转换为红黑树

    * 如果桶达到了阈值,进行resize扩容

6、说一下hashMap什么时候需要扩容,扩容又是如何实现的?

1、初始化数组的时候需要调用 resize
2、在putVal的时候,如果发现当前加入值之后已经超过了 cap * 0.75,进行扩容

扩容的细节:
1、在初始化的时候
    * 判断是否调用了无参构造器
        * 调用无参构造器 : 默认 16 0.75
        * 如果没有调用无参构造器 : 调用tableSizefor处理后的容量(2的整数次幂) 和 负载因子

2、数组已经进行了初始化
    将数组的大小变成原来的两倍,然后通过重新hash的方式,遍历整个老的结构,将所有元素重新分配到新的结构中去

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

HashCode相同,说明会出现哈希碰撞,如果当前key值相同则替换旧值(调用equals),否则链接到链表后面,链表长度超过8的时候转为红黑树

8、HashMap的负载因子有什么用? 为什么默认0.75?

loadFactor是HashMap的拥挤程度,影响hash操作到同一个位置的概率,默认HashMap的负载因子为0.75
	当谈到为什么0.75的时候:
 		原因是如果设置成1。这样会发生大量的hash碰撞。有些位置的链表会很长,就不利于查询。
 		省空间而费时间。如果设置成0.5,hash碰撞的几率小了很多,但是会频繁扩容,费空间而省时间。
 		大佬们经过研究,0.75的数值比较均衡,在空间和时间做了个取舍。
 参考链接: https://www.jianshu.com/p/234750beb6f3

9、为什么引入红黑树?

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

10、在使用HashMap的时候使用什么作为Key,可以使用自己的class作为key吗?

选择Integer,String这些不可变的类型,这些类很规则的复写了hashCode 和 equals方法
可以的,但是需要去重写equals()和hashCode()方法。
参考链接: https://blog.csdn.net/weixin_41459547/article/details/88421677?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param

总体题目参考链接: https://www.cnblogs.com/zengcongcong/p/11295349.html

你可能感兴趣的:(java基础面试题系列)