Java面试中的集合框架问题

1、ArrayListLinkedList的区别

ArrayListLinkedList都是基于List接口实现的,但底层结构不同

ArrayList底层基于动态数组,支持随机访问因为根据下标进行访问,但是插入和删除的话就会因为元素的移动造成效率比较低。他不是线性安全的,只适合于在单线程下频繁的读取数据

LinkedList底层是双向链表,每个节点包含者前驱和后继,它在任意位置插入和删除元素效率都会很高,前提情况下是定位到位置,但是在随机访问的时候就需要遍历整个链表导致访问速度慢,它也不是线性安全的。

  • 读多写少->ArrayList

  • 频繁在头部或者尾部增删元素->LinkedList

如何让他们变的线程安全呢

  • 在方法内使用,局部变量则是线程安全的

  • 用工具类包装,生成一个线程安全的代理版本

List<String> list = Collections.synchronizedList(new ArrayList<>());

2、HashMap得底层原理是什么,JDK8之后有什么优化?

HashMap底层就是由一个Node<K,v>[]数组构成,每个数组位置称为一个桶,每个桶存储一个链表和红黑树的头节点。

JDK8之前,当发生哈希冲突的时候,使用链表解决,最坏的情况就是查询时间是O(n)。当数据量大、链表过长时,性能急剧下降。

然后JDK8引入了优化:当某个桶的链表长度达到8,并且数组长度>=64时,该链表会转换成红黑树,使查找、插入、删除的时间复杂度从O(N)降低到O(logN)

如果链表长度达到8但数组长度<64,则会优先进行扩容,而不是转树,以期望通过扩容分散哈希冲突

这个阈值8是基于泊松分布计算出来的,正常情况下极少达到,转树主要是为了防御恶意攻击。

3、ConcurrentHashMap是怎么保证线程安全的?和HashMap有什么不同?

JDK8中,concurrentHashMap不再使用Segment分段锁,而是采用更细粒度的同步策略

底层结构依然是数组+链表/红黑树,和HashMap类似

线程安全的实现方法是:

  • 使用CAS操作来保证初始化和扩容时的线程安全

  • 在put元素的时候,如果该桶有多个元素,就是用synchronized锁住链表的头节点或红黑树的根节点

  • 锁的粒度非常小,只有多个线程用时修改同一个桶才会有竞争,大大提升了并发性能

  • 而读操作完全无锁,依赖volatile保证可见性,所有读性能几乎和非线性安全容器一样高

想象一个图书馆:

  • HashMap:谁都能改书架,没人管 → 乱

  • Hashtable:整个图书馆只有一把大锁 → 谁进都得等

  • ConcurrentHashMapJDK 8):每个书架自己有小锁 → 你改 1 号书架,我不影响改 2 号

4、HashMap的key为什么建议用String或Integer?如果我们自定义key的话注意什么?

String和Integer被广泛用作HashMap的key,是因为它们具备做key的理想特性:

  • 不可变性:一旦创建,hashCode不会变,适合做key

  • 正确重写了hashCode()和equals():能保证逻辑一致

  • 线程安全:无状态,多线程访问安全

当我们使用自定义对象作为key时,必须:

  • 重写equals方法:定义对象相等的业务逻辑

  • 重写hashCode方法:保证相等的对象有相同的哈希值

  • 最好将key设为不可变对象:避免hashCode变化导致无法定位桶

否则可能导致对象存进去却取不出来,因为HashMap依赖hashCode定位桶,再用equals判断是否真相同。

最后一句金句就是

hashCode()决定去哪桶,equals决定是不是你

5、HashMap的key可以为null吗?get(null)怎么工作的?多(null)key会怎样?

HashMap对null键做了处理

  • 在put和get的时候,会判断key是否为null

  • 如果时null,就会被调用putForNullKey()或者getForNullKey()这类专用方法

  • null键对应的Entry被存储在数组的第0个位置

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
    Node<K,V> first;
    
    if (key == null)
        return getNullKeyNode(); // 专门处理 null 键
    // ...
}
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value); // 专门的方法处理 null
    // ...
}

HashMap 扩容时,并不会创建新的 HashMap 实例,而是创建一个长度为原来两倍的新数组。”

“然后遍历旧数组中的每个链表,将节点重新分配到新数组中。”

JDK 8 的优化在于:由于数组长度是 2 的幂,扩容后只需看 hash 值的一个高位 bit:”

  • “如果该位为 0,节点留在原索引位置”

  • “如果该位为 1,节点移到 ‘原索引 + 旧容量’ 的位置”

“这样就避免了对每个 key 重新计算 hash,提升了扩容效率。”

6、TreeMapHashMap的区别

  • HashMap:基于哈希表 + 链表/红黑树,不保证顺序,查询性能高(O(1)),适合大多数场景。”

  • LinkedHashMap:在 HashMap 基础上加了双向链表,维护插入顺序或访问顺序,适合需要顺序遍历或实现 LRU 缓存的场景。”

  • TreeMap:基于红黑树,键有序,支持自然排序或自定义 Comparator,查询、插入、删除时间复杂度为 O(log n),适合需要排序或范围查询的场景(如排行榜、区间查找)。”

“所以:”

  • “要快 → HashMap

  • “要顺序 → LinkedHashMap

  • “要排序 → TreeMap

// 自然排序
TreeMap<String, Integer> map1 = new TreeMap<>();

// 自定义排序:按字符串长度排
TreeMap<String, Integer> map2 = new TreeMap<>((a, b) -> a.length() - b.length());

7、fail-fast与fail-safe

fail-fast

1、fail-fast是什么快速失败

抽象点就是我正在读一本书(遍历集合),这个时候突然有人走了进来对我正在遍历的集合进行删除或者修改,我就会说书变了!我不读了。

它是对所有的非线程安全集合都在检查会抛出ConcurrentModificationException

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");

// 边遍历边删除 → 危险!
for (String s : list) {
    if (s.equals("a")) {
        list.remove(s); // ❌ 抛 ConcurrentModificationException
    }
}

源码核心

// ArrayList 内部
int modCount = 0; // 修改次数

// 迭代器创建时
int expectedModCount = modCount; // 记下“我开始时的版本”

// 遍历时检查
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

就像你读书时,书页右上角有个编号:

  • 你开始读时是 v1

  • 有人改了,变成 v2

  • 你一翻页,发现编号变了 → “书被改了!” → 抛异常

这里可以使用迭代器删除就可以安全不用报错

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("a")) {
        it.remove(); // ✅ 允许,迭代器会同步 modCount
    }
}
fail-safe

它就是不管集合到底修没修改,都不影响我读

哪些集合

  • ConcurrentHashMap

  • CopyOnWriteArrayList

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");

// 遍历
for (String s : list) {
    list.add("c"); // ✅ 不会抛异常!
    System.out.println(s);
}

为什么安全?—— 看 CopyOnWrite

CopyOnWriteArrayList 的原理:

  • 读操作:直接读内部数组,不加锁

  • 写操作:复制一份新数组,在新数组上修改,然后替换引用

private transient volatile Object[] array;

public boolean add(E e) {
    Object[] oldArray = array;
    Object[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1);
    newArray[newArray.length - 1] = e;
    array = newArray; // 原来的数组还在,迭代器继续读它
}
  • 他不是改你手里的书,

  • 而是复印一本新书,在新书上改

  • 你继续读旧书,不受影响

总结

fail-fastfail-safe 是集合迭代器处理并发修改的两种策略。”

fail-fast:大多数非线程安全集合(如 ArrayListHashMap)采用的策略。迭代器会记录 modCount,一旦发现集合被外部修改,立即抛出 ConcurrentModificationException。这是一种‘快速报错’机制,提醒开发者注意并发问题。”

fail-safeConcurrentHashMapCopyOnWriteArrayList 等并发集合采用的策略。迭代器基于集合的快照当前状态进行遍历,原集合的修改不会影响迭代过程,不会抛异常。但读到的数据可能不是最新的(弱一致性)。”

“所以:”

  • fail-fast:发现问题立刻报,适合单线程或加锁场景”

  • fail-safe:你改你的,我读我的,适合并发读场景”

8、ArrayList底层实现原理

  • 它是基于动态的数组实现的

  • 当第一次添加的时候,这个数组长度初始化是10

  • ArrayList在扩容的时候是原来的1.5倍,然后将数组拷贝到扩容的数组中

  • ArrayList在添加数据的时候

    • 确保数组已使用的长度+1能够存下下一个数据

    • 计算数组的容量,如果数组已使用长度+1后大于当前的数组长度,则调用grow方法扩容

    • 确保新增的数据有地方存储,则将新元素添加到位于size的位置上

    • 返回添加成功布尔值

9、HashMapConcurrentHashMap在并发场景下的主要区别有五点

  1. 线程安全HashMap 不安全,多线程下可能死循环或数据丢失;ConcurrentHashMap 安全,采用 CAS 和 synchronized 锁桶头节点。

  2. 锁粒度ConcurrentHashMap 锁的是单个桶的头节点,不同桶可并发操作,性能高。

  3. 扩容机制ConcurrentHashMap 支持多线程协作扩容,多个线程可同时迁移数据,提升扩容效率。

  4. null 值限制ConcurrentHashMap 不允许 keyvaluenull,避免 get 返回 null 时的语义歧义。

  5. 性能权衡HashMap 单线程性能更好,ConcurrentHashMap 在并发场景下更稳定可靠。

“所以,单线程用 HashMap,多线程必用 ConcurrentHashMap。”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值