Java集合框架中List常见问题

Java集合框架中List常见基础面试题

考查点:

  • List的基础知识掌握情况

  • 对应实现的区别

  • 线程安全

  • 使用场景

问题:

说下Vector和ArrayList、LinkedList联系和区别?分别的使用场景。

答案:

线程安全
  • ArrayList:底层是数组实现,线程不安全。查询和修改非常快,但增加和删除速度较慢。

  • LinkedList:底层是双向链表,线程不安全。查询和修改速度慢,但增加和删除速度快。

  • Vector :底层是数组实现,线程安全。操作时使用synchronized进行加锁。

使用场景

  • Vector:已经很少使用。

  • 增加和删除场景多时,使用LinkedList

  • 查询和修改多时,使用ArrayList

保证线程安全的ArrayList实现方式

在多线程环境中,直接使用ArrayList可能会导致线程安全问题。为了保证线程安全,可以采用以下几种方式:

方式一:自定义同步包装类

可以创建一个自定义的线程安全包装类,根据具体业务需求,在addupdateremove等方法上加锁。这种方式提供了最大的灵活性,但需要手动管理锁,可能会增加编程复杂度。

复制代码
public class SyncArrayList<E> {
    private final ArrayList<E> list = new ArrayList<>();
    private final Object lock = new Object();

    public void add(E e) {
        synchronized (lock) {
            list.add(e);
        }
    }

    public E remove(int index) {
        synchronized (lock) {
            return list.remove(index);
        }
    }

    // 其他方法...
}

方式二:使用Collections.synchronizedList

可以使用Collections.synchronizedList方法来包装一个ArrayList,从而实现线程安全。这种方式简单易用,但所有的操作都需要通过集合的迭代器来进行,以保证线程安全。

复制代码
List<String> syncList = Collections.synchronizedList(new ArrayList<String>());

方式三:使用CopyOnWriteArrayList

CopyOnWriteArrayList是一种线程安全的变体,适用于读多写少的场景。每次修改操作都会复制整个底层数组,从而保证读操作的高性能。这种方式简单且线程安全,但可能会因为复制数组而影响性能。

复制代码
List<String> cowList = new CopyOnWriteArrayList<>();

方式四:使用ReentrantLock

可以使用ReentrantLock来手动管理锁,这种方式提供了比synchronized更细粒度的锁定控制,可以根据具体需求灵活使用。

复制代码
import java.util.concurrent.locks.ReentrantLock;

public class LockArrayList<E> {
    private final ArrayList<E> list = new ArrayList<>();
    private final ReentrantLock lock = new ReentrantLock();

    public void add(E e) {
        lock.lock();
        try {
            list.add(e);
        } finally {
            lock.unlock();
        }
    }

    public E remove(int index) {
        lock.lock();
        try {
            return list.remove(index);
        } finally {
            lock.unlock();
        }
    }

    // 其他方法...
}

总结

选择哪种方式取决于具体的使用场景和性能需求。如果需要频繁的读写操作,CopyOnWriteArrayList可能是一个不错的选择。如果需要更细粒度的控制,可以考虑使用ReentrantLock。对于简单的线程安全需求,Collections.synchronizedList提供了一个快速且方便的解决方案。

CopyOnWriteArrayList的掌握情况(追问系列)

考查点:

  • CopyOnWriteArrayList的实现原理

  • CopyOnWriteArrayList与Collections.synchronizedList的区别

  • CopyOnWriteArrayList的使用场景

  • CopyOnWriteArrayList的设计思想及其优缺点

问题:

如果回答到上面的点则继续问,没回到则问,了解CopyOnWriteArrayList吗?和Collections.synchronizedList实现线程安全有什么区别,使用场景是怎样的?

答案:

CopyOnWriteArrayList与Collections.synchronizedList的区别
  • CopyOnWriteArrayList

    • 执行修改操作(如addsetremove)时,会复制一份新的数据,代价昂贵。

    • 修改完成后,将原来的集合指向新的集合完成操作。

    • 使用ReentrantLock来保证不会多个线程同时修改。

    • 场景:适合读操作远大于写操作的场景(读操作是不需要加锁的,直接获取,但是删除和增加需要加锁,读多写少)。

  • Collections.synchronizedList

    • 线程安全的原因是因为几乎每个方法都是使用synchronized加同步锁。

    • 场景:写操作性能比CopyOnWriteArrayList好,但是读操作性能并不如CopyOnWriteArrayList。

CopyOnWriteArrayList的设计思想及其优缺点

