Ohhnews

分类导航

$ cd ..
DZone Java原文

探索Java 25的若干语言增强特性

#java 25#语言新特性#语法简化#模式匹配#并发与安全

尽管Java 26在今年3月中旬已经发布,但Java 25是目前最新的LTS(长期支持)版本,因此我选择优先关注它。无论Java 25的某些语言改进是否仍以预览功能的形式提供,本文都将简要概述其中的几个。主要目的是首先让开发人员意识到,Java正通过其API贡献者不断优化和演进;其次,是激发大家深入探索这些增强功能的好奇心和兴趣。

在JDK 25提出的众多功能中[资源1],本文简要探讨了以下五个语言增强点:视频10音频5音频6访问广告商网站前往页面

  • JEP 512 – 紧凑源文件和实例main方法
  • JEP 513 – 灵活构造函数体
  • JEP 507 – 模式、instanceofswitch中的原始类型
  • JEP 506 – 作用域值
  • JEP 502 – 稳定值

紧凑源文件和实例main方法 (JEP 512)

该功能最初在JDK 21中作为JEP 445“未命名类和实例main方法”提出,随后根据反馈在接下来的版本中逐步改进,并于JDK 25中最终确定。目标很明确:简化Java的入口点,方便初学者和小程序使用——减少样板代码和仪式感——同时与标准Java语言和工具链完全兼容。

假设我们想快速编写一个小程序,实现以下功能:

  • 提示用户并循环读取输入
  • 如果用户输入exit(不区分大小写),则打印"Goodbye!"并结束
  • 否则,打印输入字符串的长度

代码直接放在包中的一个名为CompactSourceFile.java的文件中,内容如下:

$ java
static final String EXIT = "exit";

String prompt(String exit) {
    return "Enter a string (or '" + exit + "' to quit): ";
}

void main() {
    while (true) {
        String input = IO.readln(prompt(EXIT));
        if (EXIT.equalsIgnoreCase(input)) {
            IO.println("Goodbye!");
            break;
        }
        IO.println("Length: " + input.length());
    }
}

简洁明了——没有类声明,只有目标简单的代码段。运行并给出几个提示后,输出符合预期:

Enter a string (or 'exit' to quit): joke
Length: 4
Enter a string (or 'exit' to quit): meeting
Length: 7
Enter a string (or 'exit' to quit): exit
Goodbye!

几点值得注意:

  • 不再需要显式的类声明
  • 尽管不可见,编译器会隐式声明一个final类,且属于未命名包
  • 传统的public static void main(String[] args)被替换为一个更简单的实例方法,它明确作为程序入口点
  • 程序入口点仍需命名为main(),因为JVM会查找这样一个可启动的方法
  • 所有字段和方法都属于隐式类,与常规情况一致
  • 简单程序直接聚焦于其目的,无需额外细节
  • 这是实验性的,也很直接。但如果演变成真正的应用程序,建议保留面向对象结构及所有已知的最佳实践

灵活构造函数体 (JEP 513)

在JDK 25之前,构造函数有一条明确规则:在super()this()调用之前不能编写任何语句。为了提升表达力和可读性,JEP 513放宽了这一限制,而现有代码依然能编译和正常运行,并且对象的安全性100%得到保留。

在Java中,当构造一个对象实例时,会发生两个阶段:一个在前,一个在后;构造函数链的层次结构开始执行。在前一个阶段,内存被分配,实例字段被初始化;在后一个阶段,一旦this()super()调用完成,对象的其余部分基本构造完成。这个过程主要出于安全考虑,确保继承的对象部分在子类相关代码运行之前完全初始化。Joshua Bloch在《Effective Java》中已经建议防止this引用“过早”逃逸。结果就是:对象在任何时刻都不会处于部分构造状态。

简单来说,从Java 25开始,现在允许在构造函数体内的this()super()之前执行语句,同时内部不会在对对象核心安全构建方面做出任何妥协。

注意:

  • 允许的语句——仅那些不依赖实例状态且保证安全的语句:
    • 操作栈上声明的局部变量
    • 构造函数参数验证
  • 语法更加宽松,对象安全性得以保留

让我们看一个小例子,其中通过近似长度和轮子数量来简单建模一个Car,其中长度继承自超类Vehicle

$ java
static class Vehicle {
    private final long length;

    Vehicle(long length) {
        if (length < 0) {
            throw new IllegalArgumentException("Length must be positive");
        }
        this.length = length;
    }

    Vehicle(double length) {
        long round = Math.round(length);
        this(round);
    }

    public long length() {
        return length;
    }
}

static class Car extends Vehicle {
    private final int wheels;

