ARTICLE DETAIL

资讯详情

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

Java容器类全解析:从HashMap到ConcurrentHashMap的底层原理与实战

Java容器类全解析:从HashMap到ConcurrentHashMap的底层原理与实战 刚带了个实习生项目里写HashMap遍历删除元素跑起来直接抛ConcurrentModificationException他愣了半天没想明白。这场景我见得太多了Java容器类看着是基础中的基础但真要往深了问——ArrayList扩容到底是1.5倍还是两倍HashMap为什么用红黑树不用普通链表ConcurrentHashMap在JDK 8里是怎么干掉分段锁的——很多写了三五年Java的人未必都能答得干净利落。这篇文章我打算把Java容器类整个摊开揉碎讲一遍从Collection和Map两大体系的宏观设计一路拆到源码级别的实现细节再配合实操选型和面试高频问题。不管你是刚学Java的学生还是准备跳槽的开发者或者工作中经常跟集合打交道的后端工程师这篇文章都能帮你在脑海里建立一张完整的容器知识地图。1. 容器类全景图Collection与Map两大家族1.1 两大接口体系的定位差异Java容器类也叫集合类整体分成两大阵营一个是Collection一个是Map。很多人刚学的时候搞不清楚这两个东西到底有什么区别其实用一句话就能说透Collection是存单个元素的Map是存键值对的。这个差异看起来简单但决定了后续所有使用场景的走向。Collection下面又分了List、Set、Queue三个子接口它们各自解决不同的问题List管有序可重复Set管唯一去重Queue管先进先出之类的排队逻辑。Map这边则不继承Collection它自成体系核心是“通过键去找值”就像查字典——你报一个词条我告诉你释义。再往底层看一层这些接口的实现类背后都是数据结构在撑腰。数组、链表、哈希表、红黑树、队列堆Java容器基本把这些经典数据结构都封装了一遍。换句话说把Java容器类学透了等于把大学数据结构课本用工程化的方式重写了一遍这也是为什么面试必考、笔试常考、工作中天天用的根本原因。从JDK的类库设计来看Collection和Map都位于java.util包下但Map并不像很多人以为的那样实现了Collection接口。历史上JDK官方对这个设计有过解释核心意思是两种抽象维度不同——一个是集合数学意义上的元素集合一个是映射关系。千万别在面试时说“Map实现了Collection接口”这句话一说出来基本就凉了。1.2 List、Set、Queue的分工与底层预览List有序、可重复、有索引。List的实现类里ArrayList是数组动态扩容查询快、插入删除慢LinkedList是双向链表头尾操作快、随机访问慢Vector是线程安全的ArrayList老前辈但现在基本没人直接用。Set无序大部分实现、不可重复。HashSet底层是HashMap的key部分LinkedHashSet在HashSet基础上加了双向链表维持插入顺序TreeSet底层是红黑树天然有序。Queue队列核心操作是offer入队、poll出队、peek看一眼队头。LinkedList也实现了Queue接口ArrayDeque是循环数组实现的双端队列PriorityQueue是堆结构出队按优先级来。这三大类不是割裂的它们之间有很强的联动关系HashSet无非是包装了一个“value永远为固定对象的HashMap”LinkedHashSet底层又复用LinkedHashMapTreeSet底层就是TreeMap。理解了这层复用关系你再看容器类的源码会发现很多类根本不是从零写的而是组合、委托、包装出来的。这也是Java容器类设计的精妙之处——用最小的核心代码撑起最丰富的功能面。1.3 Map体系的结构与实现Map是个顶层接口下面常见实现有这么几个HashMap哈希表链表红黑树最常用的实现乱序。LinkedHashMap在HashMap基础上加双向链表支持插入顺序或访问顺序遍历。TreeMap红黑树实现按键的自然顺序或自定义比较器排序。Hashtable老古董全方法加锁性能差基本被ConcurrentHashMap取代。ConcurrentHashMap并发安全的HashMapJDK 8后用CASsynchronized锁桶实现。很多人容易混淆Hashtable和HashMap的命名——前者中间的t是小写因为它是小写t的专有名词源自“hash table”两个词。这俩除了名字看起来像内部设计思路完全不同后面我会详细拆。2. 核心实现原理拆解从扩容到哈希冲突2.1 ArrayList扩容机制先算好账再动手ArrayList底层就是个Object[]数组构造时可以指定初始容量如果不指定默认是10。问题来了数组是定长的往里面塞元素塞满了怎么办答案是扩容——新创建一个更大的数组把旧数据用System.arraycopy拷过去。JDK 8里扩容公式是int newCapacity oldCapacity (oldCapacity 1);也就是1.5倍扩容oldCapacity oldCapacity/2。不是两倍更不是固定加多少。为什么要取1.5这个数如果扩容倍数太小比如1.1倍添加元素的均摊成本高频繁触发复制如果太大比如两倍内存浪费严重。1.5倍是一个在时间与空间之间平衡得比较好的经验值。注意一下oldCapacity 1表示右移一位即除以2所以oldCapacity (oldCapacity 1)就是1.5倍。如果是从默认容量10开始涨扩容序列是10 → 15 → 22 → 33 → 49……而不是规整的倍数增长。实操中我会建议如果你能估算出数据量直接用new ArrayList(capacity)指定初始容量。比如从数据库查出10万条记录要封装成List你明确知道会是10万条那就直接给10万。这能省掉多次扩容的复制开销——虽然System.arraycopy是native方法很快但大数据量下多次扩容累积的开销依然不可忽略。和ArrayList形成对比的是LinkedList。LinkedList底层是双向链表每个节点是一个Node对象存着数据、前驱引用、后继引用。它无所谓“扩容”因为节点是随用随建。但代价是每个元素多存两个引用内存占用比ArrayList高。这还没算上节点对象本身的头开销在64位JVM上一个Node对象可能占到24字节以上。所以如果有人跟你说“LinkedList插入快就用它”你得反问一句“你在哪个位置插入头部尾部还是中间”——中间插入照样要先遍历找位置复杂度O(n)。2.2 HashMap源码级拆解哈希、冲突、树化HashMap是Java容器里内容最丰富的一个类也几乎是面试必考题。它底层是数组链表红黑树三合一的结构。数组的每一个格子叫bucket桶当两个键的哈希值落到同一个桶时用链表把它们串起来。但如果同一个桶里的元素越来越多链表查询复杂度退化成O(n)所以JDK 8做了一个重要优化——当链表长度达到8且数组长度达到64时链表会转成红黑树把查询效率从O(n)压到O(log n)。为什么阈值偏偏是8源码注释里有一段来自概率统计的解释假设哈希函数均匀分布一个桶里元素数量服从泊松分布链表长度达到8的概率约为千万分之六。换句话说正常场景下链表长度几乎不可能超过8如果真超过了说明哈希函数出现了严重的不均匀或者有人恶意构造了哈希碰撞这时候用红黑树兜底抵抗最坏情况。红黑树不是Java发明的新东西它本质上是一棵自平衡的二叉搜索树通过颜色约束根黑、叶黑、红节点的孩子必须黑、任意路径黑节点数相同保证树高不超过2log(n1)。正因为这个约束查、插、删的复杂度都能维持在O(log n)级别。HashMap默认初始容量16加载因子0.75。0.75这个值的含义是当元素个数到达容量*加载因子16*0.7512时触发扩容数组加倍到32、64、128……容量始终保持2的幂。为什么必须是2的幂关键在设计上。HashMap定位桶的下标用的是(n - 1) hash而不是hash % n因为位运算比取模快得多。但位运算是建立在容量为2的幂这个前提下的——n-1的二进制的低几位全是1 hash等价于取hash值的低位天然均匀而且扩容时元素迁移特别方便新位置要么在原位置要么在原位置旧容量处。这个特性让JDK 8的resize过程不需要重新计算每个元素的hash只需看新增的那一位bit是0还是1效率极高。再说哈希本身的处理。JDK 8的hash()方法长这样static final int hash(Object key) { int h; return (key null) ? 0 : (h key.hashCode()) ^ (h 16); }叫“扰动函数”把高16位异或到低16位。为什么要这样做因为计算桶下标用的是(n - 1) hash当n比较小时比如16n-1的低4位是1高位全是0hash只有低4位参与运算高位的差异全被丢弃非常容易碰撞。让高16位参与低16位的运算等于把高位信息压缩到低位参与定址能明显减少碰撞概率。这个细节面试的时候能主动讲出来是加分项。2.3 equals、hashCode与对象键的陷阱HashMap的查找过程分两步先用hashCode()定位到桶再通过equals()在桶里的链表/红黑树上找精确匹配。所以有一个铁律重写equals必须重写hashCode且equals相等的两个对象hashCode必须相等。否则的话两个逻辑上相等的对象可能被分到不同的桶get永远找不到——这就像你把书放到了图书馆的A区却在B区找它。举个例子如果不重写hashCode两个“内容相同”的自定义对象各自继承Object.hashCode()这个方法是基于对象内存地址生成的所以两个对象的hashCode必然不同。哪怕它们equals是true也没用因为HashMap根本不会拿它们去互相比较——它俩压根不在一个桶里。所以在开发中用自定义对象做Map的key要格外小心。我见过最坑的案例用一个可变对象做key放进去之后又改了对象的内容导致hashCode()结果变了后来按原逻辑去找怎么都找不到。更麻烦的是那个“丢失”的键值对还在Map里占据内存但永远无法访问。最安全的做法用String、Integer、Long这类不可变对象做key。它们hashCode稳定、equals实现正确、不可变是HashMap最理想的伙伴。如果非要用自定义对象要么保证对象不可变所有字段final不提供修改方法要么给这个类设计一个稳定不变的hashCode逻辑。3. 实战选型数据结构和业务场景一一对应3.1 从需求反推容器选择容器选型不是靠感觉得来的你得先弄清楚自己的业务到底有哪些访问模式。我总结了一下比较常见的需求场景和对应容器业务场景推荐容器理由按下标随机访问多几乎不做插入删除ArrayList底层数组按下标访问O(1)频繁在头部/尾部插入删除元素ArrayDeque / LinkedList头尾操作O(1)按某个字段去重HashSet / LinkedHashSet哈希去重O(1)LinkedHashSet保序需要按自然顺序或自定义规则遍历TreeSet / TreeMap红黑树天然有序中序遍历输出升序通过唯一键快速找对应对象如用户ID查用户HashMap键值映射哈希O(1)查值要求遍历顺序和插入顺序一致LinkedHashMap双向链表维护插入序需要“最近最少使用”淘汰的缓存LinkedHashMap(accessOrdertrue)访问顺序removeEldestEntry可实现LRU按优先级处理任务PriorityQueue堆结构O(log n)出队最小/最大元素这里要特别提醒一点很多教科书上写“LinkedList适合频繁插入删除”实际上现代工程里绝大多数场景用ArrayList反而更快。为什么因为LinkedList每个节点分散在内存里大量随机访问触发CPU缓存失效而ArrayList内存连续遍历时可以很好地利用CPU缓存预取。JDK开发团队也在官方文档里直言不讳地建议“大多数场景ArrayList比LinkedList更合适后者主要用于某些特定结构需求”。所以别迷信复杂度理论工程上得看实测表现。3.2 容量预估与性能优化说到性能优化容器这块最值得做的两件事一是预估容量二是避免无谓的自动装箱。先讲容量。ArrayList的默认构造不会立刻创建一个容量10的数组——JDK 8里用的是懒加载第一次add元素时才分配容量10的数组。同样HashMap的构造方法里也不会立刻分配16个bucket而是在第一次put时初始化。这个设计是为了省内存如果你创建了一个空Map就扔在那不用没必要占空间。但如果你明确知道数据规模提前给容量很重要。以HashMap为例如果你要放100个元素直接new HashMap()的话它会在放第13个元素时触发第一次扩容因为16*0.7512后续还得扩好几次。更推荐的做法是写代码时按照“预计元素数/加载因子”来反推初始容量然后赋值。比如你知道要放100个元素那就用new HashMap(100 / 0.75f 1)约等于134HashMap会向上取2的幂到256这样全程不需要扩容。再讲自动装箱。HashMapInteger, Integer里如果put一堆intJava会自动装箱成Integer对象。装箱不仅多出对象创建的开销还意味着额外的内存。脏数据量一大的时候这个开销是实打实的。Java没有原生的int键HashMap除非用第三方库如fastutil所以该装箱还是得装但要心里有数——千万级数据量下尽量少在容器里存包装类型做批量操作。3.3 Stream与容器的高频组合Java 8之后Stream API几乎成了操作容器的主流写法。举几个高频场景把一个List转成MapMapLong, User idUserMap userList.stream() .collect(Collectors.toMap(User::getId, Function.identity()));这段代码看着简洁但有两个著名的坑。第一个坑如果User::getId有重复值会抛IllegalStateException: Duplicate key。第二个坑如果value为nullFunction.identity()返回null用户对象Collectors.toMap默认不允许null value会抛NPE。解决方案是使用重载方法提供合并函数MapLong, User idUserMap userList.stream() .collect(Collectors.toMap(User::getId, Function.identity(), (oldVal, newVal) - newVal));第三个参数(oldVal, newVal) - newVal表示遇到重复key时保留后一个。如果你需要保留前一个就(oldVal, newVal) - oldVal。如果value可能是null但你又必须要null那就只能换成Collectors.toMap的三参或四参版本配合HashMap::new指定map工厂同时value包装一下。按某个字段分组也是高频操作MapInteger, ListUser usersByAge userList.stream() .collect(Collectors.groupingBy(User::getAge));分组结果默认是HashMap但groupingBy还支持传入下游收集器和Map工厂比如你想得到有序MapMapInteger, ListUser usersByAge userList.stream() .collect(Collectors.groupingBy(User::getAge, TreeMap::new, Collectors.toList()));Stream操作容器的好处是代码结构清晰坏处是——如果能用普通for循环一趟搞定的事没必要非套Stream。Stream本身也有对象创建和调用链开销在超大数据量下不如传统写法快。写代码前先权衡可读性高的同时也要关心性能不是Stream万能。4. 并发场景下的容器选择4.1 为什么Vector已经“过时”了早年间Vector是线程安全的List它把所有方法都加上了synchronized用起来确实不会并发出错。但问题也出在这里每个方法调用都要抢同一把锁方法与方法是独立的组合操作却完全不安全。举个例子经典的“先检查再删除”if (!vector.isEmpty()) { vector.remove(0); }两个线程同时进来一个判断非空另一个同时删除第一个线程的remove就可能抛异常。你可以说“那给这段代码自己也加锁嘛”可一旦这么做了Vector内部方法锁和外部代码锁是两码事根本防不住并发。还有一个更致命的问题因为每个方法都要抢锁Vector在高并发下的吞吐量极其难看。它存在了那么多年更多是作为历史遗留被提到——现在如果你还在新项目里用Vector基本可以被认定是“停留在JDK 1.1时代的老兵”。替代方案也很明确单线程或外部同步用ArrayList读多写极少的并发场景用CopyOnWriteArrayList。4.2 CopyOnWriteArrayList读写分离的抄写员CopyOnWriteArrayList的设计思路非常清奇——读操作完全不锁写操作先复制一份新数组在新数组上做修改然后替换旧数组引用。读操作读的是旧数组写操作先复制再改再发布读和写没有任何锁竞争所以读性能极高适合“遍历远多于修改”的场景。典型应用是事件监听器列表一堆监听器注册在上面每次发布事件就遍历它们但注册/注销动作极少发生。但它的缺点也摆在明面上每次add都会完整复制底层数组如果元素很多、写操作频繁复制代价非常大。还有更隐性的一点——CopyOnWriteArrayList的迭代器是“弱一致性”的迭代过程中如果别的线程做了修改迭代器不会反映出来也不会抛ConcurrentModificationException因为它遍历的是迭代器创建时那一份快照。这在某些要求强一致性的业务里是致命的。所以用CopyOnWriteArrayList之前先问自己两个问题读多写少吗写操作真的很低频吗答案都是“是”才敢放心用。4.3 ConcurrentHashMap现代Java并发容器的顶梁柱并发容器里最重要、也最值得研究的就是ConcurrentHashMap。JDK 7时代的ConcurrentHashMap用的是分段锁把整个Map分成默认16个Segment每个Segment是一把独立的锁不同线程操作不同Segment可以并行整体并发度是16。但JDK 8之后彻底重写了实现分段锁被丢弃改为锁桶——每个桶数组槽位独立加锁使用synchronized锁住链表/红黑树的头节点配合CAS进行无锁插入。JDK 8的put流程大致是计算hash定位桶下标。如果桶为空用CAS把新节点放进去无须加锁——这是无锁快速路径。如果桶不空synchronized锁住桶头节点然后往链表或红黑树里插入。如果链表长度达到树化阈值同样转红黑树。用synchronized锁桶而不是用那种更复杂的锁机制其实有个微妙的好处synchronized在JDK 6以后引入了偏向锁、轻量级锁的优化路径锁竞争很小时开销极低。而且锁粒度从Segment缩小到了bucket两个线程操作不同桶时完全互不干扰理论上并发度接近桶的数量。ConcurrentHashMap还有几个面试高频考点它不允许null键和null值。为什么要禁因为并发环境下无法分辨“值不存在”和“值为null”。HashMap可以get返回null表示没找到但ConcurrentHashMap如果允许null读线程会面临一个歧义是没这个键还是值本身就是null为了避免这个混沌作者直接不让put null。它的size()不是精确值。因为高并发下很难拿到一个准确的size快照JDK 8里size()返回的是一个估计值。如果你在做容量判断时依赖它要注意这个特性。更骚的是mappingCount()官方文档说它才是返回元素数量的推荐方法虽然它也是估计值。遍历时它也是弱一致性的和CopyOnWriteArrayList类似的逻辑——迭代器不会抛并发修改异常但不保证看到最新状态。如果业务要求“遍历期间必须看到所有已提交的元素”ConcurrentHashMap并不合适你得额外加锁或做快照。4.4 Collections.synchronizedXXX看似线程安全的陷阱Collections.synchronizedMap(new HashMap())是很多人用来“给HashMap加锁”的方式。它确实让每个方法都加了synchronized但这层同步和Vector有一样的毛病——复合操作为了安全必须自己再加锁。比如经典的MapString, Integer map Collections.synchronizedMap(new HashMap()); if (!map.containsKey(key)) { map.put(key, 1); }两个线程同时执行这段代码完全可能发生一个线程put成功后另一个线程拿之前的状态也执行put覆盖掉。要解决这个竞态你必须在外部再加锁把containsKey和put包成一个原子操作。可是既然都要外部加锁那用不用synchronizedMap还有什么意义从性能上看synchronizedMap的全方法锁粒度大、竞争激烈吞吐量远不如ConcurrentHashMap。所以这个工具类现在的处境挺尴尬的基本是个“看着安全、用着操心”的过渡方案。在面试时如果有人问你“synchronizedMap能用吗”比较好的回答是“能用但要么你的访问模式特别简单要么你能在外部做好完整的加锁设计否则不如换个更合适的并发容器”。5. 高频面试题与避坑清单5.1 面试官最爱问的容器类问题Top 10结合我自己面试候选人和被面试的经验整理了一份容器类高频考题附带简要解析Q1ArrayList和LinkedList的区别底层结构数组 vs 双向链表。随机访问ArrayList O(1)、LinkedList O(n)头尾插入删除LinkedList O(1)、ArrayList尾部O(1)摊还但头部O(n)。实际工程中ArrayList用得多得多因为内存连续性好缓存利用率高而且绝大多数场景是遍历和随机访问。Q2HashMap底层数据结构数组链表JDK 8后当链表长度≥8且数组长度≥64时转红黑树。查询平均O(1)最坏O(log n)。Q3HashMap扩容流程当size超过容量×加载因子时触发扩容容量加倍元素重新分布。JDK 8利用2的幂特性元素在新数组中的位置要么原位置要么“原位置旧容量”不需要重新计算每个hash。Q4为什么加载因子是0.75时间与空间的折中。太高如1空间利用率高但碰撞概率增大太低的浪费内存。0.75是官方基于大量测试得出的相对均衡的值。Q5HashSet底层原理就是HashMapvalue恒为一个固定Object对象。所以HashSet的元素就是HashMap的key不重复、可null。Q6TreeSet和TreeMap有序性怎么来的底层红黑树插入时按比较器自然顺序或自定义Comparator进行节点旋转与着色平衡中序遍历得到升序序列。Q7HashMap和Hashtable的区别HashMap非线程安全、允许null键值、JDK 8引入红黑树优化Hashtable线程安全全方法锁、不允许null、比较老。Q8如何用Map实现LRU缓存LinkedHashMap的accessOrdertrue构造模式配合重写removeEldestEntry当元素数量超过阈值时自动删除最久未访问的条目。Q9什么情况下用CopyOnWriteArrayList读多写极少并且能接受弱一致性。Q10ConcurrentHashMap为什么性能好JDK 8用CAS无锁插入空桶锁桶头节点锁粒度比全表锁细很多减少竞争volatile的读写让元素可见性有保障。5.2 实务中的隐藏陷阱以下这些坑我在实际开发和Code Review里都碰到过整理成清单供你避开第一遍历HashMap时删除元素。用for-each循环遍历HashMap的同时调remove会抛ConcurrentModificationException。正确姿势是用迭代器的remove()方法或者用JDK 8的removeIfmap.entrySet().removeIf(entry - entry.getValue() 0);第二TreeSet元素必须可比较。把对象丢进TreeSet前要么对象实现了Comparable接口要么在TreeSet构造时传入Comparator。否则运行时抛ClassCastException。注意TreeSet判断“重复”依据的是compareTo返回0而不是equals两者不一致时容易翻车。第三自定义对象的去重问题。HashSet去重依赖hashCode()和equals()缺一不可。只重写equals不重写hashCode会导致HashSet里出现两个equals相等但hashCode不同的对象去重失败。这也是为什么IDE生成equals时总问你要不要一起生成hashCode。第四LinkedHashMap做LRU要开accessOrder。默认构造是accessOrderfalse表示按插入顺序迭代要变成访问顺序必须用new LinkedHashMap(16, 0.75f, true)。同时要重写removeEldestEntry方法否则容量满了也不会自动淘汰。第五数组转List的坑。Arrays.asList()返回的是一个长度固定的List底层还是那个数组不是ArrayList。调用add或remove会抛UnsupportedOperationException。我见过太多人在这里栽跟头——把asList返回的List当普通List用一调add就崩。要真想要一个灵活结构可以用new ArrayList(Arrays.asList(...))。第六subList的视图陷阱。List.subList()返回的是原列表的视图不是新列表。对subList做结构性修改会影响原列表而且对原列表做结构性修改后再操作subList会抛ConcurrentModificationException。老实说这个API设计经常被吐槽但既然在JDK里摆着就按它的规矩来。5.3 源码级追问再深挖一层这几题属于加分项答得出来说明你真正读过源码、有自己的理解。为什么HashMap的默认容量是16不是10也不是20因为容量必须是2的幂而16是2^4是个漂亮的最小合理化值。太小了频繁扩容太大了小Map浪费内存。16能在绝大多数场景下用最少的内存换来最多的map容量。为什么JDK 8的resize里判断新位置是看(e.hash oldCap) 0因为容量oldCap是2的幂扩容后新容量是oldCap的两倍下标定位从(n-1)hash变到(2n-1)hash。新旧下标的差值恰好是oldCap。e.hash oldCap这一位决定元素留在原位0还是迁移到原位置oldCap非0。这段代码的逻辑让扩容过程极其高效不需要重新计算hash值。HashMap线程不安全具体指什么多线程同时put时如果有两个线程同时触发了resize旧版本pre-JDK 8可能出现链表环导致get死循环。这个问题在JDK 8里通过引入红黑树和resize的优化基本不再出现但并发下数据丢失、覆盖的竞态问题依然存在。所以多线程环境一次都别用HashMap直接上ConcurrentHashMap。fail-fast和fail-safe什么区别fail-fast指的是迭代器在迭代过程中如果发现结构被修改modCount变化立即抛ConcurrentModificationException——ArrayList、HashMap的迭代器都是这个风格。fail-safe指的是迭代过程中“结构修改不抛异常但迭代结果可能是旧快照”的风格CopyOnWriteArrayList和ConcurrentHashMap的迭代器属于这一类。面试时这个表述干净利落地说出来基本能镇住一半面试官。5.4 我个人的容器类使用规范最后分享一套我给自己定的书写规范算不上标准答案但至少踩坑少能用不可变集合就用不可变集合。Java 9之后有List.of、Map.of构造完就不可变既线程安全、还能防止代码里误改共享数据结构。以前我老看到有人把自己的ArrayList直接暴露出去给别人add线上问题就是这么一层层闹出来的。所有public方法返回集合前先确认应该返回什么类型。如果调用方只读不写返回Collections.unmodifiableList包装一下如果调用方要顺序遍历返回List而不是HashSet——HashSet的顺序不稳定换一个JDK版本或者不同哈希函数顺序就会变调用方如果按顺序处理数据容易被坑。做性能优化前先测量。不要在代码里“凭感觉”给ArrayList预分配超大容量、给HashMap设偏高初始容量。先跑一把真实数据量看实际占用和耗时再调整。容器优化绝不是玄学一切以JVM监控和profiler数据为准。容器类是Java里最基础、最常用、也最值得深入研究的类库。它们不是孤立的知识点而是和数据结构、并发、JVM内存模型都产生强关联的知识网络。把这套东西理解透你写Java代码的稳定性和效率都会明显上一个台阶。
返回列表