Java集合框架底层原理与选型实践:从ArrayList到HashMap深度解析
2026/9/17 22:18:51 网站建设 项目流程

最近带团队做代码评审,我发现一个很有意思的现象:很多能熟练写出业务代码的同学,聊到 Java 集合框架时,还是停留在“会用 API”的层面。问他们 ArrayList 扩容一次翻几倍、HashMap 为什么默认负载因子是 0.75、HashSet 去重到底依赖什么,往往答不上来。恰好我最近在整理 Java 基础面试的复习资料,把集合这块重新过了一遍,所以这篇就把我对 Java 集合知识点的理解做一个系统梳理。

这篇内容适合准备 Java 面试的人、做代码重构的人,以及想把自己脑子里的集合知识体系整理得更完整的人。我不打算只罗列接口和实现类,那网上到处都有。我重点讲三件事:集合框架到底为什么这么设计、核心实现类的底层原理、实际开发中怎么选型才不踩坑。看完之后,你再看那些“八股文”都会觉得通透很多。

1. 集合框架的整体设计:先搞懂 Java 集合到底解决什么问题

1.1 编程里的集合,和高等数学里的集合有什么不一样

很多人第一次听说“集合”是在大一高等数学上册里,数学上定义的集合是指“确定且互异对象的整体”,研究的是元素是否属于某个集合、集合之间的交并补关系。Java 里的集合虽然也叫 Set,但本质完全是另一回事——它是一套用来存放对象的容器,解决的是对象怎么存、怎么取、怎么遍历、怎么排序的问题,侧重点是数据结构。

不过这两者有个重要的共通点:数学集合要求元素“互异”,Java 的 Set 也要求元素“不重复”。理解这个点之后,再去想 HashSet 怎么实现去重、TreeSet 怎么维持有序,思路就会顺很多。很多人学集合时觉得东西太多记不住,就是因为没先分清“容器”和“数据结构”这两个视角,结果把接口、实现类、并发集合一股脑混在一起背。

1.2 Collection 和 Map:两大家族,各管一摊

Java 集合框架的顶层可以分成两大家族:Collection 和 Map。

Collection 是单元素集合的根接口,下面又分三个分支:

  • List:有序、可重复,元素按插入顺序排列,可以通过下标访问。典型实现是 ArrayList、LinkedList。
  • Set:无序、不可重复,重点解决“去重”问题。典型实现是 HashSet、LinkedHashSet、TreeSet。
  • Queue:队列,主要用于“先进先出”或按优先级处理元素。典型实现是 ArrayDeque、PriorityQueue。

Map 是另一个独立家族,不继承 Collection,它存的是键值对,重点解决“按 key 找 value”的问题。典型实现是 HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap。

为什么 Map 要单独设计成一个家族?因为键值对这种模型没法用单元素序列来表达。一个 Map.Entry 里有 key 和 value 两个对象,和 List 里存的单个对象不是一个维度。而且 Map 的很多操作,比如按 key 遍历、按 key 哈希定位、对 key 排序,都和单元素集合完全不同。把两者分开,接口职责更清晰,用起来也更顺手。

另外,所有集合类都实现了 Iterable 接口,也就是说,凡是能用增强 for 循环的集合,底层都实现了这个接口。这里有个小知识:foreach 语法糖在编译后其实是基于 iterator 实现的,所以如果一个类没实现 Iterable,它是没法用增强 for 遍历的。这也是为什么 ArrayList、HashSet 这些类都有 iterator() 方法。

1.3 抽象类的作用:为了少写重复代码

集合框架里还有一层很容易被忽略的设计:抽象类。比如 AbstractList、AbstractSet、AbstractMap。你去看 ArrayList 的源码,会发现它继承自 AbstractList,而很多方法比如 add、remove、set 在 AbstractList 里已经有骨架实现了,子类只需要补上个别抽象方法。

这就是典型的“模板方法模式”:父类定义好算法骨架,子类实现具体细节。比如 AbstractList 里已经实现了 iterator(),它依赖子类提供的 get()、size()、add() 等方法,子类不需要重复造轮子。

面试官问“集合框架的设计有哪些值得借鉴”,这其实是个很加分的回答点。面试官想听的往往不是你能背几个耗时对比,而是你有没有从框架设计者的角度去思考:为什么要抽象一层接口?为什么要搞抽象类?为什么 ArrayList 要继承 AbstractList 而不是直接实现 List?理解了这层之后,你才能说自己是“学过”集合框架而不是“看过”集合框架。

