Java 24 新功能示例

Java 开发工具包 24 是 Java 标准版的下一个版本,现已于 2024 年 3 月 18 日发布。 Java 24 引入了几个明显的功能,旨在增强语言的功能、性能和安全性。我们将对这些功能进行概述,并辅以示例代码片段以加深理解。在本文中,我们将结合示例探讨一些最基本的、对开发人员友好的 Java 24 新特性。

您也可以查看最近的 LTS 版本 Java 21 新特性示例。

有关其他版本的功能,请访问 Java 8 之后的 Java 功能。

目录

如何启用 Java 24 的预览功能?

要编译和运行包含预览功能的代码,我们必须指定额外的命令行选项。由于我们要讨论 Java 24 的一些预览功能,因此需要在编译和运行时显式启用它们:

编译:

  javac --enable-preview --release 24 YourClass.java

执行:

  java --enable-preview YourClass

让我们通过示例逐一开始探索 Java 24 新功能。

模式、instanceof 和 switch 中的基元类型(JEP-488,第二预览版)

State in Java 22:

在 Java 22 之前,模式匹配主要应用于引用类型。开发人员可以将模式匹配与 “instanceof “操作符一起使用,以简化对象的类型检查和转换。然而,原始类型并不包括在内,这限制了模式匹配在所有数据类型中的表现力和一致性。

Example: Using instanceof with Primitive Types in Java 22

Object value = 42;
if (value instanceof Integer i) {
    System.out.println("Integer value: " + i);
}

Example: Using switch with Primitive Type Patterns in Java 22

Object value = 3.14;
String result = switch (value) {
    case Integer i -> "Integer: " + i;
    case Double d -> "Double: " + d;
    default -> "Unknown type";
};
System.out.println(result);

Java 24 的增强功能:

该功能在 Java SE 23 中首次预览,本版本对其进行了重新预览。Java SE 23 与此版本之间没有变化。Java 24 将模式匹配扩展到包括所有模式上下文中的基元类型,如 instanceof 操作符和 switch 表达式及语句。这一增强功能使代码在处理对象和基元类型时更加简洁易读。instanceof 操作符和 switch 表达式及语句已扩展到可处理所有基元类型。

Example: Using instanceof with Primitive Types

int value = 42;
if (value instanceof int) {    // Possible with Java 24 
   System.out.println("Integer value: " + i); 
} 

int value = 10; 
if (value instanceof int i && i > 5) {   // Possible with Java 24
  System.out.println("Value is an integer greater than 5"); 
}

Example: Using switch with Primitive Type Patterns

int status = 2;
String message = switch (status) {
    case 0 -> "OK";
    case 1 -> "Warning";
    case 2 -> "Error";
    case int i -> "Unknown status: " + i;
};
System.out.println(message);

double value = 3.14; 
switch (value) {
  case int i -> System.out.println("value is an integer");
  case double d -> System.out.println("value is a double"); // Not possible before Java 24 
  default -> System.out.println("value is something else"); 
}

double d = 3.14;  
switch (d) { 
  case 3.14 -> System.out.println("Value is PI");
  case 2.71 -> System.out.println("Value is Euler's number");
  default -> System.out.println("Value is not a recognized constant"); 
}

float rating = 0.0f;
switch (rating) {
  case 0f -> System.out.println("0 stars");
  case 2.5f -> System.out.println("Average");
  case 5f -> System.out.println("Best");
  default -> System.out.println("Invalid rating");
}

灵活的构造体 (JEP-492, Third Preview)

State in Java 23:

在 Java 22 之前,Java 语言要求任何显式构造函数调用(super() 或 this())都必须作为构造函数的第一条语句出现。这一限制意味着任何初始化或验证代码都必须出现在这些调用之后,从而可能导致代码结构不够直观。

首次在 Java 22 中作为 JEP 447 预览:super(…) 之前的语句(预览),并在 Java 23 中作为 JEP 482 再次预览: 灵活的构造函数主体(第二次预览)。

Java 24 的增强功能:

此功能已在本版本中重新预览,未做任何重大更改。

示例: 调用超类构造函数前验证参数

class Person {
    String name;
    Person(String name) {
        this.name = name;
    }
}

class Employee extends Person {
    Employee(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        super(name);
    }
}

模块导入声明 (JEP-494, Second Preview)

