问遍了身边的面试官朋友,我整理出这份 Java 集合高频面试题(2021年最新版)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云解析 DNS,旗舰版 1个月
简介: 今天我们继续下一个重要的面试内容:集合框架。HashMap作为 Java 中最靓的仔,毋庸置疑将是本文的主角。

前言


大家好,我是囧辉,面试系列开篇:Java 基础高频面试题(2021年最新版),发表后受到不少同学的喜欢。


今天我们继续下一个重要的面试内容:集合框架。HashMap作为 Java 中最靓的仔,毋庸置疑将是本文的主角。

image.png

可能有些同学看过我之前的 HashMap 文章:面试阿里,HashMap 这一篇就够了,会想:辉哥果然又颓废了、堕落了,估计是将之前的内容就照搬过来水一篇,鄙视,取关,不看也罢,*()&*……&*

image.png

你们不对劲,你辉哥是这种人?当然不是的,本文的 HashMap 部分在之前的基础上进行了补充和完善,希望大家能看到完善的点。

最后,本文按BAT 面试标准给出解析,希望在这金三银四的日子里,祝你一臂之力,拿下大厂 Offer


面试系列


我自己前前后后加起来总共应该参加了不下四五十次的面试,拿到过几乎所有一线大厂的 offer:阿里、字节、美团、快手、拼多多等等。

每次面试后我都会将面试的题目进行记录,并整理成自己的题库,最近我将这些题目整理出来,并按大厂的标准给出自己的解析,希望在这金三银四的季节里,能助你一臂之力。

面试文章持续更新中...

内容

链接地址

面试经验分享

921天,从小厂到入职阿里

两年Java开发工作经验面试总结

4 Java 经验,阿里网易拼多多面试总结、心得体会

5 Java 经验,字节、美团、快手核心部门面试总结(真题解析)

复习2个月拿下美团offer,我都做了些啥 

如何准备好一场大厂面试 

简历

如何写一份让 HR 眼前一亮的简历(附模板)

Offer 选择

跳槽,如何选择一家公司

Java 基础

Java 基础高频面试题(2021年最新版)

一道有意思的初始化面试题

集合(HashMap

Java 集合框架高频面试题(2021年最新版)

面试阿里,HashMap 这一篇就够了

并发编程

面试必问的线程池,你懂了吗?

面试必问的CAS,你懂了吗?

MySQL

面试必问的 MySQL,你懂了吗?

MySQL 8.0 MVCC 核心原理解析(核心源码)

Spring

面试必问的 Spring,你懂了吗?

Mybatis

面试题:mybatis 中的 DAO 接口和 XML 文件里的 SQL 是如何建立关系的?

Redis

面试必问的缓存使用:如何保证数据一致性、缓存设计模式

面试必问的 RedisMemcached VS Redis

面试必问的 Redis:高可用解决方案哨兵、集群

面试必问的 Redis:主从复制

面试必问的 RedisRDBAOF、混合持久化

面试必问的 Redis:数据结构和基础概念

JVM

Java虚拟机面试题精选(二)

Java虚拟机面试题精选(一)

分布式

面试必问的分布式锁,你懂了吗?

算法

位图法:判断一个数是否在40亿个整数中?


正文


1、介绍下 HashMap 的底层数据结构吧。

JDK 1.8HashMap 底层是由数组+链表+红黑树组成,如下图所示,而在 JDK 1.8 之前是由数组+链表组成,就是下图去掉红黑树。

image.png

2、为什么使用“数组+链表”?

使用数组+链表是为了解决 hash 冲突的问题。

数组和链表有如下特点:

数组:查找容易,通过 index 快速定位;插入和删除困难,需要移动插入和删除位置之后的节点;

链表:查找困难,需要从头结点或尾节点开始遍历,直到寻找到目标节点;插入和删除容易,只需修改目标节点前后节点的 next prev 属性即可;

HashMap 巧妙的将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式来解决哈希冲突。

首先通过index 快速定位到索引位置,这边利用了数组的优点;然后遍历链表找到节点,进行节点的新增/修改/删除操作,这边利用了链表的优点。简直,完美。

image.png

3、为什么要改成“数组+链表+红黑树”?


通过上题可以看出,数组+链表已经充分发挥了这两种数据结构的优点,是个很不错的组合了。

但是这种组合仍然存在问题,就是在定位到索引位置后,需要先遍历链表找到节点,这个地方如果链表很长的话,也就是 hash 冲突很严重的时候,会有查找性能问题,因此在JDK1.8中,通过引入红黑树,来优化这个问题。

使用链表的查找性能是 O(n),而使用红黑树是 O(logn)


4、那在什么时候用链表?什么时候用红黑树?

对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。

对于移除,当同一个索引位置的节点在移除后达到 6 个(阈值6),并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

5、为什么链表转红黑树的阈值是8

我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果( B 我装定了)。

网络异常,图片无法展示
|

红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。


理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006就我们这QPS不到10的系统,根本不可能遇到嘛),这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。


