Java集合框架深度解析:从数据结构到并发编程的40道核心面试题
1. 集合框架Java开发的基石与面试必争之地如果你是一名Java开发者无论你是刚入行的新人还是准备跳槽的资深工程师在面试前你的复习清单里一定少不了“Java集合”这一项。这几乎成了一个定律。为什么因为集合框架是Java语言中应用最广泛、最核心的API之一它直接关系到你写出的代码是否高效、是否健壮、是否优雅。面试官通过几道集合相关的题目就能快速摸清你对数据结构的理解深度、对API的熟悉程度以及在实际编码中处理数据的能力。我见过太多候选人因为一个ConcurrentModificationException没答上来或者对HashMap的扩容机制支支吾吾而与心仪的Offer失之交臂。今天我们不玩虚的直接上干货。我将结合自己十多年面试别人和被面试的经验为你梳理出40道真正高频、有深度的Java集合面试题并附上经过实战检验的答案和背后的原理剖析。这不仅仅是“背答案”更是帮你建立起关于Java集合的完整知识体系让你在面试中不仅能答对更能讲透展现出你的技术底蕴。无论你是想巩固基础还是冲刺大厂这份清单都值得你花时间仔细琢磨。2. 基础与核心接口理解集合的设计哲学2.1 Collection与Map两大阵营的根本区别这是所有集合问题的起点必须烂熟于心。Java集合框架主要分为两大接口家族Collection和Map。它们的根本区别在于存储元素的方式。Collection接口存储一组独立的元素称为“单列”。它有三个主要的子接口List有序、可重复的集合。你可以精确控制每个元素的插入位置也可以通过整数索引访问元素。想象成一张有编号的购物清单。Set无序、不可重复的集合。它最常用于去重和数学上的集合运算。核心在于保证元素的唯一性equals()和hashCode()方法是关键。Queue 队列一种特殊的线性表遵循先进先出FIFO等特定规则。常用于任务调度、缓冲等场景。Map接口存储的是键值对Key-Value映射称为“双列”。每个键Key映射到一个值Value。键是唯一的值可以重复。你可以把Map想象成一个字典或电话簿通过名字Key来查找对应的电话号码Value。注意很多初学者会混淆Collection和Collections。Collection是一个接口是集合的根接口之一而Collections是一个工具类里面全是静态方法用于操作或返回集合比如排序sort()、打乱shuffle()、获取线程安全包装synchronizedCollection()等。2.2 Iterable与Iterator遍历集合的统一方式为什么Collection接口会继承Iterable接口这体现了Java集合框架一个重要的设计思想提供一种统一、安全的方式来遍历各种不同类型的集合。Iterable接口只定义了一个方法IteratorT iterator()。它返回一个实现了Iterator接口的对象。Iterator接口则定义了遍历集合的标准方法hasNext()是否还有下一个元素、next()获取下一个元素和remove()移除当前元素。使用Iterator遍历的优势通用性 所有Collection的实现类都可以用同一种方式遍历代码通用性强。安全性 在遍历过程中如果直接使用集合的remove方法删除元素非Iterator自己的remove方法很可能会抛出ConcurrentModificationException。而Iterator.remove()是唯一安全地在遍历中删除元素的方法。灵活性 它为“增强for循环”for-each提供了支持。实际上Java的for-each循环底层就是通过调用iterator()方法来实现的。示例与陷阱ListString list new ArrayList(Arrays.asList(A, B, C)); // 错误做法在for-each循环中直接调用list.remove() for (String s : list) { if (B.equals(s)) { list.remove(s); // 运行时抛出ConcurrentModificationException! } } // 正确做法1使用Iterator的remove方法 IteratorString it list.iterator(); while (it.hasNext()) { if (B.equals(it.next())) { it.remove(); // 安全删除 } } // 正确做法2使用Java 8的removeIf方法推荐 list.removeIf(s - B.equals(s));2.3 快速失败Fail-Fast与安全失败Fail-Safe机制这是面试中关于并发修改异常的高频考点理解其原理至关重要。快速失败Fail-Fast代表类ArrayListHashMap非并发包下的。原理 集合内部维护一个“修改计数器”modCount。当创建迭代器时迭代器会记录下当前集合的modCount值。在每次调用迭代器的next()或remove()方法时都会检查当前集合的modCount是否与迭代器记录的期望值相等。如果不相等说明集合在迭代期间被其他线程或当前线程非迭代器方式修改了就会立即抛出ConcurrentModificationException。目的 这是一种错误检测机制旨在尽早发现并发修改的bug避免程序在不确定的状态下继续运行。它并不能解决并发问题只是快速暴露问题。安全失败Fail-Safe代表类CopyOnWriteArrayListConcurrentHashMap。原理 采用“写时复制”或“快照”思想。例如CopyOnWriteArrayList在遍历时是基于创建迭代器那一刻的底层数组副本进行的。后续的写操作增、删、改会作用在一个新的数组副本上不会影响正在进行的遍历。因此不会抛出ConcurrentModificationException。特点 避免了并发修改异常但迭代器看到的是某一时刻的旧数据视图可能无法反映遍历开始后最新的修改。这是一种弱一致性的保证。选择策略在单线程环境下注意不要在for-each循环中直接修改集合即可避免Fail-Fast异常。在多线程环境下读写同一集合必须使用java.util.concurrent包下的并发集合类如ConcurrentHashMap,CopyOnWriteArrayList它们提供了线程安全和Fail-Safe或更高级别的并发保证。3. List家族深度解析ArrayList、LinkedList与Vector3.1 ArrayList底层实现与扩容机制ArrayList是使用最频繁的List实现其本质是一个动态增长的数组。核心字段Object[] elementData 存储元素的底层数组。int size 记录当前列表中实际元素的数量。添加元素与扩容流程当调用add(E e)方法时首先检查当前数组容量是否足够size 1 elementData.length。如果不足则会触发扩容。默认的扩容机制是创建一个新的数组新数组的容量是旧数组的1.5倍即int newCapacity oldCapacity (oldCapacity 1)。将旧数组中的所有元素复制到新数组中。将新元素添加到新数组的末尾并更新size。源码级别的细节初始容量 无参构造时默认是一个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA在第一次添加元素时才会扩容到默认容量10。指定容量的构造器new ArrayList(100)。如果你能预估数据量强烈建议在创建时指定初始容量。这可以避免多次扩容带来的数组拷贝开销是提升性能的有效手段。Arrays.copyOf 扩容时实际使用的方法内部调用了System.arraycopy这个原生方法效率较高。性能特点查询快O(1) 基于数组索引直接定位速度极快。增删慢O(n) 在中间位置插入或删除元素需要移动该位置后的所有元素。尾部插入不触发扩容时是O(1)。3.2 LinkedList真的是“增删快查询慢”吗LinkedList是一个双向链表。每个节点Node包含三个部分前驱引用prev、元素本身item和后继引用next。“增删快”的真相前提是已知节点位置。如果已经持有某个节点的引用在其前后插入或删除该节点确实只需要修改几个引用时间复杂度是O(1)非常快。但通常我们是通过索引或值来操作。例如list.add(index, element)LinkedList需要先遍历找到第index个节点这个遍历操作的时间复杂度是O(n)。找到之后插入本身是O(1)。所以整体仍然是O(n)。对比ArrayList ArrayList的插入也需要O(n)来移动元素。但在数据量较大且插入位置靠前时LinkedList的遍历开销可能比ArrayList的内存块移动开销更大因为CPU缓存不友好。所以“LinkedList增删一定比ArrayList快”是一个误区。只有在频繁在头部进行插入删除addFirst/removeFirst O(1)或者已持有ListIterator进行迭代插入时其优势才明显。“查询慢”的真相通过索引get(int index)访问需要从头或尾开始遍历内部会优化判断index离哪端更近时间复杂度O(n)。而ArrayList的get是O(1)。使用场景需要实现栈、队列或双端队列Deque时LinkedList实现了Deque接口方法丰富。需要频繁在列表两端进行添加/删除操作。内存碎片化敏感的场景链表不需要连续内存空间。3.3 Vector与Stack为什么说它们过时了Vector是一个古老的、线程安全的动态数组实现。它的所有公共方法都使用了synchronized关键字修饰保证了同一时刻只有一个线程能执行这些方法。为什么过时了性能开销 粗粒度的synchronized锁导致即使在单线程环境下也有不必要的性能损耗。设计陈旧 它的扩容机制默认是翻倍2倍而ArrayList是1.5倍。方法命名也不如新的集合框架统一如addElementvsadd。有更好的替代品需要线程安全的列表用CopyOnWriteArrayList读多写少或Collections.synchronizedList(new ArrayList())更灵活。需要同步的Map用ConcurrentHashMap。需要栈Deque接口的实现类如ArrayDeque提供了更完整、性能更好的栈操作push,pop,peek应完全替代古老的Stack类。结论 在新代码中没有任何理由再使用Vector和Stack。了解它们只是为了应付历史遗留代码或面试。4. Set家族与唯一性奥秘HashSet、LinkedHashSet、TreeSet4.1 HashSet如何保证元素不重复这是Set最核心的问题。HashSet的内部实现其实就是封装了一个HashMap。存储结构HashSet的底层使用HashMap来存储元素。当你向HashSet添加一个元素e时实际执行的是map.put(e, PRESENT)。这里的PRESENT是一个静态的、无意义的Object对象充当HashMap键值对中的“值”。去重关键 既然用了HashMap那么去重的逻辑就完全依赖于HashMap的Key的唯一性。而HashMap判断Key是否重复依赖于两个方法hashCode()和equals()。添加新元素时 首先调用该元素的hashCode()方法计算哈希值定位到数组桶中的位置。如果该位置为空直接放入添加成功。如果该位置不为空哈希冲突则调用equals()方法依次与该位置链表或树上的每一个元素进行比较。如果equals()返回true则认为元素已存在放弃添加对于HashMap是覆盖值对于HashSet因为值固定所以无效果。如果遍历完所有冲突元素equals()都返回false则将新元素添加到链表或树的末尾。因此要正确地将自定义对象存入HashSet或作为HashMap的Key必须同时重写hashCode()和equals()方法并且遵守它们的契约equals()相等的两个对象hashCode()必须相等。hashCode()相等的两个对象equals()不一定相等哈希冲突。4.2 LinkedHashSet与TreeSet的特性和排序LinkedHashSet它是HashSet的子类。内部通过LinkedHashMap实现。特性在HashSet保证元素唯一性的基础上额外维护了一个贯穿所有元素的双向链表。这个链表记录了元素的插入顺序。效果 当你迭代LinkedHashSet时元素的顺序就是它们最初被插入的顺序。这对于需要保持插入顺序且去重的场景非常有用比如缓存最近访问的记录。TreeSet它是基于TreeMap红黑树实现的。特性元素是有序的。默认按照元素的自然顺序Comparable接口进行排序或者通过在构造时传入一个Comparator比较器来定制排序规则。性能 添加、删除、查找的时间复杂度都是O(log n)因为红黑树是一种自平衡的二叉搜索树。使用场景 需要元素自动排序且唯一的集合。注意存入TreeSet的元素必须实现Comparable接口或者在构造TreeSet时提供Comparator否则在添加元素时会抛出ClassCastException。三者的选择特性HashSetLinkedHashSetTreeSet底层结构HashMapLinkedHashMapTreeMap (红黑树)排序保证无插入顺序自然顺序/定制顺序性能(O(1) avg)最优接近HashSet略慢O(log n)唯一性依据hashCode() equals()hashCode() equals()compareTo()/compare() (返回0视为重复)使用场景通用去重不关心顺序去重且需保持插入顺序去重且需自动排序实操心得 对于自定义对象如果同时要放入HashSet和TreeSet必须小心。HashSet依赖hashCode/equalsTreeSet依赖compareTo/compare。务必保证两种逻辑的一致性即compareTo返回0的对象其equals方法应该返回true。否则会导致集合行为混乱。5. Map家族王者HashMap源码级深度剖析HashMap是面试中集合部分的重中之重几乎必问。必须从源码层面理解其设计。5.1 底层数据结构演进数组链表红黑树JDK 1.8之前HashMap采用“数组链表”的形式。哈希冲突的元素会挂在数组对应位置的链表上。当链表过长时查询效率会退化为O(n)。JDK 1.8进行了重大优化引入了红黑树数据结构数组NodeK,V[] table 链表 红黑树。转换阈值 当同一个桶数组位置中的链表长度超过8TREEIFY_THRESHOLD并且当前HashMap的容量数组长度大于等于64MIN_TREEIFY_CAPACITY时该链表会转换为红黑树TreeNode。退化阈值 当扩容resize或删除元素导致红黑树的节点数小于等于6UNTREEIFY_THRESHOLD时红黑树会退化为链表。为什么是8和6这是基于统计学上的泊松分布。在理想的随机哈希下链表长度达到8的概率极低小于千万分之一。将阈值设为8可以保证在绝大多数情况下链表都不会转成树从而避免红黑树复杂的结构带来的额外开销。而退化阈值设为6有一个7的缓冲区间是为了避免频繁的树化和退化抖动。5.2 核心参数、哈希计算与索引定位核心参数capacity 数组的容量默认16必须是2的幂。loadFactor 负载因子默认0.75。它决定了HashMap在多少满的时候进行扩容。threshold 扩容阈值计算公式为capacity * loadFactor。当size元素总数超过threshold时触发扩容。size 当前HashMap中键值对的总数。哈希计算与索引定位计算哈希值 首先调用Key的hashCode()方法得到一个32位的int值h。扰动函数 为了减少哈希碰撞JDK对原始哈希码进行了一次扰动计算(h key.hashCode()) ^ (h 16)。这一步将高16位与低16位进行异或目的是让高位的信息也参与到后续的索引计算中使得哈希分布更加均匀。计算数组索引 通过(n - 1) hash来计算元素应该放在数组的哪个位置。这里n是数组长度2的幂。n-1的二进制形式是低位全1例如15的二进制是1111。操作相当于一个取模运算hash % n但效率更高。这也解释了为什么容量必须是2的幂只有2的幂减一才能得到低位全1的掩码才能均匀地映射哈希值。5.3 put方法全流程与扩容机制resizeput(K key, V value)方法是HashMap的灵魂。其核心步骤如下初始化 如果数组table为空或长度为0则调用resize()方法进行初始化分配默认容量16的数组。计算索引 根据Key的哈希值计算数组下标i。插入桶如果table[i]为空直接新建一个Node放入。如果table[i]不为空哈希冲突 a. 检查第一个节点如果该节点的hash和key都与新元素相同或equals则认为是同一个Key执行值覆盖。 b. 如果第一个节点是树节点TreeNode则调用红黑树的插入方法。 c. 否则遍历链表。如果找到相同的Key则覆盖值如果没找到则将新节点插入链表尾部JDK1.7是头插法1.8改为尾插法避免了环形链表问题。结构转换检查 插入链表后如果链表长度达到8则调用treeifyBin()方法。在该方法中会检查当前数组容量是否达到64如果达到则将链表转为红黑树如果未达到则只进行扩容。扩容判断 插入成功后size加1。如果size threshold则调用resize()方法进行扩容。扩容机制resize 扩容是HashMap性能的关键点之一目的是减少哈希冲突维持O(1)的查询效率。创建新数组 新数组的容量是旧数组的2倍newCap oldCap 1新的扩容阈值也变为原来的2倍。重新哈希Rehash 遍历旧数组的每一个桶非空位置。如果桶里只有一个节点直接根据新容量计算新索引e.hash (newCap - 1)放入新数组。JDK 1.8的优化 对于链表或树不需要像1.7那样对每个节点重新计算哈希。由于新容量是旧容量的2倍旧索引的计算公式是hash (oldCap-1)。观察发现元素在新数组中的位置要么是原索引要么是原索引旧容量。具体取决于该节点的哈希值在“旧容量”对应二进制位上的值是0还是1。这大大提升了扩容时数据迁移的效率。如果是红黑树会调用相关方法进行树的拆分和迁移。5.4 线程安全问题与ConcurrentHashMap的引入HashMap是非线程安全的。在多线程环境下同时进行put操作可能引发多种问题数据覆盖 两个线程同时计算到同一个空桶位置先后写入后写入的会覆盖先写入的导致数据丢失。死循环JDK 1.7 在扩容resize进行链表转移时采用头插法在多线程环境下可能导致链表形成环后续get操作进入死循环。JDK 1.8改为尾插法从根源上避免了这个问题但依然不是线程安全的。size不准确 多个线程同时修改size可能导致最终大小与实际不符。解决方案使用Hashtable 不推荐全表锁性能差。使用Collections.synchronizedMap(new HashMap()) 得到一个同步包装器所有方法用同一把锁适合竞争不激烈的场景。使用ConcurrentHashMap这是官方推荐的高并发场景下的Map实现。它通过分段锁JDK 1.7或CASsynchronizedJDK 1.8实现了更细粒度的锁控制保证了线程安全的同时提供了更高的并发性能。其get操作通常完全无锁效率极高。6. 并发集合精讲ConcurrentHashMap与CopyOnWriteArrayList6.1 ConcurrentHashMap在JDK 1.7和1.8中的实现差异JDK 1.7的实现分段锁Segment数据结构 一个ConcurrentHashMap由一组Segment段组成每个Segment本质上是一个小的HashMap。锁机制 每个Segment继承自ReentrantLock独立加锁。当操作发生在不同的Segment上时可以完全并行。默认有16个段所以理论上支持16个线程并发写。缺点 锁的粒度虽然比Hashtable细但依然是段级别的。对于同一个段内的操作仍需竞争锁。并且查询时需要遍历两次先找段再找段内的元素。JDK 1.8的实现CAS synchronized数据结构 放弃了Segment采用了与HashMap1.8类似的“数组链表红黑树”结构。锁机制锁的粒度细化到了每个数组元素的头节点桶。put操作 如果目标桶为空使用CASCompare-And-Swap无锁操作尝试插入新节点失败则重试或升级为锁。如果目标桶不为空则使用synchronized关键字锁住这个桶的头节点然后在链表或红黑树上进行插入操作。优势锁粒度更细 并发度最高可达数组长度远高于1.7的16。查询操作完全无锁get、size、isEmpty等读操作通常不需要加锁直接访问volatile变量性能极高。数据结构更先进 直接复用HashMap的红黑树优化查询效率更高。总结 JDK 1.8的ConcurrentHashMap在并发性能、内存占用和代码复杂度上都有显著优化是目前绝对的主流。6.2 CopyOnWriteArrayList的适用场景与读写规则CopyOnWriteArrayList是List接口的一个线程安全实现其核心思想是“写时复制”。工作原理读操作 所有读操作getiterator都不加锁直接访问当前内部的数组引用。写操作 任何会修改集合内容的操作addsetremove都会先获取一把独占锁ReentrantLock然后将当前内部数组完整地复制一份在新的副本数组上进行修改。修改完成后再用新的数组替换掉旧的数组引用。最后释放锁。特点与适用场景优点读性能极高完全无锁且不会抛出ConcurrentModificationException。适合读多写少的场景比如监听器列表、配置信息的缓存。缺点内存占用大 每次写操作都会复制整个底层数组如果数组很大会带来巨大的内存开销和GC压力。数据一致性弱 读操作读到的是写操作开始前的旧数组快照无法感知到写操作完成后最新的数据。这是一种最终一致性。写性能差 复制数组的成本高不适合频繁写的场景。使用注意事项迭代器Iterator持有的是创建它那一刻的数组快照。在迭代过程中即使其他线程修改了集合迭代器也不会受到影响也不会抛出异常但迭代不到最新的修改。因为每次写都复制所以不要用于元素数量巨大且频繁修改的场景。与synchronizedList对比Collections.synchronizedList(new ArrayList()) 读写操作都加锁同一把锁保证了强一致性但在高并发读时性能有瓶颈。CopyOnWriteArrayList 读写分离读无锁写加锁并复制保证了读的高性能和弱一致性。选择哪一个取决于你的业务场景对一致性和性能的权衡。7. 工具类与最佳实践Collections和Arrays7.1 Collections类中的高频工具方法Collections是一个包含众多静态方法的工具类犹如集合的“瑞士军刀”。排序与查找sort(ListT list): 对列表进行自然排序元素需实现Comparable。sort(ListT list, Comparator? super T c): 使用自定义比较器排序。binarySearch(List? extends Comparable? super T list, T key): 对已排序的列表进行二分查找效率O(log n)。如果列表未排序结果未定义。同步包装与不可变包装synchronizedXxx(Collection c): 如synchronizedList,synchronizedSet,synchronizedMap。返回一个线程安全的集合包装器。注意 在迭代返回的集合时必须手动在迭代代码块上加锁否则可能抛出并发修改异常。List syncList Collections.synchronizedList(new ArrayList()); // 必须这样迭代 synchronized (syncList) { Iterator i syncList.iterator(); while (i.hasNext()) { // ... } }unmodifiableXxx(Collection c): 如unmodifiableList返回一个不可修改的集合视图。任何试图修改该集合的操作都会抛出UnsupportedOperationException。用于防御性编程保护内部数据不被意外修改。其他实用方法reverse(List? list): 反转列表。shuffle(List? list): 随机打乱列表洗牌。frequency(Collection? c, Object o): 返回指定元素在集合中出现的次数。disjoint(Collection? c1, Collection? c2): 如果两个集合没有交集返回true。addAll(Collection? super T c, T... elements): 一次性添加多个元素比循环调用add更清晰。7.2 集合选型与性能考量实战指南在实际开发中选择正确的集合类对性能至关重要。以下是一些核心准则单列还是双列根据数据模型决定用Collection还是Map。是否需要排序需要自动排序TreeSet,TreeMap。需要保持插入顺序LinkedHashSet,LinkedHashMap。不需要顺序HashSet,HashMap。是否需要线程安全不需要 优先使用非同步集合性能最好。需要且读远多于写 考虑CopyOnWriteArrayList。需要高并发读写Map首选ConcurrentHashMap。需要竞争不激烈或需要强一致性 使用Collections.synchronizedXxx()包装。List的选择ArrayList vs LinkedList绝大多数情况用ArrayList。CPU缓存友好随机访问O(1)。即使中间插入删除对于现代CPU顺序内存拷贝的速度也往往快于链表的遍历。只有当你需要频繁在列表两端进行添加/删除操作实现队列/双端队列或者作为Deque使用时才考虑LinkedList。Map的选择HashMap vs LinkedHashMap vs TreeMap绝大多数情况用HashMap。O(1)的平均性能。需要按插入顺序或访问顺序迭代LinkedHashMap可通过构造参数设置按访问顺序排序实现LRU缓存。需要Key按自然顺序或定制顺序排序TreeMap。初始化容量 如果能预估集合最终的大小务必在构造时指定初始容量如new ArrayList(1000)new HashMap(256)。这可以避免或减少扩容带来的性能损耗对ArrayList和HashMap尤其重要。遍历方式对ArrayList使用索引的for循环最快。对LinkedList绝对不要使用索引的for循环性能灾难O(n²)必须使用迭代器或for-each循环。通用且安全的方式是使用迭代器Iterator或for-each循环。性能误区提醒LinkedList不一定比ArrayList增删快已在前文详述。Vector和Hashtable已过时不要用。synchronizedXxx包装器在迭代时需要手动同步容易出错高并发下性能不如并发集合。8. 40道经典面试题速查与精解以下是我精选的40道题目覆盖了从基础到高级的各个层面。建议你先自己思考再看后面的精要解析。一、基础概念 (1-5)Java集合框架有哪些主要接口画出大致继承/实现关系图。Collection和Collections有什么区别List、Set、Map三者的区别如何决定使用HashMap还是TreeMapArrayList和LinkedList的区别是什么二、ArrayList LinkedList (6-10)6.ArrayList的底层实现是什么默认初始容量是多少扩容机制是怎样的 7. 在ArrayList中间插入元素一定比LinkedList慢吗为什么 8.ArrayList的subList方法返回的列表有什么特点 9. 如何实现ArrayList的线程安全 10.Vector和ArrayList的区别为什么说Vector过时了三、HashMap 深度篇 (11-20)11.HashMap的底层数据结构是什么JDK1.8 12.HashMap的put方法具体流程是怎样的 13.HashMap的扩容resize过程是怎样的1.8做了哪些优化 14.HashMap的负载因子是什么为什么默认是0.75 15.HashMap的长度为什么是2的幂次方 16.HashMap和Hashtable的区别 17.HashMap是线程安全的吗如何实现线程安全 18.HashMap1.7和1.8在解决哈希冲突上有何不同头插法 vs 尾插法 19.HashMap中如果两个Key的hashCode相同如何存储 20. 为什么重写equals方法时必须重写hashCode方法跟HashMap有什么关系四、Set 与唯一性 (21-25)21.HashSet是如何保证元素不重复的 22.HashSet、LinkedHashSet和TreeSet有何区别 23. 如何让一个自定义对象可以作为HashSet的元素或HashMap的Key 24.TreeSet排序的自然排序和定制排序如何实现 25.Set和List在contains方法性能上有何差异五、并发集合 (26-30)26.ConcurrentHashMap在JDK1.7和1.8中实现有何不同 27.ConcurrentHashMap的size方法是如何实现的 28.CopyOnWriteArrayList适用于什么场景其读写规则是怎样的 29. 什么是快速失败Fail-Fast和安全失败Fail-Safe举例说明。 30. 如何边遍历ArrayList边安全地删除元素六、工具类与比较器 (31-35)31.Collections.sort()方法内部使用什么排序算法TimSort 32.Comparable和Comparator接口的区别 33.Arrays.asList()得到的List有什么限制固定大小不支持增删 34. 如何优雅地将一个数组转换为ArrayList 35.Collections类中有哪些方法可以创建不可变集合七、迭代与遍历 (36-38)36.Iterator和ListIterator有什么区别 37. 增强for循环的底层原理是什么 38. 遍历Map有哪几种方式推荐哪种八、综合与陷阱 (39-40)39. 下面代码的输出是什么为什么java MapString, String map new HashMap(); map.put(new String(key), value1); map.put(new String(key), value2); System.out.println(map.size());40. 在多线程环境下对同一个HashMap进行 put 操作可能引起什么问题死循环、数据丢失等精解提要部分难题第7题 不一定。ArrayList中间插入需要移动后面所有元素内存块拷贝LinkedList需要遍历找到位置指针跳转。当数据量较小或插入位置靠后时ArrayList可能更快。需要具体分析。第13题 JDK1.8扩容优化在于链表元素在新数组中的新位置要么是原索引要么是原索引旧容量无需重新计算hash提升了rehash效率。第14题 负载因子0.75是空间和时间成本的折衷。过高如1.0减少空间开销但增加哈希冲突降低查询效率过低如0.5提高查询效率但增加扩容频率浪费空间。第19题hashCode相同即哈希冲突。它们会存储在同一个桶数组位置中形成链表或红黑树。HashMap会用equals方法来区分它们是否是同一个Key。第27题 JDK1.8的ConcurrentHashMap的size()是一个近似值它通过累加每个段或CounterCell的计数来实现为了性能没有全局加锁。mappingCount()方法更推荐。第33题Arrays.asList()返回的是Arrays内部类ArrayList它包装了原始数组是固定大小的。调用add()或remove()会抛UnsupportedOperationException。第39题 输出是1。因为String类重写了equals和hashCode两个内容为key的String对象被认为是相同的Key所以第二次put会覆盖第一次的value。掌握这40个问题你对Java集合的理解将达到一个非常扎实的程度。但记住面试不仅是背诵更是理解背后的原理和设计思想并能用清晰的语言表达出来。最好的学习方式是打开JDK源码结合本文的讲解自己去跟踪几个关键方法的执行流程印象会深刻得多。