一、Eclipse Collections (EC)

Eclipse Collections 是由 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)广泛使用,但在现代应用中存在明显短板:

问题 说明
缺乏基本类型支持 必须使用 IntegerLong,导致装箱开销
API 能力薄弱 无内置 groupByzipflatCollect 等高阶操作
不可变集合缺失 需手动包装或依赖 Guava
内存效率一般 HashSet/HashMap 对象头和 Entry 开销大
函数式支持滞后 Java 8 Stream 是“附加层”,非集合原生能力、

2)EC 的核心价值

  • 原生支持基本类型集合(如 IntList, LongSet
  • 丰富的函数式 API(类似 Scala、Kotlin 集合)
  • 高性能实现(比 JDK 快 20–50%)
  • 完整的可变/不可变集合体系
  • 内存优化(如 UnifiedMapHashMap 更紧凑)

二、核心特性

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
是否创建中间集合 惰性流不创建,但终端操作可能触发装箱 所有操作直接在原集合上执行,无中间流对象
是否装箱 IntStreammapToObj 会装箱 IntList.select(...) 保持 int全程无装箱
方法调用开销 高(StreamPipelineSink 多层抽象) 低(直接调用 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(避免装箱)

ECIntObjectHashMapFastUtilInt2ObjectOpenHashMap 在原理上高度相似,都采用开放寻址 + 平行数组。

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 对象

JDKHashMapHashSet 每个元素都封装在一个 Node<K,V>Entry 对象中,带来显著的内存开销(每个对象至少 24–32 字节,含对象头、哈希码、指针等)。

ECUnifiedMap 采用 “平行数组 + 单桶位图” 的紧凑布局:

  • Unified 的含义:统一存储,消除中间对象

  • 键(keys)和值(values)分别存储在两个独立的 Object[] 数组中

     所有 key 存在一个数组所有 value 存在另一个数组
    Object[] keys;     // [key1, key2, key3, ...]
    Object[] values;   // [val1, val2, val3, ...]
    
  • 使用一个 int[]long[] 作为哈希表索引与状态标记(空/占用/删除)

2)原始类型集合:零装箱,原生数组

  • 对于 MutableIntListMutableLongSet 等原始类型集合:
  • 内部直接使用 int[]long[] 等原生数组
  • 完全避免 IntegerLong 包装对象的创建

2、迭代与计算优化

1)高度优化的迭代器

EC 的迭代器(如 MutableList.iterator())经过深度内联优化,减少方法调用开销:

  • 大量方法标记为 final
    • final 方法不能被重写 → 调用目标在编译期就完全确定 → 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)减少指针跳转

JDKLinkedListHashMapNode 链表会导致频繁的 非连续内存跳转,而 EC 的数组式结构避免了这一点。

四、与 FastUtilJDK的对比

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 键 和 空槽必须禁止 null key
  • 删除后留 tombstonekey[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]
    • 若为 0free)→ 插入到 keys[index], values[index]
    • 若为 1occupied)且 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、JDKHashMap

  • 每个键值对封装为 Node<K,V> 对象
  • 对象分散在堆中 → 指针跳转多、缓存不友好
  • 装箱 + 对象头 → 内存膨胀 3–4 倍

b、ECUnifiedMap / MutableIntIntMap

  • 采用 双数组 + 状态标记:虽无 Entry 对象,但通常维护 三个数组
    • keys[] 存储键
    • values[] 存储值
    • states[](或位图)标记槽位状态(空/占用/删除)
  • 1个哈希槽位 = index,但 keys[index], values[index], occupied[index] 是三个独立数组的元素
  • Entry 对象,大幅减少对象数量,但哈希冲突仍通过线性探测或二次探测处理(非开放寻址)

  • 内存访问需跨多个数组 → 缓存行利用率略低
  • 设计更注重通用性与安全性(如支持 null 键/值)

c、FastUtilInt2IntOpenHashMap

  • 采用 开放寻址哈希(Open Addressing)
    • 所有数据连续存放,所有键值直接存入两个连续 int[] 数组
    • 一个哈希槽位 = (key[i], value[i]),冲突时 i → i+1 → i+2...
    • 冲突时通过 线性探测 + 位运算优化的哈希函数 解决
    • 无链表、无对象、无间接引用,
  • 内存布局极致紧凑,CPU 缓存命中率极高

2)EC 为何不采用开放寻址

EC 的设计哲学更偏向 通用性与安全性FastUtil 为性能牺牲了灵活性,EC 为通用性接受了一点性能折衷

  1. 支持 null 键/值FastUtil 的原始类型 Map 不支持 null(因用特殊值如 0 表示空),而 EC 允许(通过 states[] 区分)
  2. 删除更安全:开放寻址中删除需标记为 “deleted”(否则破坏探测链),ECstates[] 天然支持
  3. 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

ContactAuthor