6、那为什么转回链表节点是用的6而不是复用8


如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。


7HashMap有哪些重要属性?分别用于做什么的?


除了用来存储我们的节点 table 数组外,HashMap 还有以下几个重要属性:

1sizeHashMap 已经存储的节点个数;

2threshold1)扩容阈值(主要),当HashMap 的个数达到该值,触发扩容。2)初始化时的容量,在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。

3loadFactor:负载因子,扩容阈值 = 容量* 负载因子。


8HashMap的默认初始容量是多少?HashMap 的容量有什么限制吗?


默认初始容量是16HashMap的容量必须是2N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的 2 N 次方,例如传 16,容量为16;传17,容量为32


9、“大于等于该容量的最小的2N次方”是怎么算的?


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;
}

我们先不看第一行“int n = cap - 1”,先看下面的5行计算。

|=(或等于):这个符号比较少见,但是“+=”应该都见过,看到这你应该明白了。例如:a |= b ,可以转成:a = a | b

image.png

>>>(无符号右移):例如 a >>> b 指的是将a 向右移动 b 指定的位数,右移后左边空出的位用零来填充,移出右边的位被丢弃。

image.png

假设n 的值为 0010 0001,则该计算如下图:

image.png

相信你应该看出来,这5个公式会通过最高位的1,拿到214181161321。当然,有多少个1,取决于我们的入参有多大,但我们肯定的是经过这5个计算,得到的值是一个低位全是1的值,最后返回的时候 +1,则会得到1个比n 大的 2 N次方。


这时再看开头的cap - 1 就很简单了,这是为了处理 cap 本身就是 2 N次方的情况。

计算机底层是二进制的,移位和或运算是非常快的,所以这个方法的效率很高。

PS:这是 HashMap 中我个人最喜欢的设计,非常巧妙。


10HashMap的容量必须是 2 N 次方,这是为什么?


核心目的是:实现节点均匀分布,减少 hash 冲突。

计算索引位置的公式为:(n - 1) & hash,当 n 2 N次方时,n - 1 为低位全是 1 的值,此时任何值跟n - 1 进行 & 运算的结果为该值的低 N 位,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为& 运算比 mod 具有更高的效率。

如下图,当n 不为 2 N 次方时,hash 冲突的概率明显增大。

image.png

11HashMap的插入流程是怎么样的?

真香,建议收藏。

image.png

image.png

12、插入流程的图里刚开始有个计算 key hash 值,是怎么设计的?


源码如下:拿到key hashCode,并将 hashCode 的高16位和hashCode 进行异或(XOR)运算,得到最终的 hash 值。

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


13、为什么要将 hashCode 的高16位参与运算?

主要是为了在table 的长度较小的时候,让高位也参与运算,并且不会有太大的开销。

例如下图,如果高位不参与运算,由于n - 1 0000 0111,所以结果只取决于 hash 值的低3位,无论高位怎么变化,结果都是一样的。

image.png

如果我们将高位参与运算,则索引计算结果就不会仅取决于低位。

image.png

image.png

14、扩容(resize)流程介绍下?

真香,建议收藏。

image.png

image.png

15、红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?

请看对下面的例子。

扩容前 table 的容量为16a节点和 b 节点在扩容前处于同一索引位置。

image.png

扩容后,table长度为32,新表的 n - 1 只比老表的 n - 1 在高位多了一个1(图中标红)。

image.png

因为 2个节点在老表是同一个索引位置,因此计算新表的索引位置时,只取决于新表在高位多出来的这一位(图中标红),而这一位的值刚好等于 oldCap