    Car(double length, int wheels) {
        if (wheels < 0) {
            throw new IllegalArgumentException("Wheels must be positive");
        }
        super(length);
        this.wheels = wheels;
    }

    public int wheels() {
        return wheels;
    }
}

void main() {
    var car = new Car(4.6d, 4);
    IO.println("Car is about " + car.length() + " meters long and has " + car.wheels() + " wheels.");
}

运行后,输出为——Car is about 5 meters long and has 4 wheels。首先观察到,Vehicle#length首先被四舍五入(因为存储为long值,第13行),然后传递给另一个构造函数。其次,在调用超类构造函数之前(第30行),对wheels进行了验证,然后赋值。

现在用记录(record)建模一个摩托车:

$ java
record Moto(long length, int wheels) {
    Moto {
        if (length < 0) {
            throw new IllegalArgumentException("Length must be positive");
        }
        if (wheels < 0) {
            throw new IllegalArgumentException("Wheels must be positive");
        }
    }

    Moto(double length, int wheels) {
        long round = Math.round(length);
        this(round, wheels);
    }
}

void main() {
    var moto1 = new Moto(3, 2);
    IO.println("Moto 1 is about " + moto1.length() + " meters long and has " + moto1.wheels() + " wheels.");
    var moto2 = new Moto(2.1d, 2);
    IO.println("Moto 2 is about " + moto2.length() + " meters long and has " + moto2.wheels() + " wheels.");
}

在Java 25之前,规范记录构造函数中允许参数验证(第2行),现在扩展到非规范构造函数(第12行),此外还允许this()调用。如果运行,moto1仅使用规范构造函数构造,而moto2同时使用两个构造函数,输出如下:

Moto 1 is about 3 meters long and has 2 wheels.
Moto 2 is about 2 meters long and has 2 wheels.

关于enum,考虑以下实验代码:

$ java
enum Bike {
    CITY(12), MOUNTAIN("10");

    private final int weight;

    Bike(int weight) {
        if (weight < 0) {
            throw new IllegalArgumentException("Weight must be positive");
        }
        this.weight = weight;
    }

    Bike(String description) {
        int weight = Integer.parseInt(description);
        this(weight);
    }

    public int weight() {
        return weight;
    }
}

void main() {
    IO.println("Bike is " + Bike.MOUNTAIN.weight() + " kg heavy.");
}

虽然像第一个构造函数中的验证在Java 25之前就允许,但现在也允许在调用this()之前进行额外操作。

总之,在classrecordenum级别,构造函数的编写方式得到了净化和改进,同时对象安全性依然得到完全保留,没有任何妥协。

模式、instanceof和switch中的原始类型 (JEP 507)

通常,模式匹配是一种语言机制,它将几个步骤组合成一个功能,便于测试某个值。重点在于检查的内容,而不一定是检查的手段。除了在instanceofswitch结构中使用模式匹配之外,Java 25还允许将其与原始类型一起使用——byteshortintlongfloatdoublecharboolean现在都纳入此模型。引用类型的限制现在被扩展,使该功能更加统一和直观,因为可应用的限制显著减少。

考虑以下示例:

$ java
void main() {
    Number doubleBoxed = 3.99;
    if (doubleBoxed instanceof int i) {
        IO.println("'num' fits in int: " + i);
    } else {
        IO.println("'num' does NOT fit losslessly in int (value=" + doubleBoxed + ")");
    }

    IO.println(describe(Byte.MAX_VALUE));
    IO.println(describe(Short.MAX_VALUE));
    IO.println(describe(42));
    IO.println(describe(Integer.MAX_VALUE));
    IO.println(describe(Long.MAX_VALUE));
    IO.println(describe(3.14f));
    IO.println(describe(2.718281828459045));
}

static String describe(Number n) {
    return switch (n) {
        case byte b -> n + " fits in byte → " + b;
        case short s -> n + " fits in short → " + s;
        case int i -> n + " fits in int → " + i;
        case long l -> n + " fits in long → " + l;
        case float f -> n + " fits in float → " + f;
        case double d -> n + " fits in double → " + d;
        case null, default -> n + " unknown numeric type";
    };
}

如果运行,会产生以下输出:

'num' does NOT fit losslessly in int (value=3.99)
127 fits in byte → 127
32767 fits in short → 32767
42 fits in int → 42
2147483647 fits in int → 2147483647
9223372036854775807 fits in long → 9223372036854775807
3.14 fits in float → 3.14
2.718281828459045 fits in double → 2.718281828459045

注意:

  • describe()允许轻松地将一个Number描述为最适合它的最紧凑类型(第19行)
  • 一个Number引用现在可以直接模式匹配到原始类型(第20行)
  • 该功能支持安全、无损的缩小检查,无需手动转换或范围检查