2. 逐个拆解核心实现类:知道底层数据结构,就不会记混

2.1 List 接口:ArrayList 和 LinkedList 到底怎么选

List 最常用的两个实现就是 ArrayList 和 LinkedList。

ArrayList 底层是 Object 数组。无参构造创建时底层是个空数组,第一次 add 才会分配默认容量 10。扩容时,新容量 = 旧容量 + (旧容量 >> 1),也就是每次扩容 1.5 倍。比如容量 10 扩容到 15,15 扩容到 22,依此类推。扩容的本质是新建一个更大数组,再用 System.arraycopy 把原数据搬过去。所以如果你能预估数据量,最好在构造时就指定初始容量,避免频繁扩容损失性能。

LinkedList 底层是双向链表,每个节点维护前驱和后继引用。它不像 ArrayList 需要连续内存,插入删除只要能找到节点,确实省去了数组平移的开销。但问题是,按索引访问做不到 O(1),必须从头部或尾部逐步遍历。实际开发里,很多人用 LinkedList 以为“插入删除快”,但业务场景里绝大多数操作是遍历和随机访问,LinkedList 反而更慢,加上每个节点额外存储两个引用,内存占用也更大。

所以选型建议其实很简单:

场景推荐
大量随机访问、尾插为主ArrayList
需要频繁在头部/中间插入删除若不要求索引随机访问,优先 ArrayDeque / LinkedList
需要当栈使用ArrayDeque 更推荐
需要当队列使用ArrayDeque 更推荐

注意,LinkedList 虽然实现了 List 接口,也能当 Deque 用,但不代表它适合所有场景。我见过不少项目里把一个 LinkedList 当成“万能容器”,遍历几百次,性能肉眼可见地慢,换成 ArrayList 之后明显改善。

2.2 Set 接口:为什么 HashSet 无序,TreeSet 有序

Set 的核心是去重,但它下面三个实现类的风格完全不同。

HashSet 底层其实就是一个 HashMap,add 的元素作为 key,value 固定为一个 Object 占位。判断元素是否重复的标准是 hashCode 和 equals:先看 hashCode 是否相同,相同再走 equals。所以如果你存自定义对象,就必须正确重写这两个方法,否则两个字段值完全一样的对象也会被当成不同元素存进去。

LinkedHashSet 是 HashSet 的子类,底层用 LinkedHashMap 实现,多了一条双向链表用来维护插入顺序。它牺牲一点性能,换来迭代时能按插入顺序输出。适合对顺序有要求、但不想用 TreeSet 排序的场景。

TreeSet 底层是 TreeMap,是红黑树结构。它不依赖 hashCode,而是按元素的自然顺序或构造时传入的 Comparator 来排序。每次插入都要比较,所以时间复杂度是 O(log n)。TreeSet 里不允许 null,因为红黑树排序时没法比较 null 的大小。如果你把比较器设置允许 null,那可以商量,否则会直接抛 NullPointerException。

这三个结构对比一张表就很清晰:

实现类底层结构迭代顺序时间复杂度
HashSetHashMap无序,基本按哈希桶顺序增删查 O(1)
LinkedHashSetLinkedHashMap按插入顺序增删查 O(1),略慢于 HashSet
TreeSetTreeMap(红黑树)按自然顺序或 Comparator增删查 O(log n)

2.3 Queue 和 Deque:队列场景用得少,但面试常考

Queue 日常业务里用得相对少一点,但在任务调度、生产者消费者、滑动窗口这些场景里很关键。

ArrayDeque 底层是循环数组,既支持先进先出,也支持双端操作。它实现 Deque 接口,可以当作栈用,而且比 java.util.Stack 更推荐。注意,ArrayDeque 不允许放 null 元素,因为它用 null 来标记“槽位为空”。栈如果允许 null,判断空栈时就会和“存在 null 元素”冲突,所以干脆禁止。

PriorityQueue 底层是二叉堆,默认是小顶堆。你放进去元素后,poll 出来的总是当前优先级最高的元素,但元素在队列内部的存储顺序并不是排序后的形态,而是堆结构。PriorityQueue 初始容量默认是 11,扩容时如果旧容量小于 64,容量翻倍,大于等于 64,扩容 1.5 倍。它同样不允许 null 元素,因为堆调整时需要比较大小,null 没法参与比较。