这一增强功能允许开发人员使用单个声明从模块导出的包中导入所有公共顶级类和接口,从而简化了导入过程并减少了模板代码。

模块系统要求将代码组织成模块,以利用强封装和可靠配置的优势。然而,将模块化库导入非模块化代码库并不方便,往往需要采用变通方法或完全迁移到模块化系统。模块导入声明使您能够简洁地导入模块导出的所有包。

State in Java 23:

该功能在 Java 23 中首次预览。

Java 24 的增强功能:

此功能在此版本中进行了第二次重新预览。在此版本中:

  • 按需导入类型声明 阴影模块导入声明。
  • 模块可声明对 java.base 模块的传递依赖。
  • java.se 模块转而需要 java.base 模块。因此,导入 java.se 模块将导入整个 Java SE API。

这一特性缩小了模块化代码库与非模块化代码库之间的差距,有助于更顺利地过渡到模块化。

Java 24 改进了模块导入声明,提高了兼容性并减少了导入模块时的潜在冲突。

模块导入声明的语法:

import module moduleName;

这里,moduleName 指的是模块的名称,您希望导入该模块的导出包。

如何导入?

模块导入声明导入以下所有公共顶级类和接口:

  1. 指定模块导出到当前模块的包。
  2. 当前模块因读取指定模块而读取的模块导出的包。这意味着,如果指定的模块有传递依赖关系,它们导出的包也会被导入。

示例

  1. 导入 “java.base “模块:java.base “模块是一个基本模块,它导出了许多包,如 “java.util”、”java.io “等。java.base 的模块导入声明相当于多个按需包导入。
    import module java.base;
    
    public class BaseModuleExample {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            System.out.println("List created: " + list);
        }
    }
    

    在本例中,无需单独的导入语句即可访问 java.util 包中的 List 和 ArrayList 类,因为 java.util 是由 java.base 模块导出的。

  2. Importing the ‘java.sql’ Module:

    java.sql “模块导出了 “java.sql “和 “javax.sql “等包。通过导入该模块,您可以访问这些包中的所有公共类和接口。

    import module java.sql;
    
    public class SqlModuleExample {
        public static void main(String[] args) throws SQLException {
            Connection conn = DriverManager.getConnection("jdbc:your_database_url");
            System.out.println("Connection established: " + conn);
        }
    }
    

    在这里,由于模块导入声明,”java.sql “包中的 Connection 和 DriverManager 可以被访问。

    处理歧义:

    当多个模块导出包含具有相同简单名称的类的包时,可能会产生歧义。例如,”java.awt “和 “java.util “都有一个 List 类。为解决此类冲突,我们可以使用单一类型导入声明或完全限定类名。

import module java.desktop;
import module java.base;

public class AmbiguityExample {
    public static void main(String[] args) {
        java.util.List<String> list = new ArrayList<>();
        System.out.println("List created: " + list);
    }
}

在这种情况下,指定 java.util.List 可明确说明使用的是哪个 List 类。

Java 模块系统中的传递依赖关系:

在 Java 的模块系统中,传递依赖关系意味着当一个模块需要另一个模块时,它也可以访问所需的模块本身依赖的模块。

  1. java.base 是 Java 中的基本模块。它包含 java.lang、java.util、java.io 等核心类。Java 中的所有其他模块都隐式地依赖于 java.base,这意味着不需要显式地要求它。
  2. java.se 是一个总括模块,包括所有标准的 Java SE 模块(如 java.sql、java.xml、java.desktop 等)。java.se 模块临时需要 java.base,这意味着当您依赖 java.se 时,您也会自动访问 java.base。

示例 #1:java.base 的隐式可用性

因为所有模块都需要 java.base,所以我们不需要在 module-info.java 文件中明确声明它。

// Java program using classes from java.base
public class BaseExample {
    public static void main(String[] args) {
        String message = "Hello, Java!";
        System.out.println(message.toUpperCase());
    }
}

由于 String 和 System.out 属于 java.base 的一部分,因此该程序无需任何模块声明即可运行。

示例 #2:明确要求 java.se

如果我们创建一个模块化应用程序并要求 java.se,我们就会自动访问 java.base。

module-info.java
modulemy.application {
    requires java.se;
}
Main.java
import java.util.List;
import java.sql.Connection;