因为只取决于这一位,所以只会存在两种情况:1  (e.hash & oldCap) == 0 ,则新表索引位置为原索引位置2(e.hash & oldCap) != 0,则新表索引位置为原索引+ oldCap 位置


16HashMap是线程安全的吗?


不是。HashMap在并发下存在数据覆盖、遍历的同时进行修改会抛出 ConcurrentModificationException 异常等问题,JDK 1.8 之前还存在死循环问题。


17、介绍一下死循环问题?


导致死循环的根本原因是 JDK 1.7 扩容采用的是头插法,会导致同一索引位置的节点在扩容后顺序反掉,在并发插入触发扩容时形成环,从而产生死循环。


JDK 1.8 之后采用的是尾插法,扩容后节点顺序不会反掉,不存在死循环问题。

JDK 1.7.0 的扩容代码如下,用例子来看会好理解点。

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

PS:这个流程较难理解,建议对着代码自己模拟走一遍。

例子:我们有1个容量为2HashMaploadFactor=0.75,此时线程1和线程2同时往该 HashMap 插入一个数据,都触发了扩容流程,接着有以下流程。


1)在2个线程都插入节点,触发扩容流程之前,此时的结构如下图。


image.png

2)线程1进行扩容,执行到代码:Entry<K,V> next = e.next 后被调度挂起,此时的结构如下图。

image.png

3)线程1被挂起后,线程2进入扩容流程,并走完整个扩容流程,此时的结构如下图。


image.png

由于两个线程操作的是同一个 table,所以该图又可以画成如下图。

image.png

4)线程1恢复后,继续走完第一次的循环流程,此时的结构如下图。

image.png

5)线程1继续走完第二次循环,此时的结构如下图。


image.png

6)线程1继续执行第三次循环,执行到 e.next = newTable[i] 时形成环,执行完第三次循环的结构如下图。

image.png

如果此时线程1调用map.get(11) ,悲剧就出现了——无限循环。


18、总结下 JDK 1.8 主要进行了哪些优化?


JDK 1.8 的主要优化刚才我们都聊过了,主要有以下几点:


1)底层数据结构从数组+链表改成数组+链表+红黑树,主要是优化了 hash 冲突较严重时,链表过长的查找性能:O(n) -> O(logn)

2)计算 table 初始容量的方式发生了改变,老的方式是从1开始不断向左进行移位运算,直到找到大于等于入参容量的值;新的方式则是通过“5个移位+或等于运算来计算。


// JDK 1.7.0
public HashMap(int initialCapacity, float loadFactor) {
    // 省略
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    // ... 省略
}
// JDK 1.8.0_191
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;
}

3)优化了 hash 值的计算方式,老的通过一顿瞎JB操作,新的只是简单的让高16位参与了运算。

4)扩容时插入方式从头插法改成尾插法,避免了并发下的死循环。

5)扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。


19Hashtable是怎么加锁的 ?


Hashtable 通过 synchronized 修饰方法来加锁,从而实现线程安全。

public synchronized V get(Object key) {
    // ...
}
public synchronized V put(K key, V value) {
    // ...
}

20LinkedHashMap TreeMap 排序的区别?


LinkedHashMap TreeMap 都是提供了排序支持的 Map,区别在于支持的排序方式不同:

LinkedHashMap:保存了数据的插入顺序,也可以通过参数设置,保存数据的访问顺序。

TreeMap:底层是红黑树实现。可以指定比较器(Comparator比较器),通过重写 compare 方法来自定义排序;如果没有指定比较器,TreeMap默认是按 Key 的升序排序(如果 key 没有实现Comparable接口,则会抛异常)。


21HashMap Hashtable 的区别?


HashMap 允许 key value nullHashtable 不允许。

HashMap 的默认初始容量为 16Hashtable 11

HashMap 的扩容为原来的 2 倍,Hashtable的扩容为原来的 2 倍加 1

HashMap 是非线程安全的,Hashtable 是线程安全的,使用 synchronized 修饰方法实现线程安全。

HashMap hash 值重新计算过,Hashtable直接使用 hashCode

HashMap 去掉了 Hashtable 中的contains 方法。

HashMap 继承自 AbstractMap 类,Hashtable继承自 Dictionary 类。

HashMap 的性能比 Hashtable 高,因为Hashtable 使用 synchronized 实现线程安全,还有就是 HashMap 1.8 之后底层数据结构优化成数组+链表+红黑树,在极端情况下也能提升性能。


