1. 引言

本文将探讨 Project Valhalla 为 Java 生态带来的一项重要特性——值类(Value-Based Classes)。值类自 Java 8 引入,在后续版本中经历了重大重构和增强。对于追求高性能开发的你来说,这个特性值得深入了解。

2. 值类(Value-Based Classes)

2.1. Project Valhalla 简介

Project Valhalla 是 OpenJDK 的实验性项目,旨在为 Java 引入新特性。其核心目标是在保持完全向后兼容的前提下,增强值类型支持、泛型特化及性能优化。

值类是 Project Valhalla 引入的关键特性之一,用于在 Java 中表示原始不可变值,同时避免传统面向对象类带来的额外开销。

2.2. 原始类型与值类型

在正式定义值类前,先回顾 Java 中的两个重要概念:原始类型和值类型。

原始类型 是表示单一值的简单数据类型,非对象。Java 提供八种原始类型:byteshortintlongfloatdoublecharboolean。为支持面向对象操作,Java 为每种原始类型提供了对应的包装类

需注意 Java 会自动执行装箱/拆箱操作,高效转换对象与原始类型:

List<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱

原始类型存储在栈内存,而对象存储在堆内存。

Project Valhalla 引入了一种介于对象和原始类型之间的新类型——值类型。值类型不可变且无身份标识,不支持继承。与原始类型类似,值类型通过值而非引用访问。

2.3. 值类定义

值类是设计用于封装 Java 中值类型的类。JVM 可自由切换值类型与其对应的值类,类似自动装箱/拆箱机制。因此值类也是无身份标识的。

3. 值类的核心特性

值类表示简单不可变值,其特性可归纳为以下几类:

3.1. 不可变性

值类需表示不可变数据,类似 int 等原始类型,具有以下特征:

  • ✅ 类必须声明为 final
  • ✅ 所有字段必须为 final
  • ✅ 只能继承 Object 或无实例字段的抽象类

3.2. 对象创建机制

值类的对象创建需遵循以下规则:

  • ❌ 不声明可访问构造器
  • ⚠️ 若存在构造器,必须标记为 @Deprecated(forRemoval=true)
  • ✅ 仅通过工厂方法实例化,调用方不应假设返回实例的身份(可能是新实例或缓存实例)

3.3. 身份标识与核心方法

值类无身份标识,作为 Java 类需明确 Object 继承方法的行为:

  • equals()hashCode()toString() 实现完全基于实例字段值
  • ✅ 对象相等性仅通过 equals() 判断,禁止使用 ==
  • ✅ 两个相等对象可互换使用,计算结果应完全一致

3.4. 重要注意事项

使用值类时需注意以下限制:

  • ⚠️ equals() 相等的两个对象可能是同一实例或不同实例
  • ❌ 不能确保监视器的独占所有权,不适合用于同步

4. 值类示例

4.1. JDK 中的值类

JDK 中多个类遵循值类规范:

  • Java 8 引入的 java.util.Optional 和日期时间 API(如 java.time.LocalDateTime
  • Java 16+ 将原始类型的包装类(如 IntegerLong)正式定义为值类

这些类带有 jdk.internal.ValueBased 注解:

@jdk.internal.ValueBased
public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc {
    // JDK 中的 Integer 类实现
}

4.2. 自定义值类实战

以三维空间点 Point 为例,创建自定义值类。空间中的特定点具有唯一性,适合用值表示(类似整数 302)。

首先定义 final 类和 final 字段,构造器设为私有:

public final class Point {
    private final int x;
    private final int y;
    private final int z;

    // 私有构造器
    private Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    // ...
}

预创建原点实例 (0,0,0) 并缓存:

private static Point ORIGIN = new Point(0, 0, 0);

提供工厂方法(根据参数返回新实例或缓存实例):

public static Point valueOfPoint(int x, int y, int z) {
    // 返回缓存的原点实例或新实例
    if (isOrigin(x, y, z)) {
        return ORIGIN;
    }
    return new Point(x, y, z);
}

// 判断是否为原点
private static boolean isOrigin(int x, int y, int z) {
    return x == 0 && y == 0 && z == 0;
}

工厂方法强制调用方放弃对对象身份的假设,禁止引用比较。

最后基于字段值实现 equals()hashCode()

@Override
public boolean equals(Object other) {
    if (other == null || getClass() != other.getClass()) {
        return false;
    }
    Point point = (Point) other;
    return x == point.x && y == point.y && z == point.z;
}

@Override
public int hashCode() {
    return Objects.hash(x, y, z);
}

测试相同坐标点的相等性:

@Test
public void givenValueBasedPoint_whenCompared_thenReturnEquals() {
    Point p1 = Point.valueOfPoint(1,2,3);
    Point p2 = Point.valueOfPoint(1,2,3);

    Assert.assertEquals(p1, p2);
}

验证原点实例的缓存行为(⚠️ 实际开发中不应依赖此特性):

@Test
public void givenValueBasedPoint_whenOrigin_thenReturnCachedInstance() {
    Point p1 = Point.valueOfPoint(0, 0, 0);
    Point p2 = Point.valueOfPoint(0, 0, 0);

    // 值类不应假设引用相等
    Assert.assertTrue(p1 == p2);
}

5. 值类的优势

值类作为 Valhalla 规范的一部分仍在演进,当前主要有两大优势:

  1. 内存效率提升
    ✅ 无引用身份标识开销
    ✅ JVM 可灵活复用实例,减少内存占用

  2. 性能优化
    ✅ 无需同步机制,提升多线程应用性能

6. 值类与其他类型的区别

6.1. 与不可变类对比

不可变类(如 StringEnum、旧版包装类)与值类有本质区别:

特性 不可变类 值类
身份标识 ✅ 有(可引用比较) ❌ 无(禁止引用比较)
构造器访问 ✅ 可公开 ❌ 必须私有/工厂方法
复杂行为 ✅ 支持复杂逻辑 ❌ 仅表示简单值
互操作性 ❌ 相同状态对象为不同实例 ✅ 相等对象可互换

核心结论:值类本质上是不可变的,但不可变类不一定是值类。

6.2. 与 Record 对比

Java 14 引入的 Record 与值类看似相似,实则不同:

  • 构造器差异:Record 必须提供公共构造器,值类禁止
  • 设计目标:Record 用于简化数据载体,值类专注于值语义
  • 身份约束: Record 可保留身份标识,值类必须无标识

7. 总结

本文深入探讨了 Java 值类的概念、实现规范及其与不可变类、Record 的区别。作为 Project Valhalla 的核心特性,值类通过消除身份标识开销,为内存敏感型和高并发场景提供了新的优化方向。虽然仍在实验阶段,但其设计理念值得在性能关键场景中尝试应用。

本文代码示例可在 GitHub 获取。


原始标题:Value-Based Classes in Java | Baeldung