Java_ec
一、Eclipse Collections (EC)
EclipseCollections是由 Goldman Sachs 开发并捐赠给 Eclipse 基金会的高性能 Java 集合库。它不仅提供了丰富的集合 API,还特别注重内存效率和性能优化,尤其擅长处理基本类型集合。本文将深入探讨EC的核心特性、使用场景及其与其他集合库的对比。
| 特性 | Eclipse Collections (EC) | FastUtil |
|---|---|---|
| 开发者 | Goldman Sachs(后捐给 Eclipse 基金会) | Sebastiano Vigna(意大利学者) |
| 首发时间 | 2014 年开源(前身 Caramel 可追溯到 2000s) | 2006 年左右 |
| 核心目标 | 提供面向对象 + 函数式的丰富集合 API,兼容 JDK |
提供极致性能 + 内存效率的 primitive 集合 |
| 是否支持高阶函数 | 强大(类似 Scala/Stream) | 极少(专注底层结构) |
1、为什么需要 EC
1)Java 标准集合的局限
尽管 java.util 集合(如 ArrayList, HashMap)广泛使用,但在现代应用中存在明显短板:
| 问题 | 说明 |
|---|---|
| 缺乏基本类型支持 | 必须使用 Integer、Long,导致装箱开销 |
| API 能力薄弱 | 无内置 groupBy、zip、flatCollect 等高阶操作 |
| 不可变集合缺失 | 需手动包装或依赖 Guava |
| 内存效率一般 | HashSet/HashMap 对象头和 Entry 开销大 |
| 函数式支持滞后 | Java 8 Stream 是“附加层”,非集合原生能力、 |
2)EC 的核心价值
- 原生支持基本类型集合(如
IntList,LongSet) - 丰富的函数式 API(类似 Scala、Kotlin 集合)
- 高性能实现(比 JDK 快 20–50%)
- 完整的可变/不可变集合体系
- 内存优化(如
UnifiedMap比HashMap更紧凑)
二、核心特性
1、两大集合家族:可变 vs 不可变
| 类型 | 接口 | 实现类示例 | 特点 |
|---|---|---|---|
| 可变 | MutableList<T>, MutableSet<T> |
Lists.mutable.empty() |
支持增删改,线程不安全 |
| 不可变 | ImmutableList<T>, ImmutableSet<T> |
Lists.immutable.of(1,2,3) |
创建后不可修改,线程安全 |
2、基本类型集合支持
EC 为常用基本类型提供专用集合(不支持 char/float 等冷门类型):
| 元素类型 | List |
Set |
Bag(多重集) |
|---|---|---|---|
int |
MutableIntList |
MutableIntSet |
MutableIntBag |
long |
MutableLongList |
MutableLongSet |
MutableLongBag |
double |
MutableDoubleList |
MutableDoubleSet |
MutableDoubleBag |
boolean |
MutableBooleanList |
MutableBooleanSet |
— |
3、函数式 API(开发效率利器)
1) 对比 jdk 流
| 对比项 | JDK Stream |
Eclipse Collections |
|---|---|---|
| 是否创建中间集合 | 惰性流不创建,但终端操作可能触发装箱 | 所有操作直接在原集合上执行,无中间流对象 |
| 是否装箱 | IntStream → mapToObj 会装箱 |
IntList.select(...) 保持 int,全程无装箱 |
| 方法调用开销 | 高(Stream → Pipeline → Sink 多层抽象) |
低(直接调用 select() 内部循环) |
JIT 优化友好度 |
中等(Lambda + 接口调用) |
高(大量 final 方法 + 内联友好) |
EC 将高阶函数直接集成到集合接口,无需转 Stream:
List<Integer> result = list.stream()
.filter(x -> x > 0) // 步骤1:过滤
.map(x -> x * x) // 步骤2:平方
.collect(Collectors.toList());
MutableIntList result = intList
.select(x -> x > 0) // 第1次遍历 → 返回新 IntList [3, 5]
.collect(x -> x * x); // 第2次遍历 → 返回新 IntList [9, 25]
MutableIntList result = intList.asLazy()
.select(x -> x > 0)
.collect(x -> x * x)
.toList(); // 触发执行
2)遍历次数
| 方式 | 遍历次数 | 中间集合 | 装箱 | 适用场景 |
|---|---|---|---|---|
| JDK Stream | 1 次 | 无 | 是 | 对象集合、简单逻辑、不想引第三方库 |
| EC Eager | 2 次 | 1 个 | 否 | 短链(1–2 步)、追求极致简单、小数据 |
| EC Lazy | 1 次 | 无 | 否 | 长链、大数据、primitive 集合、高性能要求 |
3)常用操作
| 操作 | 方法 | 说明 |
|---|---|---|
| 过滤 | select(Predicate) |
类似 filter |
| 映射 | collect(Function) |
类似 map |
| 扁平映射 | flatCollect(Function) |
类似 flatMap |
| 分组 | groupBy(Function) |
返回 Multimap |
| 折叠 | reduce(BinaryOperator) |
聚合计算 |
| 配对 | zip(Iterable) |
两个集合按位组合 |
| 分块 | chunk(int size) |
分批处理 |
4、Map
| 用途 | 可变集合 | 不可变集合 |
|---|---|---|
| 创建空 Map | Maps.mutable.empty() |
Maps.immutable.empty() |
| 从现有数据创建 | Maps.mutable.of(k1, v1, k2, v2) |
Maps.immutable.of(k1, v1, ...) |
1)基本类型 Map(避免装箱)
EC的IntObjectHashMap和FastUtil的Int2ObjectOpenHashMap在原理上高度相似,都采用开放寻址 + 平行数组。
| Key → Value | 可变 Map 类型 | 工厂方法示例 |
|---|---|---|
int → int |
MutableIntIntMap |
IntIntMaps.mutable.empty() |
int → long |
MutableIntLongMap |
IntLongMaps.mutable.empty() |
int → double |
MutableIntDoubleMap |
IntDoubleMaps.mutable.empty() |
int → Object |
MutableIntObjectMap<V> |
IntObjectMaps.mutable.<V>empty() |
long → int |
MutableLongIntMap |
LongIntMaps.mutable.empty() |
long → Object |
MutableLongObjectMap<V> |
LongObjectMaps.mutable.<V>empty() |
Object → int |
MutableObjectIntMap<K> |
ObjectIntMaps.mutable.<K>empty() |
2)特殊 Map 类型
| 类型 | 说明 | 使用场景 |
|---|---|---|
BiMap<K, V> |
双向映射,支持 inverse() 反查 |
编码 ↔ 名称(如 “USD” ↔ 840) |
Multimap<K, V> |
一个 key 对应多个 value | 用户 → 角色列表、标签聚合 |
SortedMap<K, V> |
有序 Map(基于比较器) | 需要按键排序的缓存 |
三、底层原理:为什么又快又好?
之所以在性能和内存效率上显著优于
JDK标准集合,甚至在某些场景下媲美 FastUtil,源于其精心设计的底层数据结构与运行时优化策略。以下是其三大核心技术支柱:
1、高效数据结构
1)UnifiedMap / UnifiedSet:告别 Entry 对象
JDK 的 HashMap 和 HashSet 每个元素都封装在一个 Node<K,V> 或 Entry 对象中,带来显著的内存开销(每个对象至少 24–32 字节,含对象头、哈希码、指针等)。
而 EC 的 UnifiedMap 采用 “平行数组 + 单桶位图” 的紧凑布局:
-
Unified的含义:统一存储,消除中间对象。 -
键(
keys)和值(values)分别存储在两个独立的Object[]数组中所有 key 存在一个数组,所有 value 存在另一个数组 Object[] keys; // [key1, key2, key3, ...] Object[] values; // [val1, val2, val3, ...] -
使用一个
int[]或long[]作为哈希表索引与状态标记(空/占用/删除)
2)原始类型集合:零装箱,原生数组
- 对于
MutableIntList、MutableLongSet等原始类型集合: - 内部直接使用
int[]、long[]等原生数组 - 完全避免
Integer、Long包装对象的创建
2、迭代与计算优化
1)高度优化的迭代器
EC 的迭代器(如 MutableList.iterator())经过深度内联优化,减少方法调用开销:
- 大量方法标记为
finalfinal方法不能被重写 → 调用目标在编译期就完全确定 → JIT 可安全地将其内联。- 最大化 JIT 内联机会,消除虚方法调用开销
- 循环体无虚方法调用(
JIT更易优化) - 支持 快速失败(fail-fast) 但开销极低
2)惰性求值支持
通过 .asLazy() 可将链式操作转为惰性流:
- 仅遍历一次
- 无中间集合分配
- 全程保持原始类型,无装箱
MutableIntList result = intList
.asLazy()
.select(x -> x > 0)
.collect(x -> x * x)
.toList(); // 此时才执行
3、 内存布局与 CPU 缓存友好
1)连续内存访问
- 原始类型集合(如
int[])在内存中连续存放 - 遍历时具有极佳的 空间局部性
- 极大减少 CPU 缓存未命中
2)减少指针跳转
JDK 的 LinkedList 或 HashMap 的 Node 链表会导致频繁的 非连续内存跳转,而 EC 的数组式结构避免了这一点。
四、与 FastUtil 和 JDK的对比
1、三大集合体系概览
| 特性 | EC |
FastUtil |
JDK |
|---|---|---|---|
| 起源 | Oracle 官方标准库 | 意大利学者 Sebastiano Vigna | Goldman Sachs(后捐给 Eclipse 基金会) |
| 定位 | 通用、标准、安全 | 极致性能、底层优化 | 高效 + 高表达力 + 工程友好 |
Primitive 集合 |
支持常用类型 | 支持全类型 | 仅支持包装类 |
| 函数式 API | 内置丰富高阶方法 | 无内置高阶方法,仅提供基础迭代 | 通过 Stream 提供函数式能力 |
| 不可变集合 | 支持 | 不支持不可变集合 | 仅提供只读视图,底层集合仍可修改 |
| 内存效率 | 优秀,比 JDK 节省 50% |
最优,无对象头,紧凑内存布局 | 较差,存在装箱和 Entry 对象开销 |
| 查询性能 | 快,优于 JDK |
极快,基于开放寻址哈希且无装箱 | 较慢,依赖链表或红黑树结构并伴随装箱 |
2、原始类型支持
| 特性 | JDK |
FastUtil |
EC |
|---|---|---|---|
| 支持类型 | 包装类(Integer, Long) |
全部 8 种 byte, char, short, int, long, float, double, boolean |
常用 4 种 int, long, double, boolean |
| 装箱开销 | 高 | 零 | 零 |
| 示例 | ArrayList<Integer> |
IntArrayList |
MutableIntList |
3、内部结构
1)FastUtil:开放寻址——“逻辑单桶”
public class Int2IntOpenHashMap {
protected transient int[] key; // 所有键
protected transient int[] value; // 所有值
protected transient boolean[] used; // 标记槽位是否被占用(可选,部分版本用特殊值如 0 表示空)
protected final float loadFactor;
}
a、工作原理:
- 哈希函数计算出初始位置
pos = hash(key) % capacity - 直接检查
key[pos]:- 若
used[pos] == false→ 空,插入 - 若
key[pos] == target→ 命中 - 否则 → 线性探测:
pos = (pos + 1) & mask,继续检查
- 若
- 键和值始终在同一索引
pos上
b、内存布局示意图
index: 0 1 2 3 4 5 6 7
key: [10] [20] [ ] [30] [ ] [ ] [40] [ ]
value: [1] [2] [?] [3] [?] [?] [4] [?]
used: [T] [T] [F] [T] [F] [F] [T] [F]
c、特点:
- 访问一个
entry只需 一次索引计算,读key[i]和value[i]是相邻内存 - 探测过程在连续地址上进行 → CPU 缓存预取高效
d、致命缺陷(对象场景)
- 无法区分
null键 和 空槽 → 必须禁止nullkey。 - 删除后留 tombstone:
key[i] = DEFUNCT_KEY,该槽位永远不能真正释放。- 导致:有效负载因子(actual fill ratio)持续下降。
- 后果:即使只用了 50% 容量,也可能因
tombstone触发扩容 → 内存浪费反超 EC。
2)EC:分离式结构——“物理三数组”
注意:EC 没有使用开放寻址,而是采用 “哈希 + 链式/探测”混合策略(早期版本用链表,新版本多用二次探测,但仍依赖 states[])
public final class IntIntHashMap {
private int[] keys; // 键数组
private int[] values; // 值数组
private byte[] states; // 状态数组:0=free, 1=occupied, 2=removed
private int occupiedCount;
}
a、工作原理:
- 哈希计算出初始位置
index - 检查
states[index]:- 若为
0(free)→ 插入到keys[index],values[index] - 若为
1(occupied)且keys[index] == key→ 命中 - 否则 → 按探测序列(如二次探测)找下一个
index'
- 若为
- 每次访问都要查三个数组的同一个下标
b、内存布局示意图:
keys: [10, 20, ?, 30, ?, ?, 40, ?]
values: [1, 2, ?, 3, ?, ?, 4, ?]
states: [1, 1, 0, 1, 0, 0, 1, 0]
↑ ↑ ↑ ↑
idx=0,1,3,6 被占用
c、问题:
-
CPU读取keys[i]时,values[i]和states[i]可能不在同一缓存行 -
尤其当数组很大时,三个数组分散在堆的不同区域 → 缓存未命中率升高
4、FQA
1)内存布局与性能
实测:存储 1 亿条 int → int 映射
| 实现 | 内存占用 | 原因分析 |
|---|---|---|
HashMap<Integer, Integer> |
≈ 3.2 GB | 每对需两个包装对象 + Node 对象(≈32B/entry) |
MutableIntIntMap(EC) |
≈ 1.1–1.3 GB | 无装箱,但使用双数组 + 元信息(如状态标记、size 等) |
Int2IntOpenHashMap(FastUtil) |
≈ 800 MB | 单一 key[] + value[],负载因子 0.75,无任何额外对象 |
a、JDK: HashMap
- 每个键值对封装为
Node<K,V>对象 - 对象分散在堆中 → 指针跳转多、缓存不友好
- 装箱 + 对象头 → 内存膨胀 3–4 倍
b、EC: UnifiedMap / MutableIntIntMap
- 采用 双数组 + 状态标记:虽无
Entry对象,但通常维护 三个数组keys[]存储键values[]存储值states[](或位图)标记槽位状态(空/占用/删除)
- 1个哈希槽位 =
index,但keys[index],values[index],occupied[index]是三个独立数组的元素 -
无
Entry对象,大幅减少对象数量,但哈希冲突仍通过线性探测或二次探测处理(非开放寻址) - 内存访问需跨多个数组 → 缓存行利用率略低
- 设计更注重通用性与安全性(如支持 null 键/值)
c、FastUtil: Int2IntOpenHashMap
- 采用 开放寻址哈希(Open Addressing)
- 所有数据连续存放,所有键值直接存入两个连续
int[]数组 - 一个哈希槽位 =
(key[i], value[i]),冲突时i → i+1 → i+2... - 冲突时通过 线性探测 + 位运算优化的哈希函数 解决
- 无链表、无对象、无间接引用,
- 所有数据连续存放,所有键值直接存入两个连续
- 内存布局极致紧凑,
CPU缓存命中率极高
2)EC 为何不采用开放寻址
EC 的设计哲学更偏向 通用性与安全性:FastUtil 为性能牺牲了灵活性,EC 为通用性接受了一点性能折衷
- 支持
null键/值:FastUtil的原始类型 Map 不支持 null(因用特殊值如0表示空),而EC允许(通过states[]区分) - 删除更安全:开放寻址中删除需标记为 “deleted”(否则破坏探测链),
EC的states[]天然支持 - API 一致性:
EC的对象集合(如UnifiedMap)也用类似三数组结构,保持内部统一
3)都是“数组”,但内存访问模式天差地别
| 特性 | FastUtil | EC |
|---|---|---|
| 结构本质 | 开放寻址哈希表 | 分离式哈希表(带状态标记) |
| 数组数量 | 2(key + value),状态可融合 | 3(keys + values + states) |
| 适用场景 | 极致性能、大数据、无 null | 业务系统、需 null、需丰富 API |
4)EC 为何更适合业务系统?
| 维度 | 优势说明 |
|---|---|
| 开发体验 | 函数式 API 让复杂逻辑“一行表达”,减少样板代码,提升可读性与可维护性 |
| 不可变集合 | 真正不可变(immutable)集合天然线程安全,避免并发 bug |
| 多元组支持 | 原生三元组集合解决复合键难题,无需手写 hashCode |
| 集合种类丰富 | Bag、BiMap、Multimap 等覆盖 90% 业务场景 |
| 工程友好 | 由 Goldman Sachs 实战打磨,被金融、电商等高要求系统广泛采用 |
5)如何选型?
| 需求 | 推荐 |
|---|---|
| 极致性能 + 最小内存 + 大数据(>10M) | → FastUtil |
| 高开发效率 + 复杂业务逻辑 + 线程安全 | → Eclipse Collections |
| 简单场景 + 零依赖 + 快速交付 | → JDK |