22、介绍下 ConcurrenHashMap,要讲出 1.7 1.8 的区别?


ConcurrentHashMap HashMap 的线程安全版本,和 HashMap 一样,在JDK 1.8 中进行了较大的优化。

JDK1.7:底层结构为:分段的数组+链表;实现线程安全的方式:分段锁(Segment,继承了ReentrantLock),如下图所示。


image.png

JDK1.8:底层结构为:数组+链表+红黑树;实现线程安全的方式:CAS + Synchronized

image.png

区别:


1JDK1.8 中降低锁的粒度。JDK1.7 版本锁的粒度是基于 Segment 的,包含多个节点(HashEntry),而JDK1.8 锁的粒度就是单节点(Node)。

2JDK1.8 版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized 来进行同步,所以不需要分段锁的概念,也就不需要 Segment 这种数据结构了,当前还保留仅为了兼容。

3JDK1.8 使用红黑树来优化链表,跟 HashMap 一样,优化了极端情况下,链表过长带来的性能问题。

4JDK1.8 使用内置锁 synchronized 来代替重入锁ReentrantLocksynchronized 是官方一直在不断优化的,现在性能已经比较可观,也是官方推荐使用的加锁方式。


23ConcurrentHashMap的并发扩容


ConcurrentHashMap 在扩容时会计算出一个步长(stride),最小值是16,然后给当前扩容线程分配一个步长的节点数,例如16个,让该线程去对这16个节点进行扩容操作(将节点从老表移动到新表)。

如果在扩容结束前又来一个线程,则也会给该线程分配一个步长的节点数让该线程去扩容。依次类推,以达到多线程并发扩容的效果。


例如:64要扩容到128,步长为16,则第一个线程会负责第113(索引112~128(索引127)的节点,第二个线程会负责第97(索引96~112(索引111)的节点,依次类推。


具体处理(该流程后续可能会替换成流程图):

1)如果索引位置上为null,则直接使用 CAS 将索引位置赋值为ForwardingNodehash值为-1),表示已经处理过,这个也是触发并发扩容的关键点。


2)如果索引位置的节点 f hash 值为 MOVED-1),则代表节点f ForwardingNode 节点,只有 ForwardingNode hash 值为 -1,意味着该节点已经处理过了,则跳过该节点继续往下处理。


3.否则,对索引位置的节点 f 对象使用 synchronized 进行加锁,遍历链表或红黑树,如果找到key 和入参相同的,则替换掉 value 值;如果没找到,则新增一个节点。如果是链表,同时判断是否需要转红黑树。处理完在索引位置的节点后,会将该索引位置赋值为ForwardingNode,表示该位置已经处理过。


ForwardingNode:一个特殊的 Node 节点,hash值为-1(源码中定义成 MOVED),其中存储 nextTable 的引用。只有发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在 table 中表示当前节点已经被处理(或则为 null )。


24ConcurrenHashMap Hashtable 的区别?


1)底层数据结构: 


ConcurrentHashMap1JDK1.7采用 分段的数组+链表 实现;2JDK1.8采用数组+链表+红黑树,跟 JDK1.8 HashMap 的底层数据结构一样。

Hashtable 采用 数组+链表 的形式,跟JDK1.8 之前的 HashMap 的底层数据结构类似。


2)实现线程安全的方式(重要):


ConcurrentHashMap


1JDK1.7:使用分段锁(Segment)保证线程安全,每个分段(Segment)包含若干个 HashEntry,当并发访问不同分段的数据时,不会产生锁竞争,从而提升并发性能。

image.png

2JDK1.8:使用 synchronized + CAS 的方式保证线程安全,每次只锁一个节点(Node),进一步降低锁粒度,降低锁冲突的概率,从而提升并发性能。

image.png

Hashtable:使用 synchronized 修饰方法来保证线程安全,每个实例对象只有一把锁,并发性能较低,相当于串行访问。

image.png

25ConcurrentHashMap size() 方法怎么实现的?


JDK 1.7:先尝试在不加锁的情况下尝进行统计 size,最多统计3次,如果连续两次统计之间没有任何对 segment 的修改操作,则返回统计结果。否则,对每个segment进行加锁,然后统计出结果,返回结果。