很多面试题会问“让你实现一个 Top K 最大元素方案”,PriorityQueue 就是默认答案。固定一个大小为 K 的小顶堆,每次新元素比堆顶大就把堆顶替换掉,时间复杂度是 O(n log K),比每次全量排序高效得多。

2.4 Map 接口:你真正需要熟练掌握的 Map 家族

HashMap:最常用,底层是“数组 + 链表 + 红黑树”,允许一个 null key 和多个 null value,不保证迭代顺序。这是整个集合框架里的重中之重,下一节我会单独展开。

LinkedHashMap:继承 HashMap,内部额外维护一条双向链表。默认按插入顺序迭代,构造时如果把 accessOrder 设为 true,就按访问顺序迭代。基于这个特性,LinkedHashMap 可以用来实现 LRU 缓存,重写 removeEldestEntry 方法就能在超过容量时淘汰最久没访问的 Entry。

TreeMap:底层红黑树,key 按自然顺序或 Comparator 排序。它实现了 NavigableMap 接口,支持 subMap、headMap、tailMap 这些范围查询。注意 TreeMap 不允许 null key,原因和 TreeSet 一样,排序时没法比较。

Hashtable:这是历史遗留类,方法用 synchronized 修饰保证线程安全,但现在几乎不推荐使用。因为它锁的粒度太粗,并发场景性能差,而且它的迭代器是 fail-fast 的,不是真正的强一致。现在并发场景用 ConcurrentHashMap 替代。

ConcurrentHashMap:并发版的 HashMap。JDK7 用分段锁,JDK8 放弃了分段锁,改用 CAS 配合 synchronized 锁桶头节点,锁粒度更细,读操作大部分不需要加锁。它不允许 null key 和 null value,这个和 HashMap 不同。原因是在并发环境下,无法区分“这个 key 不存在”和“这个 key 对应的 value 是 null”,容易产生二义性。

3. HashMap 源码级核心:面试高频,日常开发也容易踩坑

3.1 底层结构:数组、链表、红黑树三者怎么配合

HashMap 底层是一个 Node 数组,每个数组槽位叫桶。放元素时,先算 key 的哈希,定位到具体桶;如果桶里没有元素,直接放;如果桶里已经有元素(发生了哈希冲突),就把新节点追加到链表末尾。

JDK8 里,链表长度超过阈值时,会尝试转红黑树。具体条件是:桶中链表长度大于等于 8,且整个数组长度大于等于 64。两个条件同时满足才树化。如果链表长度到了 8,但数组长度还不到 64,HashMap 会优先扩容而不是转树,因为扩容后哈希冲突会缓解,链表长度自然变短。树化之后,如果因为删除元素导致红黑树节点数少到 6,会退化为链表。注意阈值 8 和 6 之间留了 7 的缓冲,避免频繁树化和退化来回摇摆。

为什么树化阈值选 8?源码注释里给了个概率解释:在随机哈希、负载因子 0.75 的情况下,桶中链表长度达到 8 的概率已经极低,大约亿分之六。所以正常情况下链表就够用了,转红黑树只是“防御性编程”,防止黑客构造大量哈希冲突的 key 导致性能从 O(1) 劣化到 O(n)。

3.2 哈希寻址过程:高位异或和位运算

put 一个元素时,HashMap 并不是直接用 key.hashCode() 作为桶下标。它会先做一次扰动:把 hashCode 的高 16 位和低 16 位异或。代码是(h = key.hashCode()) ^ (h >>> 16)

为什么要做这一步?因为桶下标用的是数组长度减一后做按位与,比如(n - 1) & hash。当数组长度比较小时,直接拿 hash 和它做与运算,高位的所有信息都会丢失,只有低位参与计算。如果 hashCode 的低位分布不均匀,冲突概率就会很高。把高 16 位异或到低 16 位,相当于让高位信息也参与取模,分散性更好。

这也是 HashMap 的一个高频面试题:为什么 HashMap 的容量是 2 的幂?因为只有 n 是 2 的幂时,(n - 1) & hash才等价于hash % n,而且位运算比取模快得多。为了在所有情况下都保持这个性质,HashMap 的构造方法会把传入的初始容量调整成大于等于该值的最小 2 的幂。这个操作叫 tableSizeFor。

3.3 负载因子 0.75 和扩容机制