深入探索,我个人认为这个功能有趣的地方在于深层嵌套模式。下面的例子允许内省对象并直接匹配内容:

$ java
record Age(int years) {}
record Wine(String name, Age age) {}

void analyze(Object value) {
    IO.println("Analyzing - " + value);
    if (value instanceof Wine(String name, Age(int years))) {
        IO.println("Wine: " + name + " (" + years + " years old)");
    } else {
        IO.println("Not a wine");
    }
}

void main() {
    var value1 = new Wine("Merlot", new Age(10));
    analyze(value1);
    var value2 = "Cabernet Sauvignon";
    analyze(value2);
}

如果运行,结果很明显,但代码干净、简洁且极具表现力:

Analyzing Wine[name=Merlot, age=Age[years=10]]
Wine: Merlot (10 years old)
Analyzing Cabernet Sauvignon
Not a wine

总之,从Java 25开始,就模式匹配功能的当前状态而言,代码整体上变得更干净、更安全。

作用域值 (JEP 506)

随着Project Loom引入虚拟线程,这为另一个增强功能留出了空间——在更结构化、可预测且更安全的方式下,在线程之间和跨线程传递不可变上下文。ScopedValues是Java 25中的一个最终确定功能,允许在精确的执行作用域边界内实现这一点。

为了更好地理解它们,我们来看下面的简单例子:

$ java
static final ScopedValue<User> USER = ScopedValue.newInstance();

record User(int id, String name) {}

static void handleFurther() {
    IO.println("handleFurther - start for " + USER.get());
    ScopedValue.where(USER, new User(2, "AD"))
               .run(() -> {
                   IO.println("handleFurther - something specific for " + USER.get());
               });
    IO.println("handleFurther - finished for " + USER.get());
}

static void handle() {
    IO.println("handle - start for " + USER.get());
    handleFurther();
    IO.println("handle - finished for " + USER.get());
}

void main() {
    ScopedValue.where(USER, new User(1, "HCD"))
               .run(() -> {
                   IO.println("main - before handling - " + USER.get());
                   handle();
                   IO.println("main - after handling - " + USER.get());
               });
    //handle();
}

共享的User的存储位置首先被创建为USER。在执行过程中传递的上下文(而不是作为所调用方法的参数)是User实例。它可被视为“当前”用户。一旦实例被绑定(第23行),其作用域在main()方法中明确定义,并贯穿整个执行过程——传递给handle(),再进一步传递给handleFurther()。访问是只读的;不能更改。如果在执行流程中重新设置,如在handleFurther()中,会创建一个新的(嵌套)子作用域,一旦该子作用域结束,之前的外部作用域将继续。

如果运行,代码会生成以下输出,这更清楚地说明了上述内容:

main - before handling - User[id=1, name=HCD]
handle - start for User[id=1, name=HCD]
handleFurther - start for User[id=1, name=HCD]
handleFurther - something specific for User[id=2, name=AD]
handleFurther - finished for User[id=1, name=HCD]
handle - finished for User[id=1, name=HCD]
main - after handling - User[id=1, name=HCD]

如果在作用域外调用handle()(第30行)并重新运行代码,到达该点时会抛出一个明确的异常——Exception in thread "main" java.util.NoSuchElementException: ScopedValue not bound

关键点:

  • where(...).run(...) —— 为lambda的执行期间绑定值,然后自动解除绑定——无需手动清理。
  • 作用域内不可变 —— 一旦绑定,无法更改(但可以在嵌套作用域中重新绑定)。
  • 虚拟线程下开销小 —— 无需复制,只是一个引用。
  • 易于推理 —— 值始终是当前作用域顶部绑定的那个。
  • ThreadLocal的良好替代品,后者生命周期无限、可变且难以推理,因为其值可以在调用栈的任何位置更改。
  • 与结构化并发(JEP 505)完美配合 —— 子任务自动共享父级的作用域值而无需复制。

总之,作用域变量极大地促进了并发的清晰性和安全性,并有助于防止诸如内存泄漏或陈旧数据泄露等问题。

稳定值 (JEP 502)

我认为这个增强功能强制实现了有效的不变性——无论是在实例级别还是对象级别。如果在Java 25之前我们创建了一个实例,将其声明为final,初始化它,并记录它应保持不变,但实际情况有时不同,因为实例的某些“内容”仍然可变。StableValue功能允许通过所有手段构造不可变实例,以便一旦初始化,对象内容也保证保持不变。