JDK 1.8:直接统计 baseCount counterCells value 值,返回的是一个近似值,如果有并发的插入或删除,实际的数量可能会有所不同。


该统计方式改编自 LongAdder Striped64,这两个类在 JDK 1.8 中被引入,出自并发大神 Doug Lea 之手,是原子类(AtomicLong 等)的优化版本,主要优化了在并发竞争下,AtomicLong 由于 CAS 失败的带来的性能损耗。

值得注意的是,JDK1.8中,提供了另一个统计的方法 mappingCount,实现和 size 一样,只是返回的类型改成了 long,这也是官方推荐的方式。


public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
// 一个ConcurrentHashMap包含的映射数量可能超过int上限,
// 所以应该使用这个方法来代替size()
public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // ignore transient negative values
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

26、比较下常见的几种 Map,在使用时怎么选择?

image.png

30ArrayList Vector 的区别。

Vector ArrayList 的实现几乎是一样的,区别在于:

1)最重要的的区别: Vector 在方法上使用了 synchronized 来保证线程安全,同时由于这个原因,在性能上 ArrayList 会有更好的表现。

2 Vector 扩容后容量默认变为原来 2 倍,而ArrayList 为原来的 1.5 倍。

有类似关系的还有:StringBuilder StringBufferHashMap Hashtable


31ArrayList LinkedList 的区别?

1ArrayList 底层基于动态数组实现,LinkedList 底层基于双向链表实现。

2、对于随机访问(按 index 访问,get/set方法):ArrayList通过 index 直接定位到数组对应位置的节点,而 LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上 ArrayList 优于 LinkedList

3、对于随机插入和删除:ArrayList 需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而 LinkedList 只需修改目标节点前后节点的 next prev 属性即可,因此在效率上 LinkedList 优于 ArrayList

4、对于顺序插入和删除:由于 ArrayList 不需要移动节点,因此在效率上比 LinkedList 更好。这也是为什么在实际使用中 ArrayList 更多,因为大部分情况下我们的使用都是顺序插入。

5、两者都不是线程安全的。

6、内存空间占用: ArrayList 的空间浪费主要体现在在list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。


32HashSet是如何保证不重复的?

HashSet 底层使用 HashMap 来实现,见下面的源码,元素放在 HashMap key 里,value为固定的 Object 对象。当 add 时调用HashMap put 方法,如果元素不存在,则返回 null 表示add 成功,否则 add 失败。

由于HashMap Key 值本身就不允许重复,HashSet 正好利用HashMap key 不重复的特性来校验重复元素,简直太妙了。

private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}


33TreeSet清楚吗?能详细说下吗?

“TreeSet TreeMap 的关系和上面说的 “HashSet HashMap 的关系几乎一致。

TreeSet 底层默认使用 TreeMap 来实现。而TreeMap 通过实现 Comparator(或 Key 实现Comparable)来实现自定义顺序。

private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}
public TreeSet() {
    this(new TreeMap<E,Object>());
}
public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

34、介绍下 CopyOnWriteArrayList

CopyOnWriteArrayList ArrayList 的线程安全版本,也是大名鼎鼎的 copy-on-writeCOW,写时复制)的一种实现。

在读操作时不加锁,跟ArrayList类似;在写操作时,复制出一个新的数组,在新数组上进行操作,操作完了,将底层数组指针指向新数组。适合使用在读多写少的场景。

例如add(E e) 方法的操作流程如下:使用 ReentrantLock 加锁,拿到原数组的length,使用Arrays.copyOf 方法从原数组复制一个新的数组(length+1),将要添加的元素放到新数组的下标length位置,最后将底层数组指针指向新数组。


35Comparable Comparator 比较?

1Comparable 是排序接口,一个类实现了 Comparable接口,意味着该类支持排序Comparator是比较器,我们可以实现该接口,自定义比较算法,创建一个该类的比较器来进行排序。

2Comparable 相当于内部比较器,而Comparator 相当于外部比较器

3Comparable 的耦合性更强,Comparator 的灵活性和扩展性更优。

4Comparable 可以用作类的默认排序方法,而 Comparator 则用于默认排序不满足时,提供自定义排序。

耦合性和扩展性的问题,举个简单的例子:


当实现类实现了Comparable 接口,但是已有的 compareTo 方法的比较算法不满足当前需求,此时如果想对两个类进行比较,有两种办法:


1)修改实现类的源代码,修改 compareTo 方法,但是这明显不是一个好方案,因为这个实现类的默认比较算法可能已经在其他地方使用了,此时如果修改可能会造成影响,所以一般不会这么做。

2)实现 Comparator 接口,自定义一个比较器,该方案会更优,自定义的比较器只用于当前逻辑,其他已有的逻辑不受影响。


36ListSetMap三者的区别?


List(对付顺序的好帮手):存储的对象是可重复的、有序的。

Set(注重独一无二的性质):存储的对象是不可重复的、无序的。

Map(用 Key 来搜索的专业户): 存储键值对(key-value),不能包含重复的键(key),每个键只能映射到一个值。


37MapListSet 分别说下你了解到它们有的线程安全类和线程不安全的类?


Map


线程安全:CocurrentHashMapHashtable

线程不安全:HashMapLinkedHashMapTreeMapWeakHashMap


List

线程安全:Vector(线程安全版的ArrayList)、Stack(继承Vector,增加poppush方法)、CopyOnWriteArrayList

线程不安全:ArrayListLinkedList


Set

线程安全:CopyOnWriteArraySet(底层使用CopyOnWriteArrayList,通过在插入前判断是否存在实现 Set 不重复的效果)

线程不安全:HashSet(基于HashMap)、LinkedHashSet(基于 LinkedHashMap)、TreeSet(基于TreeMap)、EnumSet


38Collection Collections的区别

Collection:集合类的一个顶级接口,提供了对集合对象进行基本操作的通用接口方法。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,常见的 List Set 就是直接继承Collection 接口。

Collections:集合类的一个工具类/帮助类,提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。


最后


面试系列持续创作中,有疑问的地方欢迎留言,我看到后都会回复。

原创不易,如果你觉得本文写的还不错,对你有帮助,请通过【点赞】和【关注】让我知道,支持我写出更好的文章。

我是囧辉,我的目标是帮助大家拿到心仪的大厂 Offer,我们下期见。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
21天前
|
缓存 Java 关系型数据库
【Java面试题汇总】ElasticSearch篇(2023版)
倒排索引、MySQL和ES一致性、ES近实时、ES集群的节点、分片、搭建、脑裂、调优。
【Java面试题汇总】ElasticSearch篇(2023版)
|
21天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
175 37
|
21天前
|
设计模式 安全 算法
【Java面试题汇总】设计模式篇(2023版)
谈谈你对设计模式的理解、七大原则、单例模式、工厂模式、代理模式、模板模式、观察者模式、JDK中用到的设计模式、Spring中用到的设计模式
【Java面试题汇总】设计模式篇(2023版)
|
21天前
|
存储 关系型数据库 MySQL
【Java面试题汇总】MySQL数据库篇(2023版)
聚簇索引和非聚簇索引、索引的底层数据结构、B树和B+树、MySQL为什么不用红黑树而用B+树、数据库引擎有哪些、InnoDB的MVCC、乐观锁和悲观锁、ACID、事务隔离级别、MySQL主从同步、MySQL调优
【Java面试题汇总】MySQL数据库篇(2023版)
|
21天前
|
存储 缓存 NoSQL
【Java面试题汇总】Redis篇(2023版)
Redis的数据类型、zset底层实现、持久化策略、分布式锁、缓存穿透、击穿、雪崩的区别、双写一致性、主从同步机制、单线程架构、高可用、缓存淘汰策略、Redis事务是否满足ACID、如何排查Redis中的慢查询
【Java面试题汇总】Redis篇(2023版)
|
21天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
21天前
|
缓存 Java 数据库
【Java面试题汇总】Spring篇(2023版)
IoC、DI、aop、事务、为什么不建议@Transactional、事务传播级别、@Autowired和@Resource注解的区别、BeanFactory和FactoryBean的区别、Bean的作用域,以及默认的作用域、Bean的生命周期、循环依赖、三级缓存、
【Java面试题汇总】Spring篇(2023版)
|
21天前
|
存储 缓存 监控
【Java面试题汇总】JVM篇(2023版)
JVM内存模型、双亲委派模型、类加载机制、内存溢出、垃圾回收机制、内存泄漏、垃圾回收流程、垃圾回收器、G1、CMS、JVM调优
【Java面试题汇总】JVM篇(2023版)
|
21天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
21天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
下一篇
无影云桌面