ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java集合框架面试全解:从ArrayList到HashMap底层原理与并发选型

Java集合框架面试全解:从ArrayList到HashMap底层原理与并发选型 1. 集合框架整体梳理与记忆主线1.1 集合框架的分层结构与核心接口很多同学准备Java面试的时候一提到集合就开始背源码、背结论比如HashMap初始容量16负载因子0.75链表转红黑树阈值8背得滚瓜烂熟结果面试官问一句为什么是8而不是9就卡住了。你如果能把集合框架当成一棵树来理解理清接口、实现类、数据结构之间的递进关系这些问题其实是可以推导出来的而不是靠死记硬背。Java集合框架最顶层的两个体系一个是Collection接口体系存放单元素一个是Map接口体系存放键值对。Collection下面又分三大分支List、Set、Queue。List是允许重复、有序的集合Set是不允许重复的集合Queue是队列语义的集合。Map体系独立于Collection但Map的实现类又和Set有千丝万缕的联系比如HashSet底层就是HashMapTreeSet底层就是TreeMap。我在实际面试中发现面试官很少直接问List和Set的区别而是喜欢把一个大的知识网络拆成连环追问。比如你先回答了HashSet基于HashMap实现那下一步就问为什么HashSet的value是同一个Object再下一步就问如果两个对象的hashCode相同会怎样。所以你准备集合面试的时候不要按着章节一节一节背一定要建立一张完整的知识拓扑每一个结论都要能向上推导到接口设计、向下追溯到源码实现。1.2 为什么面试官爱考集合以及怎么高效准备集合框架为什么是Java面试的绝对重点因为它同时考察了几个层面的能力第一你对JDK基础类库的熟悉程度这是Java开发者的基本功第二你对数据结构与算法的理解比如数组、链表、红黑树、哈希表都是笔试和面试手撕代码的高频考点第三你在实际业务中做技术选型的能力比如什么时候用ArrayList什么时候用LinkedList高并发场景下用HashMap会不会出事这直接反映出你有没有线上实战经验。企业招聘Java开发最怕招到那种只会写CRUD、遇到性能问题就懵的候选人。集合恰恰是把业务代码和底层原理连接得最紧密的一块知识。你写任何业务代码几乎都离不开集合接口数据要转List、账务明细要放进Map做聚合、去重用Set、队列用Queue。面试官通过集合这一个点就能判断出你的基础是否扎实、有没有深入研究过JDK源码、能不能应对线上OOM或死循环这样的突发问题。我在带新人时经常说一句话不要单独背集合面试题要把集合框架和JVM内存、并发编程、设计模式联系起来学。比如ArrayList扩容涉及数组拷贝和内存分配HashMap的并发问题涉及线程安全Collections工具类里的各种包装方法涉及装饰器模式。这样横向串联起来你面试时候的回答深度会明显不一样因为你能从多个维度去解释一个技术决策而不是只说出一个标准答案。2. List核心考点底层实现与扩容机制2.1 ArrayList动态扩容背后的设计思想ArrayList是Java面试中最亲民的集合类因为大家平时写代码用得太多了但面试考起来一点不简单。ArrayList本质上是一个动态数组它默认的初始容量是10每次扩容的时候会创建新数组然后把旧数组里的元素用System.arraycopy拷贝过去。关键考点在于扩容的倍率——JDK 8里面是oldCapacity (oldCapacity 1)也就是原来的1.5倍。你有没有想过为什么扩容是1.5倍而不是2倍或者1.2倍这涉及到扩容策略的时间复杂度和空间利用率的权衡。如果扩容倍数太大比如2倍那扩容一次能撑很久但空间浪费很严重可能出现明明只放了几百个元素却占了上千个容量的情况如果扩容倍数太小比如1.1倍那扩容频率会非常高频繁的数组拷贝会导致性能下降明显。1.5倍是经过权衡之后一个比较折中的方案——每次扩容新容量是旧容量的1.5倍均摊下来添加元素的平均时间复杂度是O(1)空间上也不会浪费太夸张。面试官还特别爱问一个细节ArrayList的size()和capacity有什么区别。size是当前元素个数capacity是数组的最大容量。很多新手以为list.size()返回的就是数组长度其实不然。当你new ArrayList()的时候底层是一个空数组只有当第一次add元素的时候才会用DEFAULT_CAPACITY(10)去初始化容量。这个设计叫做懒加载目的是避免创建对象时就分配多余内存。提示如果你能预估元素数量一定要用new ArrayList(expectedSize)这样能避免多次扩容带来的数组拷贝损耗。这个习惯在数据量大时性能差异非常明显。ArrayList还有一个容易被问倒的点——subList陷阱。list.subList(0, 5)返回的不是一个新的ArrayList而是原列表的一个视图底层用的是SubList内部类。如果你对subList返回的结果执行add或者remove操作会触发原列表的modCount变化导致原列表在迭代时抛出ConcurrentModificationException。这个坑我在实际项目中见过不止一次很多人把subList的结果当独立列表用结果线上报错查了半天。2.2 LinkedList双向链表的功能边界LinkedList在面试中和ArrayList是打包出现的对比考点。LinkedList底层是双向链表JDK 8里的实现每个节点有三个属性item数据、prev前驱节点、next后继节点。因为每个节点还额外保存了两个指针所以LinkedList比ArrayList更占内存——一个元素除了数据本身还要存储两个引用在64位JVM上开启了压缩指针的情况下每个节点额外开销约16字节。LinkedList的优势在于头尾操作addFirst、addLast、removeFirst、removeLast这些操作的复杂度都是O(1)因为它不需要移动其他元素。但是注意一点LinkedList的get(int index)和add(int index, E element)并不是O(1)——虽然它内部有一个优化会根据index和size/2的比较决定从头遍历还是从尾遍历但整体依然是O(n)。很多人背结论说LinkedList查找慢、插入快这个说法不够严谨插入快指的是在已知节点位置的情况下插入如果你需要先查到某个位置再插入那这个查找的O(n)成本也要算进去。我面过一些候选人一听到LinkedList是链表实现的就赶紧说那它插入快。面试官马上追问那我现在要在第1000个位置插入一个元素链表做了什么操作这时候就能看出是真懂还是背结论了。还有一个隐藏考点LinkedList实现了Deque接口所以它可以直接当作栈或者队列来用。当你写LinkedList的时候官方是建议使用ArrayDeque来实现栈和队列因为ArrayDeque基于循环数组平均性能更高内存更紧凑。不过话说回来LinkedList的pop、push、offer、poll这些方法在日常刷题时用起来确实方便而且作为一面手撕算法的工具它完全够用。2.3 面试必问的ArrayList vs LinkedList对比面试官非常喜欢让候选人用表格或口头陈述来对比ArrayList和LinkedList我建议你在准备时记住以下核心差异而不是背一个数组一个链表这种空话对比维度ArrayListLinkedList底层结构动态数组双向链表随机访问O(1)按索引直接定位O(n)需要遍历尾部插入均摊O(1)可能触发扩容O(1)指定位置插入O(n)涉及元素移位O(n)需要先定位定位后插入O(1)内存占用相对紧凑有预分配冗余每个元素额外存储两个指针应用场景读多写少、随机访问频繁头尾操作频繁、无需随机访问我在实际开发中总结出来一个很朴素的经验大部分业务场景下选ArrayList就行LinkedList的优势场景其实非常少。原因很简单现代CPU对连续内存的访问是有缓存友好的特性——ArrayList底层是连续数组遍历时CPU缓存命中率很高而LinkedList每个节点在内存中散落分布每次访问都可能发生缓存缺失实际性能要打折扣。再加上LinkedList的节点对象更多、GC压力更大所以很多人说LinkedList在某些情况下比ArrayList慢得多并不是错觉。另外还有一个容易被忽略的点LinkedList没有实现RandomAccess接口而ArrayList实现了。这个接口是一个标记接口它影响到Collections.binarySearch等工具类对集合遍历方式的选择——实现了RandomAccess的集合二分查找会走索引遍历逻辑没实现的则走迭代器遍历逻辑。这也侧面说明有时候判断一个集合能不能高效随机访问不能只看类名还要看它是否实现了这个标记接口。3. HashMap全解数组链表红黑树的组合艺术3.1 HashMap底层数据结构与put流程HashMap是整个Java集合面试的核弹级考点可以说如果HashMap答不好这场面试基本就凉了一半。Java 8版本的HashMap底层结构是数组 链表 红黑树。数组的每个位置叫桶(bucket)当多个键哈希冲突时元素在同一个桶里以链表形式串联当某个桶的链表长度太长时链表会转化为红黑树以提升查询效率。先记住一个足够深度的put流程这个流程我建议你能手写出来而不是靠感觉。当你调用map.put(key, value)时对key.hashCode()做一次扰动计算让高位也参与低位运算降低哈希冲突概率。根据哈希值按位与(n - 1)得到桶下标n是当前数组长度。如果数组为null或者长度为0先触发resize()初始化。如果目标桶位置为null直接new Node放入。如果桶位置不为null说明发生了哈希冲突此时需要判断如果链表头节点是key相同的旧节点直接覆盖value否则遍历链表找到相同key则替换value没有相同key就在链表尾部插入一个新节点Java 8是尾插法。插入完成后判断该桶链表的节点数是否达到树化阈值8并且数组长度达到64满足则转换为红黑树。最后判断当前的size是否超过了扩容阈值容量乘以负载因子超过就resize扩容。视频课程和面经里都会提到这个流程但你有没有想过为什么计算桶下标要用(n - 1) hash而不是hash % n这一标准取模运算原因有两点第一当n是2的幂次方时n-1的二进制低位全是1按位与运算可以完美替代取模而按位与比取模运算快得多第二这样分布更均匀因为取模运算在n不是2的幂的时候高位会丢失冲突概率更大。3.2 扩容机制、负载因子与寻址算法的深层逻辑HashMap的默认容量是16负载因子是0.75。扩容阈值threshold等于capacity乘以loadFactor也就是说当元素个数超过16乘以0.75等于12的时候HashMap会扩容为原来的2倍。这个0.75负载因子是怎么来的官方注释里提到这是时间复杂度和空间复杂度的一个折中。如果负载因子调大比如调成1.0那么空间利用率会提高但哈希冲突概率更高链表更长查询效率下降如果调小比如0.5那么冲突少了但空间浪费太多频繁扩容也带来性能损耗。0.75这个值是JDK源码作者根据大量实验数据算出来的近似地让链表长度服从泊松分布在大多数情况下保证冲突概率极低。扩容的时候有一个非常体现设计功底的细节扩容为原来的2倍后元素在新数组中的位置要么在原来的下标要么在原下标 旧容量这两个位置之一。为什么因为数组长度从16变成32n-1的掩码相当于多了一位1某个元素在新掩码下多出的那一位正好等于它旧hash值中对应那一位的值那一位是0就呆在原位是1就移动到原下标旧容量。Java 8就是根据e.hash oldCap是否等于0来判断元素应该留在原位还是移动到高位这样就不需要重新计算每个元素的hash值了。这个无需rehash的设计不仅高效而且在并发环境下还避免了Java 7头插法扩容导致的死循环问题。Java 7扩容时用头插法转移元素在多线程环境下容易出现环形链表而Java 8改成尾插法后理论上不再有这个死循环问题——但HashMap依然不是线程安全的并发put还是会导致数据覆盖等问题。3.3 树化条件与红黑树相关考点链表转红黑树的条件有两个两者必须同时满足第一某个桶的链表长度大于等于8第二整个数组长度不小于64。如果链表长度到了8但数组长度还不到64这时候不会立即树化而是先进行一次resize扩容让元素分散到更多桶里。这个设计思路很清晰小数组下树化意义不大因为容量太小导致哈希冲突严重与其用红黑树解决冲突不如把数组做大。那为什么树化阈值是8源码注释里给了一个统计学解释在负载因子0.75的情况下链表长度达到8的概率大约是千万分之一也就是说在正常情况下几乎不会出现这么长的链表。如果真出现了说明元素分布的hash函数已经恶化了此时引入红黑树来兜底把最坏情况下的查询复杂度从O(n)降到O(log n)。面试中还常问另一个数字——为什么退化阈值是6而不是7或8这是为了留缓冲避免链表和红黑树频繁地互相转换。如果阈值都是8某个桶的长度在7到8之间反复横跳就会导致频繁的树化、退化性能开销很大。6和8之间隔了2个差值相当于加了一个滞回区间防止抖动。注意红黑树是面试高级岗位时的加分项。你至少要能说清楚红黑树的五个性质——节点非红即黑、根黑、叶子黑、红节点不能连续、任意节点到其叶子节点的路径包含相同数量的黑节点——以及为什么插入和删除后需要旋转和变色来恢复平衡。3.4 为什么HashMap是线程不安全的这是我劝诫过很多次的一个高频考点。HashMap在多线程环境下至少有三个问题一是多线程同时put时可能发生数据覆盖比如两个线程同时判断某个桶为null然后同时new Node放入后写的覆盖了先写的一个元素就丢了二是扩容时多个线程同时对shared数组做rehash可能在Java 8之前版本导致链表循环引用三是size的值是普通int多线程下并发增减并不安全。如果你在面试中说Java 8的HashMap扩容采用了尾插法所以没有死循环问题了面试官大概率会接着问那它线程安全了吗千万不要踩这个坑——没有死循环不等于线程安全数据覆盖问题在Java 8依然存在。严谨的说法是Java 8修复了扩容时死循环的问题但HashMap仍然不是线程安全的并发场景应该使用ConcurrentHashMap。我在实际工作中也见过有人用HashMap做缓存然后上线后偶发出现数据消失的问题排查到最后都是并发put覆盖导致。新手容易误以为我用HashMap并且加个synchronized修饰方法就安全了但其实不加同步的并发读写HashMap脏读、覆盖、无限循环都可能发生风险极大。4. 并发场景下的集合选型线程安全与读写策略4.1 ConcurrentHashMap的演进与实现对比并发场景下首选ConcurrentHashMap。Java 7版本它的实现是分段锁结构内部维护了一个Segment数组每个Segment继承自ReentrantLock多个线程可以同时操作不同的Segment从而把锁竞争分散到16个段上。Java 8抛弃了Segment这种设计改成更细粒度的CAS synchronized——直接用Node数组锁的粒度从段细化为单个桶节点。Java 8的ConcurrentHashMap在put时如果目标桶位为空使用CAS直接写入不需要加锁如果桶位不为空用synchronized锁住该桶的头节点再进行链表或红黑树的插入。这样并发度大大提升——两个线程只要锁的不是同一个桶节点就能真正的并行写入。而且synchronized在JDK 8之后经过锁升级优化偏向锁、轻量级锁、重量级锁性能并不比ReentrantLock差代码也简洁了不少。需要提醒的是ConcurrentHashMap的size()在并发写场景下不是精确值它返回的是一个估测值JDK 8用了一个CounterCell数组来分散计数最终通过累加来统计。面试中被问到怎么计算size的时候不要说直接读size字段而要说出baseCount加累加CounterCell这套机制才能体现出你真的读过源码。4.2 Collections工具类包装方法与Hashtable的取舍除了ConcurrentHashMap还有一个老牌线程安全Map叫Hashtable。Hashtable是JDK 1.0就有的类内部直接用synchronized锁住整个表所以读和写都会被同一个锁阻塞并发性能非常差。还有Collections.synchronizedMap(new HashMap())这种方式返回的是一个同步包装类本质上也是给每个方法加synchronized锁。既然有了ConcurrentHashMap这两者在高并发场景下都不推荐。低并发场景或仅需要线程安全的简单封装时Collections.synchronizedMap也有它的价值——代码简单、改动小、不会引入额外的复杂度。而ConcurrentHashMap在读多写少的场景下几乎是无锁的因为它的get操作不加锁依靠volatile CAS保证可见性。如果数据量不大、并发压力不高用synchronizedMap完全没问题如果写多读多、要求高吞吐还是用ConcurrentHashMap更合适。面试官如果问Hashtable为什么慢你要能答出两点锁的粒度太大整表加锁和锁本身是重量级的。对比之下ConcurrentHashMap锁的粒度是单个桶读操作又不加锁性能自然好很多。4.3 CopyOnWriteArrayList与其他并发集合并发场景下的List很多人不知道用哪个。常用的有两种CopyOnWriteArrayList和Collections.synchronizedList(new ArrayList())。CopyOnWriteArrayList的名字说明了它的写策略每次写操作add、remove等都会复制一份底层数组在副本上修改然后通过volatile数组引用替换旧数组读操作直接读旧数组不加锁。这个方案让读多写少的场景非常高效——比如配置文件刷新、白名单列表、缓存键集合之类的场景。面试喜欢问CopyOnWriteArrayList的缺点你要能主动说出来写操作代价高昂每次add都要复制整个数组如果列表很大或者写频繁内存和GC压力会很大另外读操作虽然能读到旧数据但不保证实时看到最新写入存在弱一致性问题。如果你的业务是写多读少CopyOnWriteArrayList反而会成为性能瓶颈不如用synchronizedList。CopyOnWriteArrayList还有一个巧妙之处它的迭代器不支持add/remove操作遍历时不会抛ConcurrentModificationException因为迭代器是在创建时基于当前数组快照的。这个快照迭代器的特性有些面试官会问到你可以顺便提一嘴与HashMap的fail-fast机制做对比。5. Set与排序规则去重逻辑和比较器体系5.1 HashSet与TreeSet的实现原理HashSet看似独立其实底层完全复用HashMap。当你new HashSet()的时候底层创建的是new HashMap()而每次add的元素作为HashMap的keyvalue统一用一个静态的PRESENT占位对象。所以HashSet的元素天然不会重复——是否重复完全由HashMap的key判断逻辑决定。而HashMap判断key是否相同的规则是先比较hashCode是否相等再比较equals方法是否返回true。所以Set去重的前提是正确重写equals和hashCode。TreeSet则基于TreeMap实现底层是一个红黑树它要求元素要么实现Comparable接口要么在构造TreeSet时传入Comparator。TreeSet的元素天然是有序的遍历时按自然顺序或自定义顺序输出——但代价是插入、删除的时间复杂度是O(log n)比HashSet的O(1)慢。我在面试中经常问候选人一个问题如果一个对象作为HashSet的元素它的hashCode变了会发生什么很多人答不上来。实际场景是如果你把对象放进HashSet后又修改了对象参与hashCode计算的字段那么这个对象的hashCode就变了但它在HashSet底层的桶下标依然是旧的。这时候你再把这个对象取出来判断是否存在会发现在另一个桶里找不到它集合里就产生了一个内存泄漏——对象永远留在Set里无法通过正常方法删除。这个坑在写缓存、写去重逻辑时特别容易踩。5.2 Comparable与Comparator对比Comparable和Comparator是Java排序体系的两块基石。Comparable是自然排序定义在元素类内部实现compareTo方法意思是我天生可以和自己比较Comparator是临时比较器定义在类外部适合做多种不同的排序规则。说人话Comparable是元素自己的默认排序规则Comparator是外部的灵活替身。举个例子你有一个Person类默认按age排序就实现Comparable但你在某个业务场景下想按name排序另一个场景想按salary排序这时候就不建议修改Person类的compareTo而是分别写不同的Comparator匿名类或Lambda表达式。面试官特别喜欢考如果一个对象实现了Comparable同时又传入了Comparator以哪个为准——答案是Comparator优先因为TreeSet或Collections.sort接收比较器时会用比较器而非自然顺序。5.3 equals和hashCode的正确姿势这是一个老生常谈却又特别容易在实战中出错的知识点。equals和hashCode的约定是如果两个对象equals为true那么它们的hashCode必须相等反过来不成立hashCode相等但equals不相等是允许的。如果你重写了equals却没有重写hashCode那么两个逻辑上相等的对象会在HashMap/HashSet中因为hashCode不同而被当成不同元素。我见过一个真实的线上bug项目里定义了一个订单对象只重写了equals方法用于业务比较但没有重写hashCode结果用这个对象做Set去重时同样的订单被存了两份最后导致统计结果翻倍。重写hashCode的时候必须在构造hashCode的字段上使用相同的字段而且这些字段最好是不可变的——如果参与hashCode计算的字段能变那集合的去重逻辑就会出问题。注意在实现hashCode时不要用乘以固定质数的玄学恐惧实际上只要能保证分布均匀、性能可接受就行。我习惯用31这个数因为31 * i在JVM里可以被优化成(i 5) - i位运算比乘法快。6. 高频面试题实战与回答思路6.1 经典速查表为了让你在面试前快速过一遍我把最常考的集合面试题整理成了一张速查表你可以把每一行当成一个自测题遮住回答列看自己能不能在两分钟内说清楚。面试题回答要点ArrayList扩容多少倍1.5倍oldCapacity (oldCapacity 1)均摊时间复杂度O(1)HashMap为什么容量是2的幂保证(n - 1) hash与hash % n等价位运算更快同时让扩容后位置计算更简单负载因子为什么是0.75时间与空间的折中冲突概率和空间利用率的平衡HashMap链表转红黑树的阈值链表长度到达8且数组长度不小于64才能树化红黑树转链表的阈值6留滞回区间避免频繁转换Java 8 HashMap插入方式尾插法相比Java 7头插法避免扩容死循环ConcurrentHashMap JDK 8实现CAS synchronized锁粒度是单个桶节点CopyOnWriteArrayList写操作代价每次写复制整个数组写多读少场景不适用HashSet如何实现去重底层是HashMap元素作为keyvalue统一PRESENT占位TreeSet要求元素满足什么实现Comparable或传入Comparator6.2 场景设计题与连环追问集合部分的面试中高级岗位非常喜欢出场景设计题。最常见的一个是给你500万个字符串统计每个字符串出现的次数不能用现成的统计框架你怎么设计这个问题其实直接指向HashMap的用法遍历字符串列表判断map.containsKey(s)存在则计数加一不存在则put初始值1。如果你能用Java 8的merge方法配合Integer::sum一行代码就能搞定既简洁又说明你熟悉JDK新特性。如果面试官进一步问字符串特别多、内存不够怎么办你可以回答使用外部排序、分片文件、Redis HyperLogLog非精确或者布隆过滤器做预过滤这些方案思路是加分项。另一个经典设计题是如何基于LinkedList实现LRU缓存。这个题目考察的是你知道LRU需要访问到元素就把它移到头部这种操作而LinkedList的remove和addFirst配合正好可以实现。如果你有经验你会知道更高效的解法是HashMap 自定义双向链表让get操作的时间复杂度从O(n)降到O(1)。这两种方案都能说清楚的话说明你对数据结构的组合使用有感觉。还有一个要留意的是手写一个简单的HashMap put逻辑。这道题看起来简单其实考察了你是否理解数组索引计算、冲突解决、扩容这三大模块。我建议你平时就写一个只支持put和get的精简版比如用Node数组存元素用(n - 1) hash计算索引冲突时用头插法或尾插法形成链表。写一遍之后再去看JDK源码很多疑问会迎刃而解。7. 备考经验与实战避坑笔记7.1 我亲测有效的记忆方法备考集合面试我最推荐的思路是从下往上的历史演进法。先理解集合框架是为了解决什么问题而设计的——比如从数组的固定长度无法自动扩容衍生出ArrayList从链表查找效率低衍生出哈希表和HashMap从HashMap线程不安全衍生出ConcurrentHashMap。当你把每个集合类都当成某个问题的解决方案去记忆它的数据结构、核心参数、适用场景就都变成逻辑推导的必然结果而不是需要死记的数字。我见过很多候选人准备面试时被源码细节淹没把每个数字背得滚瓜烂熟但一说到为什么就支支吾吾。我的建议是对于每个核心参数16、0.75、8、6、64你至少要能答出它的来源和权衡逻辑。数字忘了可以现场推导但逻辑没想清楚就会暴露真实水平。7.2 实战中最容易忽略的细节写代码时大家都会用集合但很多细节是面试和线上问题的高发区。我在最后把这些低级错误但代价极高的坑列出来希望能帮大家避雷第一Arrays.asList()返回的不是java.util.ArrayList而是Arrays内部的一个私有ArrayList它不支持add和remove操作。如果你对它调用add会抛UnsupportedOperationException。正确的转换方式是new ArrayList(Arrays.asList(...))。第二集合嵌套使用时比如ListMapString, List 这种结构操作内部集合前一定要判空否则一个null就够你排查半天。我习惯在组装这种复杂结构时提前用computeIfAbsent来避免空指针非常顺手。第三注意集合序列化的坑。HashMap在序列化时并不会序列化整个table数组而是先遍历所有Node节点把key和value分别序列化。因为扩容后数组下标会变化直接序列化数组反而会造成数据错误。这个设计也解释了为什么HashMap的table不能用final修饰——因为扩容时要重新赋值数组引用。第四如果需要频繁遍历并删除集合中的元素不要用fori循环正序删除因为删除元素后索引会错位可能跳过元素。正确做法是使用迭代器的remove方法或直接用removeIf。从Java 8开始removeIf是最简洁的方案也是我日常开发中的首选。写到这里我想起自己第一次深入研究HashMap源码的时候被那一行行位运算和扩容逻辑绕得晕头转向。后来我把每个参数都代入具体数字一点点推演流程突然发现这些设计像搭积木一样环环相扣。如果你正在准备面试别急把集合框架当成一棵树慢慢梳理从根接口到叶子实现类再到并发变体和工具类理顺之后你会发现面试题变得不再八股而是有逻辑、有血有肉的知识体系。希望这篇总结能帮你少走一些弯路面试顺利通过。
返回列表