HashMap 的默认初始容量是 16,默认负载因子是 0.75。扩容的临界值 threshold = 容量 * 负载因子。默认情况下,16 * 0.75 = 12,也就是说,元素个数达到 12 时,HashMap 就会扩容成 32,紧接着 threshold 变成 24,以此类推。

为什么负载因子要选 0.75?这是时间复杂度和空间复杂度的一个折中。负载因子太高,比如 1.0,内存能省一些,但哈希冲突会变多,链表变长,查询效率下降;负载因子太低,比如 0.5,冲突少了但数组大部分空间是空的,频繁扩容也很浪费。0.75 是在常见哈希函数下经过权衡后比较均衡的一个值,也是默认值。如果你明确容器里放的元素很多,可以在构造时指定更小的负载因子,或者直接用容量估算公式,而不是等着反复扩容。

JDK8 扩容时,元素位置重新计算有个很巧妙的规律:因为容量翻倍刚好是二进制左移一位,元素要么还在原来的下标位置,要么从原来下标位置移动到“原下标 + 旧容量”的位置。比如旧容量 16,元素原来在桶 3,扩容成 32 后,它只可能在 3 或者 19,判断依据是 key 的 hash 值在旧容量对应那一位上是 0 还是 1。这个规律让扩容时不需要重新计算每个 key 的哈希,只需要看新增的那个 bit 位就够了,JDK8 就用了这个优化,把扩容过程的性能提了一截。

这里还要提一个经典坑:JDK7 的 HashMap 扩容时用的是头插法,多个线程同时对同一 HashMap 扩容时,链表可能形成环形结构,get 时就会进入死循环。JDK8 改成了尾插法,解决了环的问题,但 HashMap 依然不是线程安全的。并发场景下,多个线程同时 put 可能导致覆盖、数据丢失,必须用 ConcurrentHashMap。

3.4 容量为什么是 2 的幂,以及初始化容量的隐藏细节

前面提到了(n - 1) & hash高效的前提是 n 为 2 的幂。所以就算你在构造时传了 17,HashMap 也会通过 tableSizeFor 调整成 32。

这里有个很容易踩的坑:new HashMap<>(7)不是说你真的能放下 7 个元素不扩容。它内部会把容量调整成 8,然后 threshold = 8 * 0.75 = 6。也就是说,你插入第 7 个元素时,HashMap 就触发扩容了。正确预估容量的做法是:new HashMap<>((int) (expectedSize / 0.75f) + 1)。比如你预计放 10 个元素,那么最好设置初始容量为(int)(10 / 0.75f) + 1 = 14,HashMap 再调整成 16,threshold 变成 12,插入 10 个元素就不会触发扩容。

很多资深一点的开发会用 Guava 的Maps.newHashMapWithExpectedSize,其实本质也是那个公式。这个细节面试官也爱考,能说出来说明你真正看过源码。

4. 迭代、排序与并发:这些实际操作里的坑,我也踩过

4.1 fail-fast 机制和 ConcurrentModificationException

ArrayList、HashMap 这些集合里都有一个 modCount 字段,记录集合被修改的次数。迭代器创建时会把这个值存到 expectedModCount 里。迭代过程中,如果集合被第三方修改(比如另一个线程在 put 数据),modCount 和 expectedModCount 不一致,迭代器就会立刻抛出 ConcurrentModificationException。

这个机制叫 fail-fast:宁可快速失败,也不让迭代在错误状态中继续跑下去,避免后续读到脏数据。它并不保证一定发生,只是尽力检测并发修改,所以不能依赖它来做并发控制。

很多人踩过这个坑:在 for-each 循环里直接调 list.remove()。表面看是 remove 后立刻 break,有时不报错,但很多情况下都会抛 ConcurrentModificationException。因为 for-each 底层用的就是迭代器,而 ArrayList 的 remove() 改变了 modCount,迭代器的 expectedModCount 没有同步更新。

正确的删除方式有三种:使用迭代器的iterator.remove();使用 JDK8 的removeIf;或者先收集要删的元素,循环结束后再统一删除。其中removeIf最简洁,内部已经处理好了迭代器同步。

4.2 集合排序:Comparable 和 Comparator 别搞混

排序是集合用得非常多的操作。List 排序用Collections.sort(list)list.sort(comparator)。如果元素实现了 Comparable 接口,比如 Integer、String,可以直接排;如果是自定义对象,就传一个 Comparator。

Comparable 是“类自己知道自己怎么排序”,一个类只能实现一种排序规则。Comparator 是“外部写一个比较器”,可以灵活定义按年龄排、按姓名排、按长度排等不同规则。两者这个区别,面试经常考。

还有一点是排序稳定性。Java 的Collections.sort()对对象使用的是 TimSort,它是稳定排序,也就是说,排序前相等的元素,排序后相对顺序不会变。这在实际业务里很有用,比如先按日期排,再按优先级排,稳定性能保证日期相同的数据之间仍然保持优先级顺序。

4.3 线程安全集合到底怎么选

集合的线程安全问题很容易被低估。ArrayList 不是线程安全的,HashMap 也不是。最简单的加线程安全方式是用Collections.synchronizedList(list)Collections.synchronizedMap(map)这类包装类,它们在每个方法上加了同步锁。但注意,复合操作还是要自己加锁。比如 contains 之后再 put,两个线程完全可能在中间穿插执行,光靠方法级别同步是防不住的。

CopyOnWriteArrayList 适合读多写少的场景,写时复制底层数组,读不需要加锁,但每次写操作都会复制整个数组,写成本很高。如果你写操作很频繁,不建议用。

ConcurrentHashMap 是并发场景下 Map 的首选。JDK8 之后,它的并发控制粒度已经细化到桶级别,大量写入不会互相干扰,且 size、isEmpty 这类读操作基本无锁,性能很好。至于 Hashtable,除非是在维护老项目,否则真的没有理由在新代码里用它。

4.4 可变对象做 key 的坑:改一下字段,就找不到了

这一点我特别想提。很多人把对象放进 HashSet,或者作为 HashMap 的 key,然后又去修改对象的属性。结果这个对象的 hashCode 变了,但它的存储桶还是按旧 hashCode 算出来的,get 的时候按新 hashCode 去查,自然就查不到了。

有个朋友曾经在项目里维护一个缓存,key 用的是一个自定义业务对象,结果对象状态变了几次之后,缓存大面积 miss,最后排查了半天才发现是这个问题。规避方案很简单:在 Set 中存放的元素、在 Map 中用作 key 的对象,尽量设计成不可变的,或者不要在放入集合后修改其 hashCode 相关字段。如果一定要修改,那就先 remove 再改再 add,千万别直接在集合里改。

5. 集合选型与性能实践:实际开发中我是这么用的

5.1 一张选型决策表

我平时做代码评审时,经常直接让同事用这张表做决策:

业务场景推荐选择
需要按下标随机访问,尾部追加为主ArrayList
需要频繁在头部插入删除,且当队列/栈用ArrayDeque
需要双端操作,且内存要求不苛刻LinkedList
需要去重,不要求顺序HashSet
需要去重,且按插入顺序访问LinkedHashSet
需要去重,且按自然/自定义顺序排序TreeSet
普通键值对缓存、索引HashMap
需要保持插入顺序或实现 LRU 的键值对LinkedHashMap
需要按 key 排序,做范围查询TreeMap
高并发读写的 MapConcurrentHashMap
读多写少的 ListCopyOnWriteArrayList

这张表不是死规则,但绝大多数场景用它选,不会错得太离谱。

5.2 初始容量和容量预估:别小看频繁扩容的开销

集合扩容对性能影响很大,尤其是数据量大的时候。ArrayList 扩容会触发数组复制,HashMap 扩容除了复制节点还要重新计算桶下标,代价更高。如果你在循环里往一个无参构造的 ArrayList 插入 10 万条数据,中间会触发很多次扩容,虽然次数是指数级减少的,但累积的 arraycopy 开销仍然可观。

所以预估数据规模时,尽量使用带初始容量的构造器。ArrayList 用new ArrayList<>(expectedSize),HashMap 用new HashMap<>((int) (expectedSize / 0.75f) + 1)。这里要注意,直接new HashMap<>(expectedSize)并不等于“能放下 expectedSize 个元素不扩容”,因为真正决定扩容的是 threshold,而不是容量本身。

5.3 不可变集合和空集合:细节里藏着不少坑

Java 9 引入了List.ofSet.ofMap.of,这些方法返回的是不可变集合,不能 add、remove,也不能修改元素。它们比Arrays.asList严格得多。Arrays.asList返回的是一个固长列表,能修改元素,但不能 add 和 remove,否则直接抛 UnsupportedOperationException。而且Arrays.asList的底层还是原来的数组,改列表元素会同步改数组,这个特性很多人不知道。

返回空集合的时候,推荐用Collections.emptyList()emptySet()emptyMap()而不是new ArrayList<>(),因为前者返回的是单例,省去新建对象。虽然节省的对象开销很小,但代码里写着也显得更专业。

5.4 集合间操作和转换:常用的几个高效写法

集合之间求交集、差集、并集,很多人第一反应是遍历,其实 JDK 已经提供了方法:

  • listA.retainAll(listB):求交集,listA变成 A 和 B 的交集。
  • listA.removeAll(listB):求差集,listA变成 A 中去除 B 之后的元素。
  • listA.addAll(listB):求并集(不去重),注意会改变listA

数组转集合用Arrays.asList(array),但要注意它返回的并不是 ArrayList 的真正形态,而是 Arrays 内部的一个私有类,长度固定。集合转数组用list.toArray(new Object[0]),JDK8 之后推荐传入长度为 0 的数组,Java 会根据集合大小重新分配。

保护类集合也很重要。如果你返回的是一个内部集合,不想让调用方随意修改,可以包一层Collections.unmodifiableList(list)。一旦有代码尝试修改,就会抛异常。这比“约定好别改”可靠得多。

6. 面试高频题与速查表:背不背源码,就看这几个问题

集合相关的面试题,翻来覆去其实就那么几个方向。这里整理一份速查,每个问题后面给的是最关键的答案点,方便你在面试前快速过一遍:

问题关键答案点
ArrayList 和 LinkedList 区别底层数据结构、随机访问复杂度、插入删除复杂度、内存占用
ArrayList 扩容规则默认容量 10,扩容 1.5 倍,底层 arraycopy
HashMap 底层结构JDK8 数组 + 链表 + 红黑树,解决哈希冲突
HashMap 为什么是 2 的幂(n-1)&hash等价于取模,扩容时元素位置只在原位和原位+oldCap 之间
负载因子 0.75 原因时间与空间的折中;泊松分布下链表长度到 8 概率极低
链表什么时候转红黑树链表长度 >= 8 且数组长度 >= 64
HashMap 为什么线程不安全并发 put 会覆盖数据,JDK7 扩容可能环形链表,JDK8 也丢数据
Hashtable 和 HashMap 区别线程安全、null key/value 限制、性能、迭代器行为
ConcurrentHashMap 怎么保证线程安全JDK8 CAS + synchronized 锁桶头,不允许 null
HashSet 如何保证去重底层 HashMap,key 用 hashCode 和 equals 判断重复
TreeMap 如何保证有序红黑树,按自然顺序或 Comparator 比较
LinkedHashMap 如何实现 LRUaccessOrder=true,重写 removeEldestEntry 淘汰最老节点
fail-fast 和 fail-safe 区别迭代中检测 modCount;fail-safe 使用副本,读到的不保证新数据
Comparable 和 Comparator 区别类内实现 vs 外部实现,一个类不能多套排序规则
为什么重写 equals 必须重写 hashCode保持 hashCode 一致,否则 HashSet、HashMap 去重失效

这里再多说一个我会重点追问的点:你能不能在纸上把 HashMap put 一个 key 的完整流程画出来?从计算 hash、定位桶、创建节点、链表追加、树化判断、到最后的扩容检查,整条链路走通,比背十道题都有用。我面试时最常问的一句话是“往 HashMap 里 put 一个对象,这个对象到底存到哪里,扩容时它又是怎么被找到的”,能把这个过程画明白的人,集合这块基本不用担心。

如果你在准备面试,我最后一条建议是:不要死记源码行号。源码只是个载体,真正重要的是它背后的权衡和取舍。比如为什么用位运算而不是取模、为什么负载因子不是 1.0、为什么树化还要等数组长度到 64,这些问题想明白了,哪怕面试官换个刁钻角度问,你也能用同样的逻辑推出来。

我在实际项目中用集合的习惯也在慢慢变化。以前赶业务的时候,随手就是一个 HashMap,很少想初始容量和负载因子。后来排查线上性能毛刺,发现很多次都是集合频繁扩容导致的,才意识到一个简单的构造参数对高并发系统的影响可以这么大。所以我现在写代码,凡是能预估规模的集合,都会顺手把初始容量带上。这个习惯看着小,长期下来省掉的不仅是扩容开销,还有一堆排查奇怪的性能事故的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询