public class SEExample {
    public static void main(String[] args) {
        List<String> names = List.of("Java", "Angular", "Mongo DB");  // java.util (from java.base)
        System.out.println("Names: " + names);

        Connection conn = null;  // java.sql (from java.se)
        System.out.println("Database connection: " + conn);
    }
}

由于需要 java.se,所以 java.util.List (来自 java.base)和 java.sql.Connection (来自 java.sql)都是可用的。

示例#3:传递依赖关系

如果一个模块 A 需要 java.se,而另一个模块 B 需要 A,那么 B 也可以传递地访问 java.base。

module-info.java for moduleA
module moduleA {
    requires java.se;
}
module-info.java for moduleB
module moduleB {
    requires moduleA;  // No need to explicitly require java.base
}

尽管模块 B 并不直接需要 java.base,但它仍然可以访问 java.base 中的类,因为模块 A 需要 java.se,而 java.se 过渡性地需要 java.base。

要点:

  1. java.base 总是可用的,不需要明确需要。
  2. java.se 是一个伞状模块,它需要多个模块,包括 java.base。
  3. 当您需要 java.se 时,您将自动访问 java.base 和所有标准 Java SE 模块。
  4. 传递依赖关系意味着,如果模块 A 需要 java.se,那么模块 B(需要模块 A)也能访问 java.base,而无需明确要求。

示例: 导入各种模块

import module java.util;
import module java.base;
import module java.sql;
import java.util.Date;

public class ModuleImportConflictExample {
    public static void main(String[] args) {
        Date date = new Date();
        System.out.println("Date: " + date);
        List<String> list = new ArrayList<>();
        System.out.println("Module import works!");
    }
}

Stream 流收集器 (JEP-485)

Stream 流 API 为处理数据流提供了一组内置的中间操作(如 map、filter 和 flatMap)。这些操作虽然功能强大,但在需要进行更复杂的转换时可能会受到限制,往往导致开发人员编写自定义收集器或采用不那么优雅的解决方案。

Java 23 中的状态:JDK 22 中的 JEP 461 将

流收集器作为预览功能提出,JDK 23 中的 JEP 473 对其进行了重新预览。

Java 24 中的增强功能:

该功能在 JDK 24 中最终确定,未作任何更改。Java 增强了流 API,以支持自定义的中间操作(称为流收集器)。此功能允许开发人员创建更灵活、更具表现力的流管道,实现以前难以实现或冗长的转换。

示例: 使用自定义中间操作

 

Stream<String> stream = Stream.of("a", "b", "c");
Stream<String> modifiedStream = stream.gather(
    () -> new StringBuilder(),
    (sb, s) -> sb.append(s.toUpperCase()),
    sb -> sb.toString()
);
modifiedStream.forEach(System.out::println);

简单源文件和实例主方法(JEP 495,第四版预览版)

这一功能使初学者更容易编写第一个程序,而不必担心大型应用程序的复杂功能。初学者无需使用不同版本的 Java,可以从简单的单类程序开始,随着学习的深入逐渐掌握更高级的功能。

经验丰富的开发人员也可以更快地编写小型程序,而无需使用用于大型应用程序的额外结构。

该功能在 Java 21(JEP 445:未命名类和实例主方法)中作为预览版首次推出,并在 Java 23(JEP 477:隐式声明类和实例主方法)中再次出现。在 Java 24 中,它将以新的术语和修订后的标题再次预览,但其他内容不变。

您可以查看《Java 平台标准版 Java 语言更新》中的《简单源文件和实例主方法》。

类文件 API (JEP-484)

与 Java 类文件交互通常需要第三方库或手动解析,因为没有用于解析、生成或转换类文件的标准 API。缺乏标准化可能会导致兼容性问题,并增加维护开销。

Java 23 中的状态:

类文件 API 最初由 JDK 22 中的 JEP 457 作为预览功能提出,并由 JDK 23 中的 JEP 466 加以完善。

Java 24 中的增强功能:

该功能建议在 JDK 24 中最终确定 API,并对其进行小幅修改。

 

import java.nio.file.Files;
import java.nio.file.Path;
import jdk.classfile.ClassFile;
import jdk.classfile.Method;

public class EnhancedClassFileAPIExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("Example.class");
        byte[] classBytes = Files.readAllBytes(path);
        ClassFile classFile = ClassFile.read(classBytes);
        System.out.println("Class Name: " + classFile.thisClass());
        for (Method method : classFile.methods()) {
            System.out.println("Method: " + method.name());
        }
    }
}

scoped 范围值(JEP-487,第四次预览)

Java 23 中的状态:

在 Java 23 中,scoped 值作为第三次预览功能被引入,使方法能够与线程内的调用者以及子线程共享不可变数据。这种机制为线程本地变量提供了一种更直接、更高效的替代方法,尤其是在与虚拟线程和结构化并发一起使用时。不过,该应用程序接口包括 ScopedValue 类中的 callWhere 和 runWhere 等方法,这些方法虽然功能强大,但却增加了应用程序接口流畅性的复杂性。

JEP 429(JDK 20)建议对范围值 API 进行孵化,JEP 446(JDK 21)建议对其进行预览,随后 JEP 464(JDK 22)和 JEP 481(JDK 23)对其进行了改进和完善。

为了获得更多的经验和反馈,我们建议在 JDK 24 中再次重新预览 API 的功能,并做出进一步修改:从 ScopedValue 类中删除

callWhere 和 runWhere 方法,使 API 完全流畅。使用一个或多个绑定的范围值的唯一方法是通过 ScopedValue.Carrier.call 和 ScopedValue.Carrier.run 方法。

Java 24 中的增强功能:

在 Java 24 中,ScopedValue 已进入第四次预览,并进行了重大改进,以提高 API 的流畅性和可用性。主要的改进是删除了 ScopedValue 类中的 callWhere 和 runWhere 方法。取而代之的是,API 现在依赖 ScopedValue.Carrier.call 和 ScopedValue.Carrier.run 方法在特定执行上下文中绑定作用域值。这一变化简化了 API,使其对开发人员来说更加直观和一致。

示例: 在 Java 24 中使用作用域值

 

import java.lang.ScopedValue;
import java.lang.ScopedValue.Carrier;

public class ScopedValueExample {
    // Define a ScopedValue
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        // Create a carrier for the scoped value
        Carrier carrier = ScopedValue.where(USER_ID, "user123");

        // Run a task within the scope of the carrier
        carrier.run(() -> {
            // Access the scoped value within the scope
            System.out.println("User ID: " + USER_ID.get());
            performTask();
        });
    }

    private static void performTask() {
        // Access the scoped value in a nested method
        System.out.println("Performing task for User ID: " + USER_ID.get());
    }
}

在本例中,定义了 ScopedValue USER_ID,然后使用 ScopedValue.where 方法在特定作用域内将其与值 “user123 “绑定,并返回一个 Carrier。Carrier.run 方法在范围值绑定的上下文中执行所提供的任务。在此范围内,任何方法都可以使用 USER_ID.get() 访问 USER_ID 值,从而确保在方法调用和线程之间安全高效地共享数据。

Java 24 中的这一增强功能简化了 API,使其更流畅、更易用,从而改善了开发人员在处理作用域值时的体验。

向量 API(JEP-489,第九个孵化器)

向量 API 允许开发人员表达向量计算,这些计算可在运行时编译为所支持 CPU 架构上的最佳向量指令,从而实现优于同等标量计算的性能。

示例:矢量加法 矢量加法

VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
float[] a = {1.0f, 2.0f, 3.0f, 4.0f};
float[] b = {5.0f, 6.0f, 7.0f, 8.0f};
float[] c = new float[4];
for (int i = 0; i < a.length; i += SPECIES.length()) {
    var va = FloatVector.fromArray(SPECIES, a, i);
    var vb = FloatVector.fromArray(SPECIES, b, i);
    var vc = va.add(vb);
    vc.intoArray(c, i);
}

结构化并发(JEP-499,第四预览版)

结构化并发将在不同线程中运行的相关任务组视为一个工作单元,从而简化了并发编程,简化了错误处理和取消,提高了可靠性,并增强了可观察性。

示例 使用结构化并发

 

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> user = scope.fork(() -> findUser());
    Future<Integer> order = scope.fork(() -> fetchOrder());
    scope.join();
    scope.throwIfFailed();
    System.out.println(user.resultNow() + " " + order.resultNow());
}

密钥衍生函数 API (JEP-478,预览版)

介绍了密钥衍生函数(KDF)的 API,这是一种从秘密密钥和其他数据衍生出额外密钥的加密算法,可增强加密应用的安全性。