StableValues是一种JVM增强功能,提供了一种实现线程安全和深度不变性的方法,是结合锁、同步、volatile变量和Atomic引用的替代方案。其行为是天生线程安全的,这是由JVM对StableValues的内部处理确保的。

我们来检查以下代码:

$ java
static class User {
    private final StableValue<String> id = StableValue.of();
    private final String name;

    public User(String name) {
        this.name = name;
    }

    public String id() {
        return id.orElseSet(() -> UUID.randomUUID().toString());
    }

    public String name() {
        return name;
    }

    @Override
    public String toString() {
        return name + " (" + id() + ")";
    }
}

private record Task(CountDownLatch latch, Runnable runnable) implements Runnable {
    @Override
    public void run() {
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        runnable.run();
    }
}

void main() {
    var user1 = new User("HCD");
    IO.println("Created " + user1);
    var user2 = new User("Andrei");
    IO.println("Created " + user2);
    IO.println("User's unique identifiers are: " + user1.id() + ", " + user2.id());
}

注意:

  • User由两个属性简单描述——name在构造时提供,而id表示内部唯一标识符。
  • id声明为StableValue,并在读取值时进行惰性初始化(在并发上下文中,由第一个执行该操作的线程初始化)
  • 一旦初始化,该值就是深度不可变的;无法更改,并保持如此直到对象被销毁

如果运行,输出如下:

Created HCD (477a7dc1-c71f-4189-8c58-13994148ff95)
Created Andrei (47647539-9cbe-4890-af23-050ee1fe9379)
User's unique identifiers are: 477a7dc1-c71f-4189-8c58-13994148ff95, 47647539-9cbe-4890-af23-050ee1fe9379

很明显,id在需要时设置,并且之后每次读取时其值都保持不变。

最后一点值得注意,关于User#id属性——作为StableValue,它自动是线程安全且无锁的。为了演示这一点,让我们运行下一段代码:

$ java
void main() {
    var user = new User("Concurrent User");
    var latch = new CountDownLatch(1);
    try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
        Future<?> result1 = exec.submit(new Task(latch, () ->
            IO.println("Task1 - Id: " + user.id() + " at " + System.currentTimeMillis())));
        Future<?> result2 = exec.submit(new Task(latch, () ->
            IO.println("Task2 - Id: " + user.id() + " at " + System.currentTimeMillis())));
        Future<?> result3 = exec.submit(new Task(latch, () ->
            IO.println("Task3 - Id: " + user.id() + " at " + System.currentTimeMillis())));

        latch.countDown();
        result1.get();
        result2.get();
        result3.get();
    } catch (ExecutionException | InterruptedException e) {
        throw new RuntimeException(e);
    }
}

任务1、2和3被创建并设置为读取预先创建的用户id,然后并行执行。下面的输出显示,在这次特定运行中,任务3设置了id,然后任务1和2使用了相同的值。

Task3 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123
Task1 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123
Task2 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123

StableValue还附带了一些更高级的辅助方法(function()intFunction()list()map()supplier()),每个都在各种场景中非常有用和适用。下面是Singleton模式如何实现的示例:

$ java
record User(int id, String name) {}

static class UserService {
    public UserService() {
        IO.println("UserService created");
    }

    public void register(User user) {
        IO.println("Registered " + user);
    }
}

static UserService getInstance() {
    return USER_SERVICE_INSTANCE.orElseSet(UserService::new);
}

private static final StableValue<UserService> USER_SERVICE_INSTANCE = StableValue.of();

void main() {
    getInstance().register(new User(1, "HCD"));
    getInstance().register(new User(2, "Andrei"));
}

目标是拥有一个UserService的单一实例,用于通过指定方法注册用户。如果运行,输出如下,清楚地显示构造函数只被调用了一次:

UserService created
Registered User[id=1, name=HCD]
Registered User[id=2, name=Andrei]

总之,StableValue增强功能确保了在JVM级别强制实现不变性——一旦值被设置,它就是稳定的,并对所有线程可见。

结论

本文简要介绍了Java 25的几个语言增强功能,希望这些直接的示例能为深入探索这些功能提供一个起点。无论你是否已经迁移到最新的LTS,无论你是否开始探索最新的添加和改进,我都认为这无论如何都值得一做。

在JavaOne 2026的一次开幕主题演讲中,我注意到了这样一句话:“Java 无处不在,AI 需要它。”我完全同意。在一个显然每个人都专注于“加速推理”的世界里,让我们对未来保持乐观,继续构建和巩固我们的Java基础——通过探索新的添加、保持最新,并逐步将它们应用到我们的个人和专业项目中去。

资源

[1] —— JDK 25 [2] —— 示例代码可在此获取。