尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java集合核心原理与面试指南:从ArrayList到HashMap深度解析
1. 从会用到懂原理Java集合复习到底在复习什么说实话Java集合这块东西很多写了三五年代码的人也不敢拍胸脯说完全搞懂了。平时CRUD写得飞起ArrayList一把梭HashMap用得滚瓜烂熟可真到了面试或者线上排查问题的时候才发觉自己只是会用不是懂原理。集合这个知识点比较特殊——它不像JVM调优那样偏门也不像并发编程那样陡峭它就是日常开发里每天都要碰的基础设施。但也正因为它太常用了反而容易被忽略。很多人复习集合就是背一遍ArrayList和LinkedList的区别、HashMap的put流程然后面试的时候背给面试官听。这种复习方式不能说错但它解决不了实际问题。我理解的复习集合应该是三个层次第一层是API层面知道有哪些集合类各自怎么用增删改查的API是什么。第二层是数据结构层面知道每个集合底层是什么数据结构增删改查的时间复杂度是多少为什么这个场景要用ArrayList而不是LinkedList。第三层是设计思想层面知道为什么Java集合框架要设计成接口、抽象类、实现类三层结构迭代器模式解决了什么问题fail-fast机制到底在保护什么。这三个层次刚好对应了日常开发、线上排错、面试深挖三个场景。这篇文章我打算把这三个层次串起来讲一遍重点放在那些文档里不会直接写、但实际工作中一定会遇到的细节上。先说一下这篇复习笔记适合谁。如果你是刚学完Java基础、准备找工作的应届生这篇文章能帮你把集合的知识点串成体系而不是东一榔头西一棒槌。如果你是有几年经验的开发准备跳槽或者梳理基础知识这篇文章里有很多面试官真正想问的东西。哪怕你只是想在项目里把集合用得更好读一读也不亏。Java集合框架的整体结构一句话可以概括两大体系三个接口若干实现类。两大体系就是Collection体系和Map体系三个接口是Collection、List、SetMap是独立体系实现类就是ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap这一堆。复习的第一步就是把这张地图刻在脑子里然后往每个节点上填细节。2. 先搞懂这棵接口继承树后面所有细节才有地方挂靠2.1 Collection体系的一条主线Collection → List/Set → 具体实现类很多初学者有一个很常见的困惑为什么Java集合要设计这么多接口直接用ArrayList不就行了吗这个问题其实回答了一个核心设计问题面向接口编程。Collection是所有单列集合的顶层接口它定义了集合最基本的操作规范——add、remove、size、contains、isEmpty、iterator这些。List在Collection的基础上增加了有序、可重复、可通过索引操作的语义所以多了get(int)、add(int, E)、remove(int)这类方法。Set则强调不可重复所以没有索引相关的方法。有了这层设计你的代码就可以这样写public void process(ListString data) { // 只管用List接口的方法不关心具体实现 }这个方法传ArrayList也行传LinkedList也行传CopyOnWriteArrayList也行。调用方想换实现根本不用改这个方法内部的代码。这就是接口的意义——把能做什么和怎么做解耦。继承树上的具体实现类每个都有明确的定位ArrayList基于动态数组查询快、增删慢尾部增删除外日常开发最常用。LinkedList基于双向链表头尾操作快、中间查询慢同时实现了List和Deque双接口。VectorArrayList的古代版本方法加了synchronized性能差已经基本被淘汰。Stack继承自Vector的栈实现同样因为继承设计和性能问题官方推荐用ArrayDeque代替。HashSet基于HashMap实现存取快但无序。LinkedHashSet继承HashSet底层是LinkedHashMap维护插入顺序。TreeSet基于TreeMap红黑树元素有序但操作复杂度是O(log n)。2.2 Map体系不是Collection的小弟而是并列的独立体系Map和Collection最大的区别在于Map存的是键值对一次存两个对象Collection存的是单个对象。这个本质差异决定了Map不可能继承Collection——你没法用Collection的add(E)语义去描述put(K, V)这种操作。Map体系的几个核心实现HashMap基于数组链表红黑树允许null键和null值无序最常用。LinkedHashMap继承HashMap额外维护了双向链表可以保持插入顺序或访问顺序accessOrder参数是实现LRU缓存的基础。TreeMap基于红黑树按键的自然顺序或自定义Comparator排序不允许null键。HashtableHashMap的古代版本线程安全但性能差已经过时。ConcurrentHashMap线程安全的高性能Map分段锁/CAS synchronized实现并发场景首选。这里插一句面试的时候经常有人把Hashtable和HashMap的区别背得滚瓜烂熟但你要是问他为什么有了Hashtable还要设计ConcurrentHashMap就答不上来了。根本原因在于Hashtable的线程安全是对所有方法加synchronized相当于给整个表加了一把大锁并发高的时候性能急剧下降。ConcurrentHashMap用的是锁分段/细粒度锁的思路读操作几乎不加锁写操作只锁对应的桶并发能力完全不同。2.3 迭代器与fail-fast机制遍历集合时的隐形规则复习集合一定会碰到迭代器。Iterator接口的设计意图很纯粹把遍历逻辑从集合实现中抽离出来不管你底层是数组还是链表还是树你只要拿到Iterator就能用统一的方式遍历。这背后是典型的迭代器设计模式。但真正工作中容易踩坑的是fail-fast机制。简单说在用迭代器遍历集合的过程中如果集合的结构被修改了比如调用了add、remove迭代器会在下一次调用next()时抛出ConcurrentModificationException。这个机制的原理不复杂迭代器内部维护了一个expectedModCount字段初始化时等于集合的modCount。集合每次结构性修改modCount都会1。迭代器在每次next()时都会检查expectedModCount和modCount是否一致不一致就抛异常。但注意fail-fast是尽量检测而不是一定检测。它是通过modCount的变化来感知并发修改的某些修改操作比如修改已有元素的值不会改变modCount也就不会被检测到。所以不要依赖fail-fast来保证安全性它只是在帮你尽早发现问题。遍历中如果确实需要删除元素正确姿势是用迭代器自己的remove方法IteratorString it list.iterator(); while (it.hasNext()) { String item it.next(); if (delete.equals(item)) { it.remove(); // 正确迭代器自己维护了modCount } }或者用JDK 8以后更优雅的写法list.removeIf(item - delete.equals(item));3. 核心实现逐个拆解ArrayList、LinkedList、HashMap的底层逻辑3.1 ArrayList动态数组的扩容机制与性能陷阱ArrayList可能是Java里最常用的集合类但大部分人只是无脑add从没想过它内部是怎么扩容的。ArrayList的底层就是一个Object数组。当你new ArrayList()的时候它创建的是一个空数组第一次add元素时数组会扩容到默认容量10之后每次容量不够就按1.5倍扩容新容量 旧容量 (旧容量 1)。来看一下扩容的核心逻辑private Object[] grow(int minCapacity) { int oldCapacity elementData.length; int newCapacity oldCapacity (oldCapacity 1); // 如果1.5倍还不够就用minCapacity if (newCapacity - minCapacity 0) { newCapacity minCapacity; } return elementData Arrays.copyOf(elementData, newCapacity); }每次扩容都要创建一个新数组然后把旧数组的所有元素拷贝过去。这个拷贝操作是O(n)的如果数据量大、add次数多反复扩容的开销非常可观。所以如果你预先能估到数据量一定要用指定初始容量的构造方法ListString list new ArrayList(10000);这个习惯能省掉很多次数组拷贝在大数据量场景下性能差距是数量级的。还有一个坑ArrayList的subList方法返回的是原集合的视图不是新集合。对这个视图做add、remove操作会直接修改原集合同时原集合的modCount也会变化。如果你在操作subList之后再去遍历原集合很容易触发ConcurrentModificationException。很多人不知道这个细节排查了半天才发现问题出在subList上。3.2 LinkedList双向链表的结构与看似美好的操作LinkedList底层是双向链表每个节点维护了prev和next两个指针。因为有了这些指针它在头尾插入删除的场景下确实是O(1)的这是它比ArrayList强的地方。但很多人对LinkedList有个误解以为链表操作什么都快。实际上链表的随机访问是O(n)的——你要找第5个元素必须从head开始一个一个往下走。ArrayList的get(int)是O(1)的直接按下标定位。还有一个隐藏的性能问题LinkedList在中间插入元素时虽然插入本身只是修改指针但找到插入位置是需要遍历的。所以LinkedList插入快是有条件的只有在已知节点的前后插入才是O(1)如果你要在第100个位置插入光定位就要O(n)。实际开发中的选择建议是场景推荐原因频繁随机访问、遍历ArrayList连续内存CPU缓存友好get是O(1)频繁在头部/尾部增删ArrayDeque / LinkedList头尾操作O(1)频繁在中间增删都不是最优建议评估数据结构是否合理数据量未知但巨大优先预估容量用ArrayList链表节点对象本身也有内存开销顺便说一句LinkedList空间利用率也比ArrayList低。ArrayList是连续数组每个元素就是一个引用LinkedList每个节点除了元素引用还要额外存储两个指针内存开销大。数据量大的时候LinkedList占用的内存可能是ArrayList的好几倍。3.3 HashMap数组链表红黑树的完整故事HashMap是整个Java集合框架里最值得深挖的一个类没有之一。它涉及了哈希算法、数组索引、链表冲突、树化退化、扩容迁移、负载因子设计等多个知识点每一块都能展开讲很久。先说整体结构HashMap底层是一个Node数组每个数组元素桶要么是null要么是一个链表的头节点要么是一棵红黑树的根节点。put一个键值对的时候流程是这样的对key计算hash值为了让高位也参与运算做了扰动处理(h key.hashCode()) ^ (h 16)。用hash值和数组长度减一做位与运算得到桶的下标(n - 1) hash。之所以用位与而不是取模是因为数组长度是2的幂次时(n-1) hash等价于hash % n但位与运算更快。如果桶里没有元素直接放进去如果有说明发生了哈希冲突遍历链表找有没有相同key有就覆盖没有就尾插新节点。链表长度超过阈值8并且数组长度超过64就把链表转成红黑树。这个过程中有两个值得仔细想的点第一个点为什么加载因子是0.75JVM默认的加载因子是0.75这是时间成本和空间成本的一个折中。加载因子越大比如1链表越容易变长哈希冲突增多查询效率降低加载因子越小比如0.5冲突减少但很多空间被浪费数组扩得早、扩得频繁浪费内存。0.75是官方在大量测试后给出的平衡点实际工程里基本不用改。第二个点扩容为什么是2倍HashMap的扩容是数组长度翻倍。这里有个很精妙的设计扩容后重新计算下标时因为容量是2的幂次每个元素的新位置只有两种可能——原下标或者原下标旧容量。判断依据就看hash值在新增的那一位上是0还是1。// 扩容迁移时的判断逻辑 if ((e.hash oldCap) 0) { // hash值在新增bit位上是0位置不变 } else { // hash值在新增bit位上是1位置 原位置 oldCap }这样做的好处是不用重新计算每个元素的hash值hash值本身没变只需要做一次位与运算就能确定新位置同时原来在同一链表上的元素会均匀分散到两个位置上大大缩短了链表长度。还有一个在JDK 8中非常重要的变化链表插入从头插法改成了尾插法。JDK 7的头插法在并发扩容时可能导致链表形成环从而在get时出现死循环。JDK 8改成尾插法后这个问题从机制上得到了缓解。但是要说清楚的是HashMap依然不是线程安全的并发写场景千万要用ConcurrentHashMap不要因为尾插法解决了死循环就觉得可以裸用HashMap了。3.4 HashSet、LinkedHashMap和TreeMap被低估的细节控集合HashSet这个类本身几乎没有什么逻辑它内部就是一个HashMappublic class HashSetE extends AbstractSetE implements SetE { private transient HashMapE,Object map; // 所有元素都放在key上value统一用一个dummy对象 private static final Object PRESENT new Object(); public boolean add(E e) { return map.put(e, PRESENT) null; } }所以HashSet的复习重点其实在HashMap上。只要HashMap搞懂了HashSet就是用key去重的应用场景没什么额外概念。LinkedHashMap值得单独说一说因为它太常被用来做LRU缓存了。它继承了HashMap但内部额外维护了一条双向链表用来记录元素顺序。有两个构造参数值得记住LinkedHashMapK, V map new LinkedHashMap(initialCapacity, loadFactor, accessOrder);第三个参数accessOrder默认是false表示按插入顺序维护设为true则表示按访问顺序维护每次get或者put访问元素后这个元素会被移动到链表末尾。基于这个特性实现一个简单的LRU缓存只需要两步class LRUCacheK, V extends LinkedHashMapK, V { private final int maxCapacity; public LRUCache(int maxCapacity) { super(maxCapacity, 0.75f, true); this.maxCapacity maxCapacity; } Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() maxCapacity; } }removeEldestEntry这个钩子方法在每次put后会被调用返回true就把最久没访问的Entry移除。这个用法在面试里问得很多实际项目里做本地缓存也很好用。TreeMap就是一个基于红黑树的有序Map它的核心价值是按键排序。你可以在构造时传一个ComparatorMapString, Integer map new TreeMap((a, b) - b.compareTo(a)); // 倒序TreeMap的操作复杂度是O(log n)比起HashMap的O(1)肯定慢但它自带排序能力在某些场景下比存完再排序要高效得多。不过要注意TreeMap不允许key为null因为要比较大小null没法比。4. 日常开发里的正确姿势初始化、遍历、不可变集合与常见坑4.1 优先使用Arrays.asList和List.of但要注意结构性限制创建一个List最传统的方式是ListString list new ArrayList(); list.add(a); list.add(b); list.add(c);其实有更简洁的写法// 方式一Arrays.asList返回的是固定大小的List ListString list Arrays.asList(a, b, c); // 方式二JDK 9的List.of返回的是不可变List ListString list List.of(a, b, c);这里有两个很容易踩的坑。Arrays.asList返回的List是Arrays内部类ArrayList注意不是java.util.ArrayList它是一个大小固定的List支持set修改元素但不支持add和remove。如果你往里add会抛UnsupportedOperationException。很多人习惯性地把asList的返回值当作普通ArrayList用一add就报错还以为是JDK的bug其实人家就是这么设计的。List.of更进一步它返回的是一个真正不可变的List连set都不允许。它的优点是因为不可变所以可以用更紧凑的内存布局也更加安全不用担心被意外修改。如果你的数据本来就是固定的、初始化后不会变优先用List.of。4.2 遍历方式的选择for-each、迭代器与Stream的适用边界遍历集合是每天都会做的事但很多人只是能用就行从未想过不同遍历方式各有优劣。for-each循环本质上是语法糖编译后会变成Iterator遍历。它适用于大多数场景代码简洁可读。但如果需要在遍历过程中删除元素直接用for-each就会抛ConcurrentModificationException这时候要用迭代器或removeIf。普通for循环配合get(i)只适用于List因为它依赖随机访问能力。在ArrayList上没问题但在LinkedList上用get(i)遍历就是O(n²)的灾难——每次get都要从头遍历链表。Stream遍历是JDK 8之后的新宠。它的优势在于可以链式组合过滤、映射、收集等操作配合Lambda表达式代码非常简洁ListString filtered list.stream() .filter(s - s.startsWith(a)) .map(String::toUpperCase) .collect(Collectors.toList());但要注意Stream不是万能的。如果只是简单遍历不涉及复杂的链式操作普通for-each的性能和可读性都更好。Stream适合的是数据处理流水线不是无脑替代for循环。4.3 Collections工具类那些锦上添花的静态方法java.util.Collections是个宝藏工具类提供了大量操作集合的静态方法但很多人用到的只有sort和shuffle。比较实用的几个// 创建不可变集合 ListString emptyList Collections.emptyList(); MapString, String singletonMap Collections.singletonMap(key, value); // 创建线程安全的集合包装 ListString syncList Collections.synchronizedList(new ArrayList()); MapString, String syncMap Collections.synchronizedMap(new HashMap()); // 查找和替换 int index Collections.binarySearch(list, key); Collections.reverse(list); Collections.fill(list, default);关于synchronizedList有个细节要特别提醒它只是对每个方法加了synchronized但复合操作不是原子的。比如先判断contains再add这种操作两个方法中间的间隙依然是线程不安全的你仍然需要自己加锁。所以如果是并发场景优先考虑JUC包下的CopyOnWriteArrayList、ConcurrentHashMap这些专门为并发设计的集合而不是Collections.synchronizedList。4.4 集合的数组互转toArray与Arrays.asList的类型陷阱集合转数组是一个高频操作但很多人踩过类型相关的坑。集合转数组有两种姿势// 方式一不传参数返回Object[] Object[] objects list.toArray(); // 方式二传入指定类型的数组 String[] array list.toArray(new String[0]);推荐使用第二种方式传入一个长度为0的数组。有人疑惑为什么不传new String[list.size()]其实这两种写法都可以但传0的写法更简洁而且JDK会根据集合实际大小重新分配数组。传入更大数组也不是不行只是多出来的空间会被置为null没什么意义。当然如果你很在意那一次数组分配的性能可以传入size大小不过在绝大多数场景下这个优化可以忽略。数组转集合用Arrays.asList这在4.1节已经说过要小心它返回的是固定大小List。还有一个细节Arrays.asList(T... a)的泛型是T如果是基本类型数组比如int[]你得到的List里只有一个元素——这个int[]本身。所以基本类型数组要先装箱Integer[] arr {1, 2, 3}; ListInteger list Arrays.asList(arr); // 正确直接Arrays.asList(new int[]{1,2,3})得到的是Listint[]不是你要的结果。5. 集合相关的内存与并发问题OOM、ConcurrentModificationException与线程安全选型5.1 Java: OutOfMemoryError: Insufficient memory里集合常常是元凶看到这个报错很多人第一反应是JVM堆内存不够马上调-Xmx。但根据我的经验线上OOM有相当大比例是代码里集合使用不当导致的单纯加内存只是治标不治本。最常见的几种集合导致OOM的场景场景一无限往集合里塞数据。比如从数据库或者消息队列拉数据循环里不加限制地add到List里数据量又没控制好。这种要么是分页没做好要么是漏了limit结果就是堆内存被打爆。场景二缓存集合没有上限。用HashMap当缓存key一直变value一直塞从不考虑淘汰策略。这种场景应该用带淘汰策略的缓存框架Caffeine、Guava Cache或者自己用LinkedHashMap实现LRU4.3节里有代码。场景三集合嵌套导致内存膨胀。MapString, ListMapString, Object这种三层嵌套数据量一大内存开销是几何级数增长的。HashMap的Entry、ArrayList的扩容冗余每层都额外吃内存。场景四把大对象全部load到内存。比如一次性把一个巨大的Excel文件读进List每条记录是一个大对象。这种应该考虑流式处理边读边处理不要让所有数据常驻内存。排查OOM的时候我比较推荐的做法是先看错误日志是在什么操作时触发的如果堆栈里出现了集合相关的代码就重点查这个集合的size、数据的来源和生命周期。如果jmap -dump能拿到堆转储堆dump的话用MAT或者JProfiler分析一下对象引用树很快就能找到到底是谁在占用最多的内存。5.2 单个线程里也会遇到的ConcurrentModificationException很多人以为ConcurrentModificationException只会在多线程环境下出现这是不对的。单线程里照样会触发而且触发场景非常隐蔽。最典型的场景ListString list new ArrayList(List.of(a, b, c, d)); for (String item : list) { if (b.equals(item)) { list.remove(item); // 这里会抛 ConcurrentModificationException } }原因是for-each在编译后用的是迭代器迭代器内部会检查modCount而直接用List.remove会改变modCount但不会同步更新迭代器的expectedModCount所以下一次next()时就检测到不一致了。还有一个很隐蔽的坑遍历嵌套遍历时内层删除元素会影响外层的modCount检查。for (String outer : outerList) { for (String inner : innerList) { // 如果在这里修改了outerList外层迭代器也会抛异常 } }这类问题的解决方式前面已经提过用迭代器的remove方法、用removeIf、或者收集要删除的元素到临时集合最后统一removeAll。5.3 线程安全集合的选型策略ConcurrentHashMap、CopyOnWriteArrayList与并发集合关于线程安全集合我不建议你背HashMap线程不安全Hashtable线程安全ConcurrentHashMap也线程安全这种表面结论而是要理解每种方案的取舍。Hashtable所有方法都加synchronized锁粒度是整个表并发性能极差。基本可以当反面教材看待。Collections.synchronizedMap同样是全表锁但因为内部用mutex对象做同步相当于一把更大的锁。并发性能比Hashtable好一点但也有限。适合低并发、只是偶尔需要线程安全的场景。ConcurrentHashMapJDK 8的版本摒弃了分段锁改用CAS synchronized锁定单个桶链表头节点/树根节点。读操作get完全无锁通过volatile保证可见性。并发性能比前两者高一个量级是并发场景的首选。CopyOnWriteArrayList读多写少场景的利器。它的原理是写时复制——每次add或remove都会把底层数组复制一份在新数组上修改然后替换引用。读操作不加锁直接读volatile数组引用。因为每次写都复制数组所以写性能较差只适合读多写极少、集合本身不大比如配置列表的场景。ConcurrentLinkedQueue基于CAS的无界非阻塞队列适合生产者-消费者模式。它的优点是永远不阻塞但缺点也是无界——如果消费者处理不过来队列会无限增长最终OOM。选型的时候先把需求理清楚是读多写多读多写少需要有序需要去重几个问题问下来选型基本就明确了。如果说得更朴素一点大多数场景用HashMap和ArrayList就够了它们不需要线程安全真正需要并发安全的场景直接上ConcurrentHashMap和CopyOnWriteArrayList基本不会错。6. 回到面试本身高频题目背后的考点到底是什么6.1 先看一张高频面试题-核心考点对照表之前在带团队和帮朋友准备面试的过程中我总结了一个规律面试官问集合题看起来问的是具体用法实际上考的是三件事——底层数据结构、时间复杂度的推演、面对并发场景的判断力。高频问题表面考点深层考点ArrayList和LinkedList的区别数据结构能否根据复杂度选择合适集合HashMap的put流程JDK源码理解是否真读过源码而非背答案HashMap和Hashtable的区别基础记忆是否理解锁粒度与并发模型ConcurrentHashMap为什么快并发机制是否理解CAS、synchronized、volatileHashSet怎么保证去重底层复用是否理解equals和hashCode的约定TreeMap和HashMap怎么选排序与性能是否理解红黑树的适用边界为什么重写equals必须重写hashCodeJava基础坑能否解释HashMap中的查找逻辑这张表不用背它是用来对照的——如果你在准备面试不妨问自己一句这些问题如果换个角度问比如HashMap在并发下到底会发生什么HashSet怎么去重的底层是谁在干活我还能答得出来吗能答出来说明你复习到位了答不出来就顺着表格去补对应的底层知识。6.2 equals和hashCode的约定HashMap、HashSet正确工作的基石很多人在复习集合时会忽略equals和hashCode但这是一个大坑。面试里问为什么重写equals必须重写hashCode的频次可能比你想象的高得多。这个问题的本质在于HashMap和HashSet的查找机制。往HashMap里put一个键值对时第一步是用key的hashCode值经过扰动后定位桶第二步才是在桶里用equals比较有没有相同的key。如果你只重写了equals而不重写hashCode就会出现这种情况两个对象equals返回true但hashCode不同导致它们被放到不同的桶里。你在map.get(对象A)时计算出的桶位置和当初put(对象B)时的桶位置不一样就永远找不到——明明A.equals(B)是true但get返回null。反过来如果你只重写了hashCode而不重写equals那么即使两个对象hashCode相同、落到同一个桶里equals比较不相等就会被当成两个不同的key。正确的做法是public class User { private Long id; private String name; Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return Objects.equals(id, user.id) Objects.equals(name, user.name); } Override public int hashCode() { return Objects.hash(id, name); } }Objects.hash这个工具方法会按照指定的字段计算一个稳定的hash值省得自己手写。这个约定不仅影响HashMap还影响HashSet、HashTable、ConcurrentHashMap等所有基于哈希的集合。如果你自定义的对象要放进这些集合里equals和hashCode必须成对重写。6.3 集合框架里那些朴素但高区分度的面试题除了上面那些大考点有些小问题也能很好地考察一个人的基础扎不扎实。ArrayList的默认容量是多少什么时候扩容扩容成多少—— 这是个很细的题。默认容量10第一次add时扩容到10之后每次不足时扩容为原来的1.5倍。答出这三个多少说明你真看过源码。HashMap允许null键吗Hashtable呢—— HashMap允许null键但null键只能有一个放在index0的桶上。Hashtable不允许null键或null值因为它的put方法会直接检查key是否为null。HashSet的add方法返回值是什么—— 返回boolean如果元素已经存在就返回false。这个特性可以用来做去重判断if (set.add(str)) { 第一次见 } else { 重复了 }。Iterator和ListIterator有什么区别—— ListIterator是Iterator的子接口只适用于List新增了向前遍历previous、获取索引nextIndex/previousIndex、修改元素set、插入元素add等方法。Comparable和Comparator的区别—— Comparable是类自身实现的排序接口一个类只能实现一种自然排序Comparator是外部比较器可以根据不同场景实现多种排序逻辑。TreeSet、TreeMap、Collections.sort、Arrays.sort这些地方都用得到。这些都是背了就会、不背就不会的题但它们背后往往有一个更大的问题你平时写代码的时候有没有认真看过JDK的源码所以我的建议很朴素复习集合最好的方式不是背面试题而是打开JDK源码把ArrayList、LinkedList、HashMap三个类的核心方法各读一遍。读完你的理解深度会和单纯背答案完全不同。7. 几个值得动手验证的练习思路复习不能只看不练。这里推荐几个可以自己动手做的小练习每一个都能帮你在实际操作中巩固上面提到的知识点。练习一用数组实现一个简易ArrayList。要求支持add、get、remove、size方法并实现自动扩容比如扩容1.5倍。做完之后你会对为什么ArrayList随机访问快、中间插入慢有切身体会。练习二写一个程序验证LinkedList在中间插入和ArrayList在中间插入的性能差距。插入10万条数据到中间位置对比耗时。你会发现LinkedList并没有想象中那么快因为查找位置本身就要遍历。练习三用LinkedHashMap实现一个LRU缓存。核心步骤就是7里提到的那样重写removeEldestEntry方法。做完之后可以写一个小测试put几个键值对然后get其中一个观察LinkedHashMap的迭代顺序变化。练习四在单线程环境下写一段会触发ConcurrentModificationException的代码再改成用Iterator.remove解决。这个练习能帮你真正理解fail-fast机制。练习五打开HashMap的源码跟着put的流程走一遍。找出resize方法看看里面那行(e.hash oldCap) 0是怎么把链表拆成两半的。这一步看懂了HashMap的扩容原理就通了。这些练习每个都不复杂但远比刷十道面试题有用。尤其是练习五源码啃下来之后你再面对HashMap相关的问题会非常从容。8. 踩坑记录与日常开发的心得体会最后整理几条我这些年实际工作中踩过的坑都是吃过亏才记住的教训分享给大家有个参考。第一别用List的contains做去重。数据量小的时候没有感知但数据量一上来List.contains是O(n)的双层循环去重就是O(n²)几万条数据就可能卡到秒级。去重用HashSetO(1)的判断差别是数量级的。第二Map的getOrDefault不等于判空。map.getOrDefault(key, defaultValue)在key不存在时返回默认值这个没问题。但如果key存在但value本身是null它同样会返回默认值。如果你需要区分key不存在和value为null这两种情况要用containsKey来做判断。第三不要边遍历边往集合里添加元素。不管是for-each还是迭代器遍历过程中新增元素都会导致modCount变化触发ConcurrentModificationException。需要边遍历边添加的场景可以考虑用一个临时集合收集遍历完统一addAll。第四集合做参数传递时警惕意外修改。当你把List或Map传给一个方法时方法内部如果改了集合内容调用方那边也会变。这就是引用的天然特性。如果不希望被修改传入之前用Collections.unmodifiableList(list)或List.copyOf(list)包一层。第五jdk版本变了集合行为也会变。比如JDK 8里HashMap引入红黑树优化链表超过8转树JDK 9引入List.of、Map.of等不可变集合工厂方法JDK 10引入List.copyOfJDK 16里Stream.toList()可以一行替代collect。所以复习的时候建议用你项目实际使用的JDK版本去看源码不要拿着JDK 8的结论套JDK 17的行为。集合这块知识说实话不难难的是真正理解。我见过太多人背了一堆面试题问ArrayList扩容机制答得头头是道但让他写一个用迭代器删除元素的代码就卡住了。理解永远比记忆重要。这篇文章我尽量把集合相关的原理、细节、实战经验都揉在一起讲了希望大家看完之后不是会背了而是真的懂了。以后遇到集合相关的面试题或者线上问题能有一种这题我有把握的底气那就说明复习到位了。
RELATED

相关推荐

嵌入式Linux中无符号整数溢出与类型转换陷阱

嵌入式Linux中无符号整数溢出与类型转换陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/9 11:51:44
BPM任务交互层深度拆解:驰骋四大菜单与处理器 vs Flowable/Camunda/Activiti

BPM任务交互层深度拆解:驰骋四大菜单与处理器 vs Flowable/Camunda/Activiti

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/9 11:51:44
ECC纠错码与椭圆曲线密码:硬件级内存保护与TypeScript/Python密码实践

ECC纠错码与椭圆曲线密码:硬件级内存保护与TypeScript/Python密码实践

1. ECC不是缩写游戏,而是工程里最沉默的守门人 ECC这个词最近在开发者圈子里反复刷屏,但很多人点开搜索结果后反而更迷糊了——有人在问“SAP ECC年结怎么搞”,有人贴出 uncorr. ECC 显示2 的报错截图,还有人用 npx ecc-univer…

📅 2026/9/9 11:51:44
MORE NEWS

更多资讯

📰

单斗挖掘机工作装置设计全流程:从参数计算到SolidWorks建模

单斗挖掘机这个题目,基本是机械专业毕业设计里最经典的重头戏之一了。你翻任何一届毕业设计选题库,它都在。为什么?因为这一个小题目,能把机械设计、液压传动、结构力学、三维建模、工程制图这些核心课程全部串起来。很多同学拿到…

📰

xvidcore 1.3.3源码深度解析:从DCT到运动估计的编码器核心实现

简介:xvidcore-1.3.3源代码包是MPEG-4 Part 2 ASP视频编码的核心开源库,也是众多播放器、转码工具与视频处理软件的重要底层组件,面向音视频编解码开发者、多媒体技术研究者以及嵌入式/桌面软件工程师,可用于学习视频编码原理、定…

📰

Hive与TimescaleDB整合:构建车联网时序数据平台实践

1. 整体设计与思路拆解先说结论:Hive和TimescaleDB这套组合,解决的并不是“数据能不能存下来”的问题,而是“存下来之后怎么让业务查得动、查得快”的问题。我做车联网数据平台时,设备每秒上报大量GPS、告警、里程、电压数据&…

📰

ECC纠错码原理与实战:从内存到SSD的硬件级数据保护

1. ECC到底是什么?别被缩写吓住,它其实天天在你手机里跑ECC这个词最近在开发者圈子里突然火了,但很多人一看到就懵——是加密算法?是SAP系统里的年结模块?还是TypeScript报错里那个让人头皮发麻的“uncorr. ecc 显示2”…

📰

Android拨号器源码定制实战:从AOSP架构到系统签名部署

简介:这是一份基于Java开发的Android拨号器工程,对应Google在Android N(7.0)上推出的拨号器应用,面向应用开发者、系统定制工程师以及研究移动通讯交互的读者。工程整合了appcompat、recycleview、cardview、design、s…

📰

Linux下USB转串口设备找不到?从驱动到权限的完整排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