ArrayList与LinkedList底层原理:从扩容到fail-fast的List面试全解析 📅 发布时间:2026/8/29 13:26:21 👁 浏览次数: 这是一篇大厂面试常见的List八股文但我要换个角度把面试官藏在问题背后的考察意图一次性说透。1. 为什么List成了听起来简单问起来要命的高频题先抛个场景很多候选人简历上写着熟练掌握Java集合框架一面开场白往往是聊一下ArrayList和LinkedList的区别。听到这个问题大部分人的第一反应是——稳了这不是送分题吗ArrayList底层是数组查询快、增删慢LinkedList底层是双向链表增删快、查询慢。然后面试官点了点头接着问那ArrayList扩容一次到底发生了什么扩容因子为什么是1.5而不是2LinkedList真的增删一定比ArrayList快吗for循环遍历LinkedList为什么比迭代器慢几个数量级这时候不少人就开始冒汗了。List相关的题之所以在大厂面试里出现频率极高是因为它天然地横跨了三个考察维度数据结构基础、JVM内存模型、工程实践取舍。面试官问的不是你记不记得这两个类的区别而是你能不能从底层机制推导出它们在不同场景下的行为差异。从一道ArrayList的扩容题可以一路延伸出数组拷贝、位运算、OOM场景、甚至GC问题从一道LinkedList的增删题可以延伸到CPU缓存命中率、内存碎片化、RandomAccess接口的设计意义。这就是List八股文的可怕之处表面是一道题背后是一张网。更关键的是市面上流传的很多标准答案本身就有问题。比如LinkedList增删快这句话在随机中间位置插入的场景下完全不成立真实的Benchmark结果往往是ArrayList更快。再比如ArrayList查询快但你用for循环遍历LinkedList时每次get(i)都是一次O(n)的链式查找整体复杂度直接变成O(n²)这个和查询快的描述形成了巨大的反差。这篇文章我想把List相关的高频面试题串联起来从源码级别一层层拆开讲清楚每个结论的边界条件和底层依据最后再给出一套可以直接背的高分回答框架。2. 从一道ArrayList扩容题入手源码里藏着面试官所有的追问点2.1 构造方法里你没注意到的细节先看ArrayList的两个最常用构造方法public ArrayList() { this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public ArrayList(int initialCapacity) { if (initialCapacity 0) { this.elementData new Object[initialCapacity]; } else if (initialCapacity 0) { this.elementData EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(Illegal Capacity: initialCapacity); } }无参构造并不像很多人以为的那样直接创建一个容量为10的数组它只是把elementData指向了一个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA真正的容量10是在第一次add的时候才被赋予的。这就是懒加载思想和单例的懒汉式是一个道理目的是延迟对象数组的分配避免创建了ArrayList却从不使用导致的没必要的内存开销。这个细节第一个坑就来了无参构造的ArrayList默认容量是0而不是10。手动new ArrayList(10)才是真正一次性分配了容量为10的Object数组。很多人在写代码时以为new ArrayList()后就已经有10个坑位了这其实是理解偏差。2.2 add方法走过的完整链路再来跟踪add方法public boolean add(E e) { ensureCapacityInternal(size 1); elementData[size] e; return true; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount; if (minCapacity - elementData.length 0) { grow(minCapacity); } }这里要注意的是第一步先检查当前数组是否还是空数组如果是就直接把minCapacity提升到DEFAULT_CAPACITY也就是10这就是第一次add时容量才变成10的由来第二步判断minCapacity是否超过当前数组长度超过才真正执行扩容grow。2.3 grow方法里的位运算和扩容因子核心的扩容逻辑在grow方法private void grow(int minCapacity) { int oldCapacity elementData.length; int newCapacity oldCapacity (oldCapacity 1); if (newCapacity - minCapacity 0) { newCapacity minCapacity; } if (newCapacity - MAX_ARRAY_SIZE 0) { newCapacity hugeCapacity(minCapacity); } elementData Arrays.copyOf(elementData, newCapacity); }oldCapacity (oldCapacity 1)这里oldCapacity 1是取oldCapacity的一半所以结果就是原来的1.5倍。举个例子容量10的数组扩容一次变成15再扩容变成22然后是33、49……这是面试官最爱追问的地方。第一个追问为什么是1.5倍而不是2倍我的理解是这本质上是一个空间换时间的平衡问题。如果扩容系数太大比如2倍那么每次扩容都会浪费大量内存特别是当ArrayList里存的是大对象时一次扩容多出来的空间全是白白占用的如果扩容系数太小比如1.25倍那扩容次数会大幅增加每次扩容都要执行一次Arrays.copyOf——底层是System.arraycopy的native数组拷贝操作频繁拷贝的性能开销很大。1.5倍是一个经验上比较合理的折中既避免了频繁扩容又不至于一次扩太多导致空间浪费。第二个追问扩容失败/超大容量时会发生什么看代码里的MAX_ARRAY_SIZE判断private static final int MAX_ARRAY_SIZE Integer.MAX_VALUE - 8; private static int hugeCapacity(int minCapacity) { if (minCapacity 0) { throw new OutOfMemoryError(); } return (minCapacity MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }ArrayList最大容量是Integer.MAX_VALUE - 8之所以减8是因为某些JVM实现里数组对象头需要额外的存储空间直接分配Integer.MAX_VALUE可能会触发OOM。当minCapacity变成负数说明size已经超过int能表示的范围或者接近上限时就直接抛OutOfMemoryError了。这个细节如果面试时能主动说出来是很加分的因为它体现了你对JVM层限制的关注。2.4 ensureCapacity方法被低估的优化手段知道了扩容逻辑后有一个很实用的优化手段预分配容量。ArrayListInteger list new ArrayList(10000);如果你能预估到数据规模大约是10000条直接指定初始容量就可以避免整个添加过程中反复扩容。每次扩容都涉及数组的创建和全部元素的拷贝虽然均摊复杂度还是O(1)但真实场景下由于GC和内存分配的存在频繁扩容对性能的影响比理论上看到的要大得多。我在实际项目中处理过几万条数据写入ArrayList的场景提前指定容量后耗时能明显下降这属于投入产出比很高的微优化。注意点初始容量设得过大也是一种浪费因为底层会一次性分配整个Object数组。如果你的数据量只有几百条却new ArrayList(1000000)内存里就先躺着一个接近8MB的空数组引用类型数组每个元素占4字节或8字节这笔开销非常不值。3. LinkedList的真实面纱双向链表的价值与它的性能尴尬3.1 链表之外它还默默实现了DequeLinkedList的底层是双向链表这个大家都知道。但很多人忽略了一点LinkedList同时实现了List和Deque两个接口所以它除了get/add之外还支持addFirst/addLast、removeFirst/removeLast、push/pop这类双端队列操作。这也是它常被用来实现栈或队列的原因——虽然Java官方推荐用ArrayDeque来当栈用但LinkedList依然是一个合法的备选。每个Node节点长这样private static class NodeE { E item; NodeE next; NodeE prev; }一个节点除了存数据还维护前驱和后继两个引用。这意味着每个元素除了本身的数据之外还要多占两个引用字段的空间在64位JVM上开了压缩指针后一般是16字节左右如果存储的是大量的小对象LinkedList的内存开销会比ArrayList高不少。3.2 增删真的比ArrayList快吗分场景看面试时背LinkedList增删快这个结论其实是要分场景的在头尾两端插入addFirst/addLastLinkedList确实快时间复杂度是O(1)只需要改指针。但ArrayList在头部插入需要把整个数组往后搬是O(n)。这是LinkedList真正有优势的场景。在随机中间位置插入LinkedList反而不占优势。虽然理论上链表的插入操作本身是O(1)但你得先找到要插入的位置这个查找过程是O(n)的——它需要从头部或尾部开始一个个遍历LinkedList底层会根据Index离头部近还是尾部近来决定从哪个方向遍历。而ArrayList在中间位置插入虽然也需要把后面的元素整体移位但System.arraycopy是native方法经过JIT优化后批量移动的效率非常高。实测数据1万条数据量级中间位置插入5000次LinkedList的耗时往往是ArrayList的几倍甚至几十倍。至于删除情况类似。ArrayList删除末尾元素是O(1)删除中间元素需要移动后续元素LinkedList删除已知节点是O(1)但按值或按下标删除时查找还是要O(n)。我自己的看法纯粹从性能角度LinkedList在绝大多数业务场景下都没有优势除非你的核心操作就是频繁地在头部或尾部插入删除并且数据量很大。这也是为什么很多资深开发会说ArrayList能解决90%用List的场景。3.3 遍历LinkedList的经典翻车现场下面这个写法是很多新手会踩的坑LinkedListInteger list new LinkedList(); for (int i 0; i list.size(); i) { Integer value list.get(i); // 每次get都是O(n) // do something }这段代码的时间复杂度是O(n²)。因为每次调用get(i)都要从链表的头部或尾部重新遍历到第i个位置做完第i次get后下一次get又要从头开始走。数据量从1000涨到10000耗时不是涨10倍而是涨100倍。正确的写法是用迭代器或者foreachfor (Integer value : list) { // do something }foreach语法糖在编译后会被转换成迭代器遍历迭代器的next()方法内部维护了一个当前节点的引用每次只是把指针往后挪一格整体复杂度O(n)。这里还涉及一个RandomAccess接口的设计ArrayList实现了RandomAccessLinkedList没有实现。RandomAccess是一个标记接口表示支持随机访问。你的代码里可以这样判断遍历方式if (list instanceof RandomAccess) { for (int i 0; i list.size(); i) { ... } } else { for (Object item : list) { ... } }这个细节在Collections.binarySearch这类工具类里会被大量使用也是面试官很可能会顺带追问的点。为什么LinkedList需要迭代器而不能用for循环配合get因为get方法的链式查找成本太高直接把这层逻辑封装到iterator里遍历一遍只需要O(n)。3.4 缓存局部性容易被忽略的性能黑手再深挖一层LinkedList遍历时虽然时间复杂度是O(n)但实际性能依然明显慢于ArrayList这里有个底层硬件层面的原因——CPU缓存。ArrayList底层是连续内存的数组遍历时能很好地利用CPU的缓存预取机制把后面连续的数据一次性加载到高速缓存中遍历过程几乎都在缓存里命中速度极快。而LinkedList的节点在内存里是分散的每个节点的地址不连续缓存命中率很低每次访问都可能要走主内存这个差距在数据量大时会被放大。这个点如果在面试里主动说出来面试官通常会眼睛一亮因为这已经超过背八股文的范畴说明你真正理解性能差异的根源。不过面试时要留意分寸不要背太抽象可以用自己的话说LinkedList的节点分散在堆内存的不同位置遍历时CPU没法按顺序预加载而ArrayList就是一块连续地址对缓存非常友好。4. fail-fast机制foreach里删除元素为什么会炸4.1 一段让无数人栽跟头的代码ListString list new ArrayList(); list.add(a); list.add(b); list.add(c); for (String s : list) { if (b.equals(s)) { list.remove(s); } }这段代码运行后会抛java.util.ConcurrentModificationException。面试官最常问的就是为什么报错为什么有时候删最后一个元素又不报错这两个问题都能答上来的人其实已经超过了大多数候选人。4.2 modCount和expectedModCount核心机制拆解ArrayList内部维护了一个字段modCount表示这个列表被结构性修改新增、删除、扩容等会改变列表大小的操作的次数。而ArrayList的迭代器Itr里有一个expectedModCount字段在创建迭代器时被初始化为当前的modCount值。每次迭代器调用next()时会先执行public E next() { checkForComodification(); ... } final void checkForComodification() { if (modCount ! expectedModCount) { throw new ConcurrentModificationException(); } }如果modCount和expectedModCount不一样就说明在迭代过程中列表被别人改过了直接抛异常。这就是fail-fast的含义尽早暴露并发修改的问题而不是让程序带着脏状态继续跑下去。回到上面的代码foreach语法糖会创建迭代器expectedModCount 3此时modCount是3因为有三个add。当遍历到b时list.remove(s)会把modCount加1变成4而迭代器里的expectedModCount还是3。下一次循环调用next()时checkForComodification发现3 ! 4直接抛异常。那删最后一个元素为什么不报错是怎么回事看集合Iterator的hasNext()实现public boolean hasNext() { return cursor ! size; }如果删除的是倒数第一个元素c那么执行完remove后size变成2此时迭代器的cursor刚好也是2因为调用next()取c时cursor自增到了3但remove的是最后一个元素lastRet是2remove后size变成2cursor被修正为2不实际Itr的remove方法里有cursor lastRet这样的修正逻辑。说人话就是删的是最后一个元素时hasNext()返回false循环自然结束根本不会走到下一次next()去检查modCount所以异常没有机会被抛出。这其实是一个巧合不是安全的删除方式。正确删除方式有三种使用迭代器的remove方法IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (b.equals(s)) { iterator.remove(); } }因为Itr的remove方法会同步维护expectedModCount执行完把expectedModCount重置回modCount所以不会抛异常。使用JDK 8的removeIflist.removeIf(s - b.equals(s));这是内部封装好的方法底层也会用迭代器但由JDK自己处理了modCount的同步最推荐。倒着删除从后往前遍历for (int i list.size() - 1; i 0; i--) { if (b.equals(list.get(i))) { list.remove(i); } }这不是用迭代器而是用下标遍历删除后后面的元素前移但前面的元素不受影响所以不会跳过元素也不会抛异常。注意只有ArrayList这类随机访问的List可以这样玩LinkedList用get(i)会导致O(n²)。4.3 fail-safe与CopyOnWriteArrayList和fail-fast相对的是fail-safe一个典型的例子就是CopyOnWriteArrayList。它的迭代器是在创建时对底层数组做了一个快照迭代时遍历的是快照数组所以迭代过程中即使原list被改动了迭代器也不会抛异常只是看不到迭代期间发生的修改。这个快照思路的代价是每次修改add/set/remove都会复制整个底层数组所以CopyOnWriteArrayList是极度偏向读多写少场景的。如果写操作频繁频繁复制数组带来的开销和GC压力会非常明显。面试里被问到CopyOnWriteArrayList为什么读操作不需要加锁时可以说因为读操作面对的是一个永远不会被修改的快照数组天然线程安全读不需要锁写操作则加了ReentrantLock来保证复制和替换的原子性。5. Arrays.asList和subList两个日常开发里最容易踩的List暗坑5.1 Arrays.asList返回的根本不是ArrayList很多人觉得Arrays.asList就是转成一个ArrayList这其实是个误判。看代码ListString list Arrays.asList(a, b, c); list.add(d); // 运行时报错UnsupportedOperationException原因在于Arrays.asList返回的是java.util.Arrays内部的一个私有静态类Arrays$ArrayList它继承自AbstractList但并没有重写add和remove方法所以调用add时会走到AbstractList的默认实现直接抛UnsupportedOperationException。同时Arrays.asList返回的list是对原数组的一个视图view不是拷贝。修改list里的元素原数组的值也会跟着变。这个视图特性在实际业务中很容易埋雷——你以为你处理的是一个独立的List其实它背后还牵着一个数组。另外还有基础类型数组的坑int[] arr {1, 2, 3}; Listint[] list Arrays.asList(arr);因为泛型只接受对象类型int[]会被当成一个整体对象所以list的size其实是1元素是一个int数组对象不是三个数字。想正确转的话用Integer[]数组或者用Java 8的StreamListInteger list Arrays.stream(arr).boxed().collect(Collectors.toList());5.2 subList的视图机制与ConcurrentModificationExceptionsubList同样是个看起来人畜无害的方法ListString original new ArrayList(); original.add(a); original.add(b); original.add(c); original.add(d); ListString sub original.subList(1, 3); // [b, c]你以为这会生成一个独立的新list吗不是它返回的是SubList内部类的一个实例持有原list的引用任何修改都会直接作用到原list上。更隐蔽的是当你对sub做了结构性修改后再去操作原list或者反过来都可能抛ConcurrentModificationException。因为SubList会检查原list的modCount变化subList本身和原list共享同一个modCount一旦原list的size发生改变比如add了一个元素再去操作subList就会抛异常。这本质上和fail-fast是同一套机制在起作用。我自己在实际开发中遇到过这样的场景从数据库查出一批数据用subList取了前20条做展示业务逻辑里不小心在原list上又add了一条然后再去遍历subList直接炸了。排查了半天才定位到是subList视图机制的问题。所以现在我的习惯是需要独立的子列表时直接new ArrayList(original.subList(1, 3))先拷贝一份再说。这样既断了和原list的关联也避免了后续各种modCount的坑。5.3 使用Lists.partition的场景注意类似地在需要分页或分批处理时很多人会用List.subList手动实现分页但如果原list期间被修改过subList就会失效。我更推荐用List.subList做完后立即转成新ArrayList或者使用一些工具类如Guava的Lists.partition它内部也是基于subList实现的但每次返回的视图依然受同样的modCount规则约束。所以不管用什么工具都要记住一点凡是从一个list派生出来的子视图都不适合在并发修改场景下长期持有。6. 排序、去重、线程安全List实战里最有区分度的三大综合题6.1 Java 8之后List排序的正确姿势面试里经常出排序的题但很多人不知道Java 8之后List有默认的sort方法Java 8集合新增的默认方法底层会调用Arrays.sort然后再把排好序的数组复制回list。标准用法list.sort(Comparator.comparing(User::getAge)); list.sort(Comparator.comparing(User::getAge).reversed()); list.sort(Comparator.comparing(User::getAge).thenComparing(User::getName));Comparator链式写法里thenComparing是实现多条件排序的关键。实际业务中经常出现先按年龄排再按姓名排再按入职时间排这种需求一条链子写清楚比写多个if判断要优雅得多。传统方式Collections.sort和list.sort的区别前者是Collections工具类的静态方法调用时也会走list.sort同一条路径后者是List接口的默认方法直接调用更简洁。两者最终都调用了Arrays.sort。另外还有一个使用Stream的方式ListUser sorted list.stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());stream的sorted不会修改原list而是返回一个新的list这对不想改动原数据的场景很友好。注意如果待排序的list里可能出现null元素Comparator还需要加nullsFirst或nullsLast否则NPE。6.2 去重的不同实现与复杂度对比List去重也是一个高频面试场景。最简单的是借助LinkedHashSetListString newList new ArrayList(new LinkedHashSet(list));这个方法的原理HashSet能保证元素不重复LinkedHashSet在HashMap基础上还维护了插入顺序所以既去重又保留了原来的相对顺序。哈希查找的时间复杂度O(1)整体去重效率O(n)。Java 8的stream也有一个distinctListString newList list.stream().distinct().collect(Collectors.toList());distinct底层依赖元素的equals和hashCode内部用LinkedHashSet实现的它同样会保留第一次出现时的顺序。这两者用哪个其实差别不大LinkedHashSet写法更简洁stream写法更灵活可以先filter再去重。如果对自定义对象去重需要正确重写equals和hashCode否则去重失效。这里有个实际场景想按User对象里某个字段去重而不是整个对象去重就不适合用HashSet了。这时可以用stream的Collectors.toMapListUser distinctUsers list.stream() .collect(Collectors.collectingAndThen( Collectors.toMap(User::getId, Function.identity(), (a, b) - a, LinkedHashMap::new), map - new ArrayList(map.values()) ));思路是把id作为key遇到重复key时保留第一个(a,b) - a用LinkedHashMap保持插入顺序。这种写法在面试时提出来往往比常规答案更亮眼。6.3 线程安全的List选型synchronizedList、CopyOnWriteArrayList还是Vector老生常谈的Vector已经被官方不推荐了因为它的所有方法都加了synchronized锁粒度太粗并发性能差。实际开发中常用的是这两个Collections.synchronizedList(new ArrayList())CopyOnWriteArrayListsynchronizedList的读写操作都加锁适合写多读少或读写比较均衡的场景但注意它内部的迭代器遍历不是自动同步的需要手动加synchronized包裹遍历代码不然依然会有并发问题。CopyOnWriteArrayList适合读多写少场景。它的读操作完全无锁写操作会复制整个数组。举个例子配置中心Configuration这种启动时加载一次之后频繁被读的场景就非常适合CopyOnWriteArrayList。但如果一个list每秒写入几百次CopyOnWriteArrayList的复制开销会非常恐怖。这道题面试环节通常会以有一个并发场景多个线程读写同一个list你会怎么做的方式出现。除了给出选型建议再补一句如果业务场景能接受读多写少首选CopyOnWriteArrayList如果写操作占比高用Collections.synchronizedList如果你的场景本质上不需要共享list优先使用线程封闭每个线程持有自己的list。这最后一句话在很多面试官看来是加分的说明你有架构思维。7. 面试官深挖List的完整链路与高分回答框架7.1 一个典型面试追问链路我参加过不少面试也当过面试官。总结下来一个围绕List的完整追问链路通常长这样第一层ArrayList和LinkedList的区别考察点底层数据结构、复杂度差异第二层ArrayList扩容机制初始容量为什么1.5倍考察点源码阅读习惯、位运算、内存意识第三层为什么LinkedList的for循环遍历这么慢考察点复杂度分析、迭代器原理、是否踩过性能坑第四层foreach删除元素为什么会抛异常考察点fail-fast机制、Iterator的hasNext/next实现第五层什么场景下选择CopyOnWriteArrayList考察点并发编程、快照机制、优缺点权衡第六层如何让一个List变成线程安全考察点工具类API熟悉度、并发场景设计能力这几层问题看起来分散内核其实是同一个你是否真正理解List不同实现类的底层行为并且能把这些行为映射到真实业务场景里去取舍。如果候选人能自己把一条链路串起来讲明白我基本可以判断他对集合这块知识不是死记硬背的。7.2 一个可以直接套用的分层回答话术关于ArrayList和LinkedList区别这道题不建议只给一个查询快慢、增删快慢的简单答案。一个比较立体的回答结构是第一层给结论ArrayList基于动态数组LinkedList基于双向链表ArrayList随机访问O(1)LinkedList随机访问O(n)ArrayList尾部插入均摊O(1)LinkedList头尾插入O(1)。第二层补边界条件ArrayList中间位置插入需要移动元素LinkedList虽然插入本身O(1)但定位插入点需要O(n)遍历所以中间插入不占优势LinkedList每个节点多了前后指针内存占用更高ArrayList扩容时涉及数组拷贝链表扩容则只是新增节点。第三层给场景建议如果读操作远多于写操作选ArrayList如果明确是头尾频繁插入删除、读操作几乎全靠遍历才考虑LinkedList实际业务里90%的情况ArrayList就够了。第四层可选加分顺带提一句RandomAccess标记接口和CPU缓存局部性对性能的影响然后再提一嘴如果考虑线程安全这两个都不是线程安全的需要额外手段。这套结构的好处是层层递进面试官想打断就能打断想深入就能顺着深入不想深入听到第一层就足够了。它不会让人觉得你在背答案更像是在和一个有经验的同事讨论技术。7.3 从List延伸出去的知识点面试官会在哪里转向List八股文问到最后面试官往往会找一个方向延伸出去从ArrayList讲到HashMap因为HashMap在JDK 8之后也用到了链表和红黑树链表和树之间的转换阈值8和64是一个非常经典的追问点。这个转向很自然因为都是集合框架的内容。从CopyOnWriteArrayList讲到并发容器比如ConcurrentHashMap的分段锁/CAS机制这就从List跳到了Map。从LinkedList讲到ArrayDeque进而问到栈和队列的实现方式再问到你所在业务里哪里用到了栈/队列。这个方向可以考察候选人对数据结构在实际项目中应用的能力。从ArrayList的元素存储讲到了内存分配进而引出Java对象在堆里的内存布局问题这就和JVM挂钩了。提前预判这些延伸方向对面试者来说非常有利。你可以在答List的题时有意地埋下一两个钩子比如提到类似地HashMap在链表长度超过8时也会转红黑树把话题引向自己熟悉的方向掌握面试节奏。这算是面试里比较高阶的技巧了。我自己在面试别人时最看重的反而不是标准答案本身而是候选人被追问到源码层面时还能不能保持逻辑清晰。能讲到RandomAccess标记接口和CPU缓存局部性这个深度的人意味着他确实写过一定规模的数据处理代码且真的观察过不同List实现的性能差异。如果你能把这些细节有条理地讲出来不光能过面试在真实业务里也大概率能用得上。List的八股文不需要怕它其实是一块很好的试金石帮助你把底层数据结构、Java集合设计思想、并发编程意识串成一张网。把这张网织好了面试时不再怕被追问写代码时也能做出更靠谱的技术选型。