示例: 使用密钥派生函数

 

SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
SecretKey key = factory.generateSecret(spec);

抗量子的基于模块-晶格的密钥封装机制(JEP-496)

Java 24 引入了抗量子的加密算法以增强安全性,特别是实现了基于模块-晶格的密钥封装机制(ML-KEM)。该算法旨在保护数据免受未来量子计算进步带来的潜在威胁。

了解 ML-KEM:

ML-KEM 是一种密钥封装机制,允许双方通过不安全的通信信道安全地建立共享秘密。然后,该共享秘密可与对称密钥加密算法一起用于加密和验证等任务。ML-KEM 的设计目的是安全抵御潜在的量子计算攻击,美国国家标准与技术研究院(NIST)已在 FIPS 203 中将其标准化。

在 Java 24 中使用 ML-KEM:

Java 24 为 ML-KEM 提供了 KeyPairGenerator、KEM 和 KeyFactory API 的实现,支持 FIPS 203 中定义的参数集 ML-KEM-512、ML-KEM-768 和 ML-KEM-1024。下面介绍如何在 Java 应用程序中使用 ML-KEM:

生成 ML-KEM 密钥对:

首先,使用 ML-KEM 的 KeyPairGenerator 生成密钥对:

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.NamedParameterSpec;

public class MLKEMExample {
    public static void main(String[] args) throws Exception {
        // Initialize the KeyPairGenerator with the ML-KEM algorithm
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ML-KEM");

        // Specify the parameter set; ML-KEM-512 is used here
        keyPairGenerator.initialize(new NamedParameterSpec("ML-KEM-512"));

        // Generate the key pair
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        System.out.println("ML-KEM Key Pair generated successfully.");
        // The public and private keys can now be used for encapsulation and decapsulation
    }
}

封装秘钥(发送方视角):

发送方使用接收方的公开密钥封装密钥:

import javax.crypto.KEM;
import javax.crypto.SecretKey;

// Assuming 'publicKey' is the recipient's ML-KEM public key
KEM kem = KEM.getInstance("ML-KEM");
KEM.Encapsulator encapsulator = kem.encapsulator(publicKey);

// Encapsulate the secret key
KEM.Encapsulated encapsulated = encapsulator.encapsulate();

// Retrieve the encapsulated message to send to the recipient
byte[] encapsulation = encapsulated.getEncapsulation();

// The sender's copy of the secret key
SecretKey secretKeySender = encapsulated.getKey();

System.out.println("Secret key encapsulated successfully.");

解封装秘钥(接收方视角):

接收方使用自己的私人密钥对收到的封装信息进行解封装:

import javax.crypto.KEM;
import javax.crypto.SecretKey;

// Assuming 'privateKey' is the recipient's ML-KEM private key
KEM kem = KEM.getInstance("ML-KEM");
KEM.Decapsulator decapsulator = kem.decapsulator(privateKey);

// Decapsulate the secret key using the received encapsulation
SecretKey secretKeyReceiver = decapsulator.decapsulate(encapsulation);

System.out.println("Secret key decapsulated successfully.");

注:在本例中,发送方和接收方都获得了相同的密匙,可用于安全通信。所选参数集(如 ML-KEM-512)决定了安全强度和性能特征。确保双方就参数集达成一致,以保持兼容性。

通过集成 ML-KEM,Java 24 增强了平台抵御量子计算威胁的能力,确保为面向未来的应用提供安全的密钥交换机制。

抗量子的基于模块-晶格的数字签名算法(JEP-497)

Java 24 引入了抗量子的数字签名机制–基于模块-晶格的数字签名算法(ML-DSA)。数字签名可对数据进行验证并检测未经授权的修改。ML-DSA 的设计目的是安全应对未来的量子计算威胁,并已由 NIST 在 FIPS 204 中标准化。

举例说明: 使用 ML-DSA

KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA");
KeyPair kp = kpg.generateKeyPair();
Signature sig = Signature.getInstance("ML-DSA");
sig.initSign(kp.getPrivate());
sig.update(data);
byte[] signature = sig.sign();

这些功能共同提高了 Java 的性能、安全性和可用性,巩固了其作为现代通用编程语言的地位。

本文文字及图片出自 Java 24 New Features With Examples

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注