设计思想

  • 读写分离 + 最终一致

    • 读操作不需要加锁,直接读取数据。

    • 写操作(如添加、删除、修改)时,复制一份新的数据,修改完成后将原集合指向新集合,保证最终一致性。

优点

  • 读操作高性能,不需要加锁。

  • 写操作时通过复制数据,避免了多个线程同时修改数据的问题。

缺点

  • 内存占用问题,由于写时复制,内存里面同时存在两个对象占用的内存,如果对象大则容易发生YoungGC和FullGC。

  • 写操作性能相对较低,因为需要复制数据。

使用场景

  • CopyOnWriteArrayList

    • 适用于读多写少的场景,如缓存、配置信息等。

    • 适用于对并发写入要求不高,但读操作非常频繁的场景。

  • Collections.synchronizedList

    • 适用于写操作频繁,且对读操作性能要求不高的场景。

    • 适用于需要细粒度控制锁的场景。

总结

CopyOnWriteArrayList和Collections.synchronizedList各有优缺点,选择哪种方式取决于具体的使用场景和性能需求。如果读操作远多于写操作,且对内存占用不敏感,可以选择CopyOnWriteArrayList。如果写操作频繁,且对读操作性能要求不高,可以选择Collections.synchronizedList。

ArrayList 扩容机制详解(含 JDK1.7 前后差异 + 核心源码剖析)

一、面试"一句话"总结

当元素个数达到当前容量上限时,ArrayList 会触发一次"1.5 倍扩容 ":申请一块 1.5 倍大小的新数组,把旧数据 Arrays.copyOf 过去,再更新内部引用完成扩容。

⚠️ JDK 版本差异:

  • JDK1.7 之前:无参构造器直接给内部数组长度 10。

  • JDK1.7 及以后 :无参构造器先给空数组(length=0),第一次 add 时才真正扩容到 10,实现"懒加载"。


二、扩容全过程(JDK1.8 源码)

核心方法链路:add(E)ensureCapacityInternal(size+1)grow(int minCapacity)Arrays.copyOf

复制代码
// 1. add 入口
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

// 2. 计算所需最小容量
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 空数组时
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取 10
    }
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 3. 真正的扩容逻辑
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
    if (newCapacity - minCapacity < 0)   // 1.5 倍不够,直接用 minCapacity
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)// 防止溢出
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) throw new OutOfMemoryError(); // 溢出变负数
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

三、JDK1.7 前后差异速记表

表格

复制

版本 无参构造行为 初始容量 触发第一次扩容时机
≤1.6 直接 new Object[10] 10 第 11 次 add
≥1.7(含 8+) 空数组 Object[0] 0 第 1 次 add → 扩到 10

四、为什么采用 1.5 倍而不是 2 倍?
  • 折中策略:扩容太频繁会浪费 CPU;一次性扩太大又浪费内存。1.5 倍在大多数场景下能平衡 CPU 与内存开销。
五、面试加分点
  1. 提前避免扩容 :若已知大概元素数量,可在构造时指定容量
    new ArrayList<>(expectedSize),减少复制次数与 GC 压力。

  2. 极端情况 :当元素数量接近 Integer.MAX_VALUE 时,扩容可能溢出,源码用 hugeCapacity 处理。


一句话收尾

ArrayList 的扩容就是"懒加载 + 1.5 倍复制",JDK1.7 以后把初始容量延迟到第一次插入,既省内存又保持高性能。

相关推荐
Dcs18 分钟前
还在用 Arrays.hashCode?Java 自己也能写出更快的版本!
java
fouryears_234172 小时前
Spring,Spring Boot 和 Spring MVC 的关系以及区别
java·spring boot·spring·mvc
阿葱(聪)3 小时前
java 在k8s中的部署流程
java·开发语言·docker·kubernetes
浮生带你学Java3 小时前
2025Java面试题及答案整理( 2025年 7 月最新版,持续更新)
java·开发语言·数据库·面试·职场和发展
板板正3 小时前
SpringAI——提示词(Prompt)、提示词模板(PromptTemplate)
java·spring boot·ai·prompt
板板正4 小时前
SpringAI——对话记忆
java·spring boot·ai
期待のcode4 小时前
图片上传实现
java·前端·javascript·数据库·servlet·交互
李长渊哦4 小时前
深入理解Java中的Map.Entry接口
java·开发语言
夜月蓝汐4 小时前
JAVA中的Collection集合及ArrayList,LinkedLIst,HashSet,TreeSet和其它实现类的常用方法
java·开发语言