Java常用类库
前言
Github:https://github.com/HealerJean
JavaUtil包:http://gitbook.net/java/util/index.html
一、FastUtil:高性能 Java 集合库
1、FastUtil 是什么
FastUtil 是由意大利米兰大学(University of Milan)开发的开源高性能集合库,专为处理 原始类型(
primitive types) 的集合和映射而设计。
-
核心目标:避免装箱、减少内存、提升缓存局部性、加速 CPU 访问
-
GitHub: https://github.com/vigna/fastutil -
Maven:<dependency> <groupId>it.unimi.dsi</groupId> <artifactId>fastutil</artifactId> <version>8.5.13</version> </dependency>
2、为什么需要 FastUtil?—— Java 标准集合的痛点
1)泛型不支持基本类型
Java 泛型只接受引用类型,所以必须用 Integer、Long 等包装类:
Map<Integer, Integer> map = new HashMap<>();
map.put(123, 456); // 自动装箱 → Integer.valueOf(123)
2)自动装箱/拆箱带来严重开销
| 问题 | 描述 |
|---|---|
| GC 压力大 | 每次操作都可能创建临时对象(如 Integer.valueOf()) |
| 缓存局部性差 | 对象分散在堆中,CPU 缓存命中率低 |
| 内存膨胀 | 一个 Integer 对象 ≈ 16~24 字节(含对象头),而 int 仅需 4 字节 |
3)内存占用对比
节省 75%+ 内存,性能提升 2~4 倍
| 场景 | 数据量 | 内存占用 |
|---|---|---|
HashMap<Integer, Integer> |
1 亿条 (int, int) | ≥ 3.2 GB |
Int2IntOpenHashMap |
1 亿条 (int, int) | ≈ 800 MB |
二、FastUtil 的底层优化原理
1、开放寻址哈希
1)JDK HashMap(链地址法)
- 使用
Node<K,V>[] table - 每个节点是一个独立对象(含 next 指针、hash、key、value)
- 冲突时挂链表 → 超过阈值转红黑树
2)FastUtil(开放寻址 + 线性探测)
-
所有键值对存储在两个平行数组中:
int[] key; // 存原始类型 key Object[] value; // 存 value(可为对象或原始类型) -
无
Entry对象 → 内存紧凑 -
冲突处理:线性探测(
Linear Probing)- 探测序列:
(h(k) + i) & mask(i = 0,1,2,…) - 使用位运算
& mask替代% length(快 10 倍+)
- 探测序列:
-
删除策略:使用 墓碑标记(Tombstone)
- 删除时打标记,不移动后续元素 → O(1) 删除
- 真正清理发生在:
- 扩容(rehash)
- 显式调用
trim()/compact() - 删除比例过高时自动压缩
- 优点:删除快;
- 缺点:可能浪费内存(大量 tombstone)
2、高效哈希函数
-
对
int/long使用 位混合(bit spreading),例如:static int mix(int x) { x = (x ^ (x >>> 16)) * 0x45d9f3b; x = (x ^ (x >>> 16)) * 0x45d9f3b; return x ^ (x >>> 16); } -
减少哈希聚集(clustering)
-
默认负载因子 0.75(可配置),平衡速度与内存
3、无泛型、无装箱
-
所有方法直接操作原始类型:
public int put(int key, int value); public boolean containsKey(int key); public int get(int key); -
零装箱 → 无临时对象 → 无 GC 压力
三、关键类型详解(Map / Set / List)
1、Map (核心优势区)
| Key 类型 | Value 类型 | FastUtil 类名 | 典型使用场景 |
|---|---|---|---|
int |
int |
Int2IntOpenHashMap |
ID → 计数、状态码、分数 |
int |
long |
Int2LongOpenHashMap |
用户ID → 时间戳、金额(分) |
int |
double |
Int2DoubleOpenHashMap |
商品ID → 价格、评分 |
int |
Object |
Int2ObjectOpenHashMap<V> |
ID → 实体对象(User, Product 等) |
long |
int |
Long2IntOpenHashMap |
时间戳 → 状态、分类ID |
long |
long |
Long2LongOpenHashMap |
大ID映射(如分布式ID → 另一个ID) |
long |
Object |
Long2ObjectOpenHashMap<V> |
订单号(long)→ Order 对象 |
int |
boolean |
Int2BooleanOpenHashMap |
权限位(userID → 是否管理员) |
byte |
int |
Byte2IntOpenHashMap |
小范围枚举 → 计数(如 HTTP 状态码) |
short |
int |
Short2IntOpenHashMap |
年份、月份等小整数 → 统计值 |
2、Set
| 元素类型 | FastUtil 类 | 替代方案 | 优势 |
|---|---|---|---|
int |
IntOpenHashSet |
HashSet<Integer> |
内存省 75%,查询快 3 倍 |
long |
LongOpenHashSet |
HashSet<Long> |
适合 Snowflake ID 去重 |
short |
ShortOpenHashSet |
HashSet<Short> |
小整数集合高效存储 |
3、List
| 类型 | 底层数组 | 用途 |
|---|---|---|
IntArrayList |
int[] |
存储大量整数(如 ID 列表) |
LongArrayList |
long[] |
时间戳列表、大 ID 列表 |
DoubleArrayList |
double[] |
数值计算、指标序列 |
四、Int2ObjectOpenHashMap<V>
1、什么是 Int2ObjectOpenHashMap
Int2ObjectOpenHashMap是FastUtil库(一个高性能Java集合框架)提供的专门用于将int类型键映射到对象值 的哈希表实现。它属于FastUtil中的 “primitivemaps”(原始类型映射)家族,专为提升性能和减少内存占用而设计。
- 包路径:
it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap - 泛型形式:
Int2ObjectOpenHashMap<V> - 用途:高效地将
int键映射到任意对象类型V。
2、底层结构
1)关键结构
int[] key; // 存 int 键(原始类型)
Object[] value; // 存 V 值(引用类型)
2)构造函数关键逻辑
- 默认负载因子:
0.75f(与 JDK 一致) - 初始容量计算:
HashCommon.arraySize(expected, f)→ 返回 ≥expected / f的最小 2 的幂 - 索引计算:
hash & mask(mask = capacity - 1)→ 比%快 10 倍以上 - 扩容阈值:
maxFill = capacity * loadFactor
public Int2ObjectOpenHashMap(int expected, float f) {
// 负载因子合法性校验:if (!(f <= 0.0F) && !(f >= 1.0F))
// 逻辑等价:0 < f < 1(负载因子必须在 0 到 1 之间)。
if (!(f <= 0.0F) && !(f >= 1.0F)) {
if (expected < 0) {
throw new IllegalArgumentException("The expected number of elements must be nonnegative");
} else {
this.f = f;
// 核心中的核心:计算哈希表的初始容量(2 的幂)。
this.minN = this.n = HashCommon.arraySize(expected, f);
// 计算索引掩码(比如 n=4096 → mask=4095)
// 后续用hash & mask替代hash % n计算索引(位运算比取模快 10 倍 +)。
this.mask = this.n - 1;
// 计算扩容触发阈值(容量 × 负载因子)。
this.maxFill = HashCommon.maxFill(this.n, f);
this.key = new int[this.n + 1];
this.value = new Object[this.n + 1];
}
} else {
throw new IllegalArgumentException("Load factor must be greater than 0 and smaller than 1");
}
}
3、核心原理
1)基于开放寻址
与
Java标准库中的HashMap(使用链地址法 + 红黑树)不同,Int2ObjectOpenHashMap使用 开放寻址(open addressing) 解决哈希冲突:
- 所有键值对存储在一个连续的数组中。
- 当发生冲突时,通过探测(如线性探测或二次探测)寻找下一个空槽。
FastUtil默认使用 线性探测(linear probing)。
a、开放寻址 核心思想
开放寻址 是一种解决哈希冲突的策略。它的核心思想是:
所有键值对都存储在哈希表本身的底层数组中(而不是像链地址法那样使用链表或树等额外结构)。当发生哈希冲突(即多个键映射到同一个索引)时,算法会在数组中“探测”(
probe)其他位置,直到找到一个空槽来存放新元素。
| 特性 | 开放寻址(如 Int2ObjectOpenHashMap) |
链地址法(如 java.util.HashMap) |
|---|---|---|
| 存储结构 | 所有元素存在一个连续数组中 | 数组 + 链表/红黑树(桶) |
| 内存局部性 | 更好(缓存友好) | 较差(指针跳转) |
| 删除操作 | 复杂(需标记“已删除”) | 简单(直接移除节点) |
| 负载因子上限 | 通常 < 1(如 0.75) | 可 > 1(理论上无硬上限) |
| 空间开销 | 低(无额外指针) | 高(每个节点有 next 指针等) |
b、插入(put)
-
哈希函数:对于键
k,先计算其哈希值h(k),然后通过取模得到初始索引:index = h(k) % table.length -
线性探测公式:探测序列:
(h(k) + i) % table.length,其中i = 0, 1, 2, ... - 如果
table[index]为空 → 直接插入。 - 如果已被占用(冲突)→ 线性探测:依次检查
index+1,index+2, …, 直到找到空槽。 - 若整个表快满(负载因子过高),则触发扩容(rehash)。
c、查找(get)
- 从
h(k) % len开始,按相同探测顺序查找。 - 遇到空槽 → 键不存在(因为插入时会填满所有可能位置)。
- 遇到匹配键 → 返回值。
d、删除(remove):这是开放寻址的难点!
-
FastUtil中通过特殊键值(如0对于int键,或null对于对象)结合状态位数组或哨兵值来高效处理删除。 - 不能简单置
null,否则会打断后续元素的查找路径。 - 解决方案:使用特殊标记(如
TOMBSTONE/ “墓碑”)表示“此处曾有数据但已被删除”。 - 查找时跳过墓碑,插入时可复用墓碑位置。
2)原始类型优化
- 键是
int(不是Integer),避免了自动装箱/拆箱(autoboxing/unboxing)。 - 内部数组直接使用
int[]存储键,Object[]存储值。 - 减少了对象创建和
GC压力,提升缓存局部性。
3)高效哈希与内存布局
a、高效哈希
- 对
int键使用高效的混合哈希(如MurmurHash变种),减少聚集。 - 支持自定义初始容量和负载因子。
b、内存布局紧凑
- 没有
Entry对象(不像HashMap中每个键值对是一个Node对象)。 - 底层由两个平行数组组成:
- 一个
int[] key数组(存储键) - 一个
Object[] value数组(存储值)
- 一个
- 内存占用远小于
HashMap<Integer, V>,尤其在大数据量下优势明显。
4、对比 HashMap<Integer, V>
结论:只要无 null key 需求,一律用
Int2ObjectOpenHashMap
| 特性 | Int2ObjectOpenHashMap<V> |
HashMap<Integer, V> |
|---|---|---|
| 键类型 | 原始 int |
装箱 Integer |
| 内存占用 | 极低(无 Entry 对象,无 Integer 对象) |
较高(每个键是一个 Integer 对象 + Node 对象) |
| 内存节省 40%~70% | ||
| 性能 | 更快(无装箱、缓存友好、开放寻址) | 较慢(装箱开销、指针跳转多) |
| 快 2~4 倍 | ||
| 线程安全 | 非线程安全 | 非线程安全 |
| 哈希冲突处理 | 开放寻址(线性探测) | 链地址法 + 红黑树(JDK8+) |
支持 null key |
不支持(会抛异常) | 支持 |
支持 null value |
支持 | 支持 |
1)对比性能(速度)
1)Int2ObjectOpenHashMap:
- 避免
Integer装箱/拆箱(zero boxing) - 使用开放寻址(open addressing)而非链表/红黑树
- 实测比
HashMap<Integer, V>快 2~4 倍(尤其在get/put高频操作时)
2)HashMap<Integer, V>:
- 每次
put(123, obj)都会Integer.valueOf(123)(装箱) - 每次
get(123)也会装箱 → 额外对象创建 + GC 压力
3)示例(300 万条数据,1000 万次查询):
-
HashMap: ~1765 ms -
Int2ObjectOpenHashMap: ~602 ms -
Int2IntOpenHashMap(value 也是 int): ~552 ms
2)对比内存占用
1)Int2ObjectOpenHashMap:
Key直接存int(4 字节),无Integer对象头(每个对象约 16~24 字节)- 内存节省 40%~70%
2)HashMap<Integer, V>:
- 每个
key都是一个Integer对象 → 大量小对象 → 堆膨胀 + GC 频繁
3)举例:存 1000 万个 <Integer, Object>:
HashMap:约 1.1 GBInt2ObjectOpenHashMap:约 320 MB
3)场景推荐
只要
key是原始int,且无nullkey需求,默认就用Int2ObjectOpenHashMap—— 它是“免费的性能提升”。
| 维度 | 推荐 |
|---|---|
| 性能 & 内存 | Int2ObjectOpenHashMap |
| 易用性 & 兼容性 | HashMap |
| 生产级高性能系统 | 强烈推荐 Int2ObjectOpenHashMap |
五、Object2ObjectOpenHashMap
1、FastUtil、EC、JDK
| 维度 | JDK |
EC |
FastUtil |
|---|---|---|---|
| 集合类 | HashMap |
UnifiedMap |
Object2ObjectOpenHashMap |
是否支持 null 键/值 |
支持 | 支持 | 不支持 null key(抛异常) |
| 内存效率(100万条) | 最差(~70 MB) | 中等(~48 MB) | 略优(~44 MB) |
| 小数据性能(<1k) | 最快 | 中等 | 略慢 |
| 大数据性能(>100k) | 稳定 | 良好 | 依赖哈希质量(好则快,差则崩) |
GC 压力 |
高(百万 Node 对象) |
低(仅数组) | 低(仅数组) |
| API 丰富度 | 基础 | ⭐ 极丰富(函数式、不可变等) | 贫乏(仅基础操作) |
| 工程友好性 | 极高(标准库) | 高(主流库) | 中(需处理 null/异常) |
┌───────────────────────┐
│ JDK HashMap │
│ - 指针跳转 │
│ - Entry 对象开销大 │
│ - 红黑树兜底 │
└──────────┬────────────┘
│ 安全、通用、支持 null
▼
┌───────────────────────┐ ┌───────────────────────┐
│ EC UnifiedMap │ │ FastUtil O2O │
│ - 三数组 │ │ - 双数组 │
│ - states[] 精确管理 │ │ - tombstone 陷阱 │
│ - 支持 null │ │ - 不支持 null key │
└──────────┬────────────┘ └──────────┬────────────┘
│ 平衡之选 │ 高风险高回报(但回报有限)
▼ ▼
[推荐用于大多数对象场景] [仅限受控高性能场景]
2、性能对比
1)场景 1:小数据(< 1,000 条)
JDK最快:JIT 对HashMap高度优化,内联率高EC/FastUtil略慢:方法调用链更长,边界检查多
2)场景 2:大数据插入/查询(100k+,哈希分布均匀)
FastUtil≈EC>JDKFastUtil缓存局部性最好(仅两数组)EC因三数组,跨缓存行访问略慢JDK因指针跳转和对象分配,最慢
3)场景 3:哈希冲突严重(如所有 key 的 hashCode() 相同)
JDK最稳:自动转红黑树,O(log n)EC/FastUtil崩溃:探测链极长,O(n) 性能雪崩
4)场景 4:频繁删除 + 插入
EC最佳:states[]精确标记,可复用 removed 槽FastUtil最差:tombstone 累积,负载因子虚高,触发不必要扩容JDK中等:链表删除 O(1),但对象仍需 GC
六、ObjectArrayList
1、基本定位
1)ObjectArrayList<E> 基础
-
全限定类名:
it.unimi.dsi.fastutil.objects.ObjectArrayList<E> -
继承关系:
public class ObjectArrayList<E> extends AbstractList<E> implements RandomAccess, Cloneable, java.io.Serializable -
作用:
FastUtil对java.util.ArrayList<E>的高性能替代实现,专用于存储对象引用(非原始类型)。
2)内部结构
- 懒初始化:默认构造函数设置
a = ObjectArrays.DEFAULT_EMPTY_ARRAY(共享空数组) - 非线程安全:所有操作无同步
- 序列化支持:通过
writeObject/readObject自定义序列化
public class ObjectArrayList<E> extends AbstractList<E> {
protected transient E[] a; // 底层数组(注意:不是 Object[],而是 E[])
protected int size; // 当前元素数量
}
3)为什么需要 ObjectArrayList?
说明:虽然不节省内存(仍存对象引用),但相比 JDK ArrayList,它在以下方面更优:
注意:这些优势主要体现在高频操作、算法场景或已引入 FastUtil 的项目中。
| 优化点 | 说明 |
|---|---|
| 更激进的扩容策略 | newCap = old + old/2 + 1,避免小容量扩容停滞 |
| 高效批量操作 | 提供 addElements(...), removeElements(from, to) 等方法 |
| 内置函数式方法 | 如 forEach, removeIf, select(Predicate)(FastUtil 特有) |
| 统一 API 风格 | 与 IntArrayList 等保持一致,便于混合使用 |
| 减少临时对象 | 在特定路径(如 forEach)中避免创建 Iterator/Stream |
| 集合类型 | 优势 | 适用场景 |
|---|---|---|
ObjectArrayList |
高效 API、更好扩容、批量操作 | 对象列表 + 高性能需求 |
Int/Long/...ArrayList |
零装箱、省内存、低 GC | 数值计算、大数据处理 |
2、核心操作
| 操作 | 实现 | 时间复杂度 |
|---|---|---|
add(E e) |
尾部追加,必要时扩容 | O(1) amortized |
add(int i, E e) |
插入,后移元素 | O(n) |
get(int i) |
直接索引访问 + 边界检查 | O(1) |
remove(int i) |
System.arraycopy 前移 |
O(n) |
removeElements(from, to) |
批量删除,一次 arraycopy |
O(n) |
forEach(Consumer) |
for 循环遍历,无迭代器 | O(n),低 GC |
1)添加元素(add(T element))
- 直接写入
items[size++] - 若容量不足,触发扩容 → 复制数组
- 无同步,非线程安全
2)随机访问(get(int index))
- 直接返回
items[index] - 有边界检查(
if (index < 0 || index >= size) throw ...) - 时间复杂度:O(1)
3)删除元素(remove(int index))
- 将
index后所有元素前移一位(System.arraycopy) - 时间复杂度:O(n)
4)函数式操作(如 select, collect)
-
内部使用快速循环(fast enumeration),避免创建中间迭代器或 Stream 对象
-
示例:
list.select(x -> x > 10); // 返回新的 ObjectArrayList,过滤高效 -
比
JDK的Stream.filter().collect()更少GC压力,尤其在小数据集上更快
3、对比 ArrayList
| 维度 | ObjectArrayList |
ArrayList(JDK) |
|---|---|---|
| 所属库 | FastUtil(第三方) | java.util(标准库) |
| 底层数组 | Object[] a |
transient Object[] elementData |
| 默认构造 | 懒初始化(空数组) | 懒初始化(JDK 8~16 会预分配 10,17+ 懒初始化) |
| 扩容公式 | old + (old >> 1) + 1 |
old + (old >> 1) |
| 特有方法 | addElements, removeElements, select |
无 |
| 迭代性能 | 使用 forEach 等可避免中间对象 |
标准 iterator() 会创建对象 |
| 生态兼容性 | 实现 List<E>,可无缝互操作 |
完全兼容所有框架 |
| 适用场景 | 高性能计算、游戏引擎、金融系统 | 通用业务开发 |
1)使用建议
1)推荐使用 ObjectArrayList 的场景
- 已引入
FastUtil,希望统一体验(如同时用IntArrayList) - 需要频繁调用
removeElements(from, to)等批量操作 - 在性能敏感循环中使用
forEach避免GC - 对小容量扩容行为有确定性要求(如算法竞赛)
2)不推荐场景
- 简单
CRUD业务,无性能瓶颈 - 无法引入第三方依赖
- 强依赖
JDK原生行为(如某些反射框架)
2)杀手锏:原始类型特化
虽然
ObjectArrayList用于对象,但FastUtil的最大优势在于对基本类型的直接支持。
- 零装箱/拆箱:直接操作基本类型,不创建
Integer等包装对象 - 内存节省:
int占 4 字节,Integer对象至少 16 字节(含对象头) -
GC友好:减少堆对象数量,降低GC频率和停顿时间 - 实测:存储 1000 万个整数
ArrayList<Integer>:约占用160 MB+ 高频 GCIntArrayList:仅占用 40 MB + 几乎无GC
| 基本类型 | 特化列表类 | 底层数组 |
|---|---|---|
int |
IntArrayList |
int[] |
long |
LongArrayList |
long[] |
double |
DoubleArrayList |
double[] |
boolean |
BooleanArrayList |
boolean[] |
3)IntArrayList
存储大量基本类型数据时,
IntArrayList的内存占用仅为ArrayList<Integer>的约 20%,节省高达 80% 的堆内存,并极大降低 GC 压力。
| 方式 | 底层结构 | 对象数量 | 堆内存估算 | GC 压力 |
|---|---|---|---|---|
ArrayList<Integer> |
Object[1_000_000] |
≈ 1,000,001 个 | ≥ 160 MB | 高 |
IntArrayList |
int[1_000_000] |
1 个(只有 IntArrayList 自身) |
≈ 40 MB | 极低 |
5、FQA
1)空间效率:trim 的核心价值
- 行为与
JDK几乎相同:a = Arrays.copyOf(a, size) - 价值:在
size << capacity时释放内存 - 典型场景:从大文件读取后过滤,保留少量结果 → 调用
trimToSize()可节省 90%+ 内存
| 操作 | 本质 | 适用时机 |
|---|---|---|
| 不 trim | 保留扩容空间,避免未来拷贝 | 列表还会增长 |
| trim | 牺牲一次拷贝,永久节省内存 | 列表已定型 + 长期持有 |
a、显著收益:
一个长期驻留的缓存列表(capacity=100万, size=1万)
- 不
trim:持续占用4MB trim后:仅占40KB→ 节省99%内存,集合的内部存储空间通常会缩小,GC压力大幅下降
| 场景 | 效果 |
|---|---|
| 减少堆内存占用 | 直接释放未使用的数组空间 |
降低 GC 频率 |
年轻代压力减小,Minor GC 更少 |
减少 GC 停顿时间 |
GC 扫描的对象图更小 |
| 避免内存碎片 | 尤其对 G1、ZGC 等区域化 GC 友好 |
| 提升系统稳定性 | 降低 OOM 风险,尤其在容器化环境(如 Docker 内存限制 |
b、什么时候 trim
trim牺牲一点时间,换大量空间 → 在内存受限或长期持有场景下,整体效率更高。
| 方面 | ArrayList.trimToSize() |
ObjectArrayList.trimToSize() |
|---|---|---|
| 拷贝开销 | Arrays.copyOf(elementData, size) |
Arrays.copyOf(items, size) |
| 时间效率 | 几乎相同 | 几乎相同 |
| 空间效率 | 相同 | 相同 |
c、什么时候不 trimToSize()
| 场景 | 原因 |
|---|---|
| 列表还会继续增长 | trim 后下次 add 又要扩容 + 拷贝,得不偿失 |
| 临时局部变量 | 方法结束就回收,没必要优化 |
| 性能极度敏感的循环内 | Arrays.copyOf 有 CPU 开销,避免在 hot path 调用 |
2)扩容策略
-
为什么要
+1?考虑oldCapacity = 1: -
ArrayList:1 + (1>>1) = 1 + 0 = 1→ 容量没变!不够用 → 实际会强制扩到minCapacity(如 2) -
ObjectArrayList:1 + 0 + 1 = 2→ 直接满足需求
| 特性 | ArrayList(Java) |
ObjectArrayList |
|---|---|---|
| 扩容公式 | old + old/2 |
old + old/2 + 1 |
| 容量=1 时 | 可能停滞(1→1) | 一定增长(1→2) |
| 设计倾向 | 通用、保守 | 高性能、防边界陷阱 |
| 适用场景 | 一般应用 | 算法、高频操作、小容器 |


