查看原文
其他

Java 面试八股文之基础篇(三)

Dobby kim 憨憨二师兄 2021-11-02

前言


这是系列文章【 Java 面试八股文】基础篇的第三期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过点击文末给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多少,我们正式开始吧!

Java 基础篇(三)

1、了解集合的 fail-fast 与 fail-safe 机制么?


当我们使用迭代器(Iterator)对 java.util 包下的 Collection 或 Map 进行遍历(不包含 java.util.concurrent 包下的容器)。如果在遍历的过程中,对容器进行了添加元素或删除元素的操作,则会抛出 ConcurrentModificationException。

如示例程序:

import java.util.*;

public class FailFast {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (list.contains(1))
                list.remove(next);
        }
    }
}

该程序运行的结果为:

Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at com.github.test.FailFast.main(FailFast.java:14)

根据程序报错信息,我们可以找到 ArrayList 中的私有内部类 Itr 中的 checkForComodification 方法的源代码:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

每当迭代器遍历一个元素前,都会执行 checkForComodification 方法,该方法会检测 modCount 和 expectedModCount 的值是否相等。当我们改变了容器元素的个数时,modCount 就会发生变化,导致 modCount != expectedModCount,就会抛出 ConcurrentModificationException,这就是 fail-fast 机制。fail-fast 是一种错误检测机制,如果我们的场景中,有多个线程同时操作集合,而错误地导致集合的元素个数发生改变,fail-fast 机制便可以快速帮助我们发现问题,从而中断程序的执行。

讲完了 fail-fast,我们再来谈一谈什么是 fail-safe。

先来看一段代码:

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class FailSafe {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (list.contains(1))
                list.remove(next);
        }

        System.out.println(list);
    }
}

该程序执行的结果为:

[2, 3]

本程序中,我们使用了 CopyOnWriteArrayList 来代替原本的 ArrayList,并且该程序没有抛出任何异常,这就得益于 CopyOnWriteArrayList 的 fail-safe 机制。

那到底什么是 fail-safe 呢?

fail-safe 是一种为了避免出现 ConcurrentModificationException 的机制。ConcurrentModificationException 异常出现的原因是我们在遍历集合时,直接在原集合上进行元素的添加,删除等操作。而 fail-safe 机制则会使得任何对集合结构的修改都在原集合的拷贝中进行。java.util.concurrent 包下的容器都实现了 fail-safe 机制,譬如 CopyOnWriteArrayList,ConcurrentHashMap 等。

fail-safe 机制的优点是避免了 ConcurrentModificationException;其缺点有二:

  1. 需要复制集合,造成了额外的内存开销
  2. 无法保证读取到的数据是目前集合中最新的内容
总结

本题涉及到的 fail-fast 和 fail-safe 机制都是一种软件设计的思想。

fail-fast 机制只是用于帮助我们快速地发现当我们在使用并发编程时存在的 bug,JDK 文档中也提出,迭代器的 fail-fast 行为并不是一定能够得到保证的,我们不应该编写依赖于此异常的代码;其实不难想到 ConcurrentModificationException 抛出的条件只是看 modCount != expectedModCount,modCount 发生变化的时候就是集合中元素个数发生变化的时候,反言之,当我们仅仅是修改了集合中的某个元素的值,那么 modCount 就不会发生变化。所以,我们可以想象在并发的环境下,fail-fast 机制并不会保障你的程序一定是没有问题的。

而对于 fail-fast 有如下几种解决或改善方案:

  1. 如果我们使用的是 java.util 包下的容器类,就需要在任何能够改变 modCount 的操作上加锁,同时在迭代器操作时对该类的对象加锁。这种做法是不推荐的,因为增删造成的同步锁可能会阻塞遍历操作;
  2. 使用 java.util.concurrent 包下实现了 fail-safe 机制的容器类,不过这样做也是有一定缺陷的,请参考上文;
  3. 尽量去使用局部变量,避免让某个变量被多个线程所共享,从根本上解决线程安全的问题。

2、请说一下 HashSet 的实现原理?HashSet 是如何保证数据不可重复的?


HashSet 是基于 HashMap 作为底层而实现的。HashSet 中的元素实际上由底层的 HashMap 的 Key 来保存;而 HashMap 的 Value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

那 HashSet 是如何保证数据不可重复的呢?

我们来看一下源代码~

向 HashSet 添加元素会调用 add 方法:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

我们看到,在向 HashSet 添加一个元素时,就是调用了底层的 HashMap 的 put 方法:

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

HashMap 的 put 方法的执行逻辑如下:

  • 首先会调用 hash(key.hashCode()) 方法计算出 hash 值 ,通过该对象的 hash 值可以快速定位到 map 中存储相同 hash 值的桶;
  • 如果该桶中没有任何元素,那么说明要添加的元素在原来的 map 中不存在,所以,存储该元素;
  • 如果该桶中已经有元素,那么遍历桶中的所有元素,并调用 equals 方法与该桶中的每个元素进行比对,如果两个元素的 hashCode 相同,且 equals 比较返回 true,则认为这两个元素的 key 相等,更新该元素的 value 值,否则就向集合中添加新的元素。

所以,一个对象的 hashCode 方法与 equals 方法可以保证向 HashSet 中添加元素不重复,且大大优化了时间复杂度。

总结

本题是一道经典的面试题,相信大家已经都熟练掌握了 HashSet 底层原理。

其实到这里还没完,面试官可能在这个问题上做出很多拓展:

譬如,既然 HashSet 只使用了底层 HashMap 的 key,那么直接使用 null 作为 HashMap 的 value 不就好了,还节省了内存空间,为何要使用 PRESENT 作为 value 呢?

其实,答案就在源代码中,我们来继续分析一下 HashSet 的 add 方法与 remove 方法的源代码:

add

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

remove

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

假设 map 当中已经有一个 (e,value1) 的键值对存储进去了,如果再去添加一个键值对为 (e,value2) 的元素,map 会替换掉原本的 (e,value1) 更新为 (e,value2) 并返回新的 value 值 value2,value2 != null ,结果返回 false,这表示我们向 HashSet 中添加的元素是重复的,而对于 Set 的定义就是不能存储重复的元素,所以,结果返回 false 也是合情合理。

但是,如果 map 的 value 存储的是 null 值,那么 map 会替换掉原本的 (e,null) 更新为 (e,null),结果返回新的 value 值 null,null == null,最终结果返回 true。这样的话,我们向 Set 中添加一个重复的元素,最终的结果返回 true,就会造成矛盾,使用 PRESENT 作为 map 的 value 就可以避免这种歧义。

remove 方法的理解同 add 方法。

那么面试官还可能会问:

直接通过 equals 比较就可以确定向 HashSet 中插入的元素是否重复了,那为啥还要先计算 hashCode?

答案就是一个字:快!

hash 算法的时间复杂度为 ,而如果每插入一个元素就要对 HashSet 中所有的元素逐个进行 equals 比较,那 add 方法就会变成一个时间复杂度为 的算法。我们可以先比较两个对象的 hashCode 是否相等,如果不相等,就不用再进行 equals 比较了;如果相等,我们也只需要遍历同一个“哈希桶”的所有元素。总的来说,向哈希表中添加元素的时间复杂度可以认为是

3、HashMap 的底层是如何实现的?从 JDK 8 开始,发生了怎样的变化?


Java 8 以前的 HashMap 简单来说就是 “数组+链表” 这样一种数据结构。

如上图所示,HashMap 大概就长成这样儿。

我们先来了解一些概念:capacity,loadfactor,threshold;

capacity 指的就是 HashMap 中桶的数量,这里面理解为底层散列表(Node<K,V>[] table;)的长度。HashMap 初始化默认的 capacity 为 16,之后每次发生扩容时,会扩容到原来的 2 倍。这里面需要注意的是,HashMap 的初始化是一种懒加载机制。在《 Java 面试八股文基础篇 》的第二期中有这样的一个问题:初始化一个空的 ArrayList,此时底层数组的容量为多少?添加一个元素后,容量为多少?。该问题的答案是:当我们去 new 一个 ArrayList 时 ,此时底层数组也是一个空数组,添加一个元素后,才会正式为底层数组 elementData  开辟空间,其初始化容量为 10。这就是一种懒加载机制。而 HashMap 也是一样的,当我们去 new 一个 HashMap 时,此时底层的散列表为空,执行 put 方法添加一个元素后,才会正式地为底层散列表 table 开辟空间,其初始化长度为 16 。

loadfactor 为负载因子,默认值为 0.75f,threshold = capacity × loadfactor;当 HashMap 的 size > threshold 时,HashMap 发生扩容(resize)。所以,很多面试官也可能会问这样的问题:HashMap 中的数据多于多少个时会发生扩容? 答案是:12 个。

接下来我向大家介绍一下 Java8 之前的 HashMap 是如何插入一个元素以及解决哈希冲突的:

如上图,当我们第一次向 HashMap 中插入了一个元素 {k1,v1} , 首先,HashMap 的 put 方法会计算该元素的 hashCode(实际上 HashMap 并不会直接使用 key.hashCode,而是使用扰动函数 hash 返回一个哈希值,在这里为了方便起见,我就直接叫做 hashCode 了),在这里我们假设该元素的 hashCode 为 115,使用除留余数法(115%16),得到该元素所在桶的下标 3,该桶的位置没有元素,直接将该元素放入到桶内;

第二次向 HashMap 中插入一个元素 {k2,v2}, 在这里我们假设该元素的 hashCode 为 119,使用除留余数法(119%16),得到该元素所在桶的下标 7,该桶的位置没有元素,直接将该元素放入到桶内;

第三次向 HashMap 中插入一个元素 {k3,v3}, 在这里我们假设该元素的 hashCode 为 119,使用除留余数法(119%16),得到该元素所在桶的下标 7,和第二次插入的元素发生哈希冲突,该桶的位置已经有元素,Java8 之前会使用头插法,将 {k3,v3} 插入到“头部”,而非 {k2,v2} 的后面,形成一个链表。这种解决哈希冲突的方法叫做 separate chaining(链地址法或拉链法)。

以上内容就是在 Java8 以前,HashMap 是如何插入元素,以及解决哈希冲突的。不过在 Java8 以后,HashMap 就作出了改变。

Java 8 开始,HashMap 变成了一种 “数组+链表+红黑树” 的一种数据结构,大概长成这样儿:

Java 8 之后,哈希表的改进主要有三点:

第一点是:resize 扩容的优化。在 JDK 7 中,如果我们的 HashMap 发生了扩容时,需要对所有的元素重新计算哈希,并重新分配它们在哈希桶中的位置;而 JDK 8 则采用了一种更加精妙的方法,其原理是这样的:假设,我们的元素在扩容之前使用 hashCode 对 capacity 进行取余后留下的余数为 n 位(二进制);因为我们每次扩容会使用 2 次幂的扩展(长度扩为原来的 2 倍),所以扩容之后得到的余数就有 n+1 位,且高位之后的那些数字是不变的。在这 n+1 位里,如果高位是 0,那么扩容后该元素的索引不变;如果高位是 1,那么扩容后的索引就变为 “原索引 + oldCapacity”。JDK 8 使用这种方式来优化扩容,减少了移动所有元素带来的消耗。

讲到这儿可能大家觉得有点迷糊,我给出一个示例,方便大家理解:

假设,元素 {k1,v1} 的 hashCode 为 116,HashMap 的 capacity 为 16。使用除留余数法(116%16),得到该元素所在桶的下标 10;如果此时 HashMap 进行扩容,capacity 为 32,使用除留余数法(116%32),得到该元素所在桶的下标仍然为 10。

假设,元素 {k2,v2} 的 hashCode 为 115,HashMap 的 capacity 为 16。使用除留余数法(115%16),得到该元素所在桶的下标 3;如果此时 HashMap 进行扩容,capacity 为 32,使用除留余数法(115%32),得到该元素所在桶的下标为 19(16 + 3)。

通过下图,也可以更直观地理解 Java 8 中 resize 的优化:

Java 8 HashMap 的第二点改进是:当指向哈希桶的链表长度超过 8 ,并且底层散列表 table 的长度达到 64 时,链表就会进化成红黑树(需要注意的是,HashMap 底层链表转换为红黑树是两个必要条件),因为 “链表 + 数组” 作为底层的哈希表存在一个问题,如果所有的元素都落在一个哈希桶上,那么哈希表的查找效率就会变低,将链表进化为红黑树可以改进复杂度,因为红黑树是一棵近似平衡的二叉搜索树,其查找效率要大大优于链表。

第三点改进是:头插法到尾插法的改进;Java 8 之前使用头插法是考虑到了热点数据这一因素,即:新插入的数据可能会更早地被使用,但是这个考虑本来就是不必要的,再加上更重要的原因:头插法会导致 HashMap 死链问题(实际上,尾插法也会导致 HashMap 的死链问题,如果在多线程并发的环境中应该避免使用 HashMap)。所以,在 JDK 8 以后,HashMap 也就弃用了头插法,改成尾插法。

总结

该问题是面试中的常考题,甚至可以说是必考题。

面试者应尽可能地说出自己对 HashMap 底层的理解,描述的越详细越好。

4、HashMap 底层链表转换为红黑树的条件是什么?


相信好好阅读文章的童鞋们已经知道本问题的答案了。我已经将这个问题的答案写在了上文的内容中。

HashMap 底层链表转换为红黑树的两个必要条件是:

  1. 链表长度超过 8
  2. 底层数组 table 的长度达到 64

我们来看一下源代码:

if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, hash);

treeifyBin 方法就是将链表转换为红黑树的方法,我们看到该方法触发的条件是 binCount >= TREEIFY_THRESHOLD - 1。TREEIFY_THRESHOLD 的值为 8,所以,当 binCount >= 7 时(binCount 从 0 开始,0~7 共 8 个数),也就是当这个哈希桶插入第 9 个节点时,执行 treeifyBin 方法。

我们再来看一下 treeifyBin 的源代码:

final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
    int n, index; HashMap.Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        HashMap.TreeNode<K,V> hd = null, tl = null;
        do {
            HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

重点在这一句:

if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();

MIN_TREEIFY_CAPACITY 的值为 64。也就是说,如果我们的散列表数组 table 的长度小于 64 时,并不会执行链表转红黑树,而是会首选扩容的方法来降低链表的长度。

总结

这道题能答全答好的人并不多。

有一部分人只知道链表长度超过 8 转换成红黑树这件事儿,并不知道另一个必要条件是底层数组 table 的长度要达到 64。

还有一些基础不牢固的人可能搞不清楚究竟是 HashMap 底层的链表长度达到 8 转换为红黑树还是链表长度超过 8 转换为红黑树?如果面试者的答案模棱两可,而恰巧面试官就是想考察你能不能避坑,那么他通过这个问题就知道你对 HashMap 底层源代码的阅读还是不够透彻的~

还有一点,为啥阈值要设定为 8 呢?

原因就是,当哈希算法的离散性特别好的时候,能够用到红黑树的几率非常小。因为数据都均匀地分布在哈希桶中,几乎不会有哈希桶的链表长度可以达到阈值。然而,如果哈希算法的性能比较差,就可能导致数据分布不均匀。

不过,随机哈希算法下节点的分布频率会遵循泊松分布:

* Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million

JDK 文档中提出链表可以达到 8 个节点的概率是极低的,几乎为不可能事件。所以,HashMap 就将 8 设计为链表进化为红黑树的阈值了。

5、HashMap 的初始化容量是多少?为什么 HashMap 的初始化容量要设计成 2 的等幂?


我们知道,自 JDK 8 起,HashMap 的底层设计为 “数组 + 链表 + 红黑树”。

所谓的 HashMap 的容量指的就是底层哈希桶数组的长度。默认的情况下,HashMap 初始化容量为 16

当我们向 HashMap 中添加一个元素时,我们首先需要知道该元素应该放入到哪个哈希桶中。最直观的想法就是,调用该对象的 hashCode 方法,该方法会返回一个整数,然后使用这个数对 HashMap 的容量取模即可知道该元素在哈希桶数组中对应的位置。实际上 JDK 的处理方式和这个思路差不多~

在 JDK 1.7 ,HashMap 会先使用 hash 函数(方法)获取一个 hash 值。hash 方法传入元素的 key 的 hashCode,返回一个 int 值,然后调用 indexFor 方法,indexFor 方法传入 hash 值和容器容量,计算出元素在哈希桶的数组中存放的位置。

JDK 1.7 hash 方法将 key.hashCode 转换成一大串数字 indexFor 方法将 hash 方法生成的一大串数字转换成哈希桶数组的下标

需要说明的是在 JDK 1.8 以后,就没有 indexFor 方法了,不过探究 JDK 1.7 的 indexFor 方法会更直观一些,因为 JDK 以后的版本中虽然没有 indexFor 这样一个单独的方法,但是只是将这个操作直接写进了其他方法中。本质上查询哈希桶数组的下标的算法实现是一样的。

我们来看下 JDK 1.7 的源码:

hash

static int hash(int h) {
    return h ^ (h >>> 7) ^ (h >>> 4);
}

indexFor

static int indexFor(int h, int length) {
    return h & (length-1);
}

indexFor 方法只做了一个操作:

h & (length-1)

这个操作的本质就是:取模运算!前提条件是 length 为 2 的幂

举个栗子:

已知,length = pow(2,n)n = 3,h = 6

h & (length - 1) 的结果为 0110 & 0111 = 0110 = 6

h % length 的结果为 6 % 8 = 6, 二者相等。

关于这个位运算的技巧,我们并不需要了解它是如何成立的,毕竟这是一个数学问题,我们只需要记住这样一个结论即可。设计者使用位运算代替取模的原因在于,位运算的效率比取模运算高的多,考虑到 HashMap 的性能,所以就这么设计了。这个位运算成立的前提是 length 为 2 的幂。所以,为什么 HashMap 的初始化容量要设计成 2 的等幂?就是为了可以使用这么一个 “骚操作” 使得 HashMap 的性能无懈可击。除了这一点外,HashMap 的初始化容量设计成 2 的等幂也是为了优化 resize 操作。这一点大家可以参考我在本文中给出的第三个问题:【 HashMap 的底层是如何实现的?从 JDK 8 开始,发生了怎样的变化?】,并从该问题中的 resize 扩容的优化中寻找答案。

至于 HashMap 默认的初始容量为何设置为 16,第一点是 16 这个数字满足 2 的幂这一要求;另外 16 作为初始化容量是一个效率与内存之间权衡考虑的结果,这个值既不能设计的太大,又不能设计的太小,而 16 这个值则刚刚好。

总结

这个题作为面试题被问到的情况应该不多,我个人觉得这是一个纯粹靠背诵的 “课外知识扩展题”。

即便面试官会拿这个题来考察面试者,他的意图也仅仅是考察面试者对 HashMap 源码的阅读程度,并不会深究 h & (length - 1) 背后的数学原理。

6、请谈一下 HashMap 中的 hash 算法是如何实现的?


直接上源代码~

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我们看到,JDK 8 的 hash 方法和前文中提到的 JDK 1.7 的 hash 方法略有不同,不过它们都并不是直接返回了 key.hashCode() ,而是对其进行“二次封装”,这种“二次封装”我们称之为扰动函数或扰动计算。

我们先通过一个示例,看一下 hash 函数的操作,再来解释为什么要这样做.

假设 h = key.hashCode() 的值为:

1111 1101 1101 1111 0101 1101 0010 1111

JDK 8 的 hash 算法将 key.hashCode() 无符号右移 16 位,然后再和原 hashCode 做异或运算。我们知道,int 型的长度为 32 位,将 hashCode 无符号右移 16 位相当于将高区 16 位移动到了低区 16 位,并将高区的部分以 0 填充。将其无符号右移 16 位的结果为:

0000 0000 0000 0000 1111 1101 1101 1111

二者进行异或运算,得到的结果就是 hash 函数的返回值:

1111 1101 1101 1111 1010 0000 1111 0000

我们看到,扰动函数的目的是为了让原来的 hashCode 的高 16位与低 16 位二进制的特征混合起来。为什么要这样做呢?

在计算出 hash 函数的返回结果后,我们会通过 h & (length - 1) 计算元素具体落到哪一个哈希桶中,或是说落到哪一个槽位(slot)上。假设目前有 16 个哈希桶,那么我们就要计算:

h
&
0000 0000 0000 0000 0000 0000 0000 1111

如果我们不做扰动计算,直接使用 h = key.hashCode(),那么我们最终得到的槽位就只能使用 hashCode 后四位的信息,这样就增加了发生哈希碰撞的概率。

所以,之所以使用扰动函数,就是为了混合原始哈希码的高位与低位,以此来增加随机性。使用异或运算的原因就在于,异或运算能更好的保留各部分的特征。

总结

又是一道没有什么卵用的面试题。不过这种没有啥卵用的题还经常被问到。

看一遍理解了就好~

总结

今天我主要分享了 Java HashMap 中的一些面试常考题和知识点,关于 HashMap 的内容还有很多,譬如:请尽可能描述 HashMap put 操作的流程;红黑树的基本原理及操作;为什么 HashMap 不是线程安全的等等。这些问题我会在后续的文章中陆续更新。

另外,Java 基础篇大约还有两三期左右就会更新完毕了,届时我会开始数据库篇的内容,敬请期待~


前几期文章的链接🔗:

好啦,至此为止,这篇文章就到这里了~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!



: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存