Java 23新特性揭秘:super不再是构造函数首条语句(附示例详解)

前言

Java 23 自 2024 年 6 月 6 日起进入所谓的“Rampdown 第一阶段”,因此该版本中将不再包含任何 JDK 增强提案 (JEP)。因此,功能集已修复。只会纠正错误,并且如有必要,将进行微小改进。

目标发布日期为 2024 年 9 月 17 日。您可以在此处下载最新的抢先体验版本。

Java 23 的亮点:

  • Markdown文档注释:最后,我们可以用Markdown编写JavaDoc
  • 使用Patterns、instanceof和switch中的基本数据类型primitive types将变量与基元类型进行匹配(预览)。
  • 使用模块导入声明(import module)导入整个模块。直接使用print()、println()和readln():不需带System.inSystem.out的输入和输出,带隐式声明的类和实例main方法。
  • 在调用super(...)之前,允许定义其它语句(即super不再是构造函数第一条语句)使用灵活的构造函数体初始化构造函数中的字段。
  • 此外,Java 21Java 22中引入的许多其他功能也正在进入新一轮预览,无论是否有细微变化。

    Java 21 中引入并在 Java 22 中重新引入的字符串模板是个例外:它们不再包含在 Java 23 中。根据 Gavin Bierman 的说法,大家一致认为需要修改设计,但对于如何实际进行修改存在分歧。因此,语言开发人员决定花更多时间来修改设计,并在以后的 Java 版本中以完全修改的形式呈现该功能。

    与往常一样,我对所有 JEP 和其他更改都使用原始英文标题。

    1. Markdown 文档注释– JEP 467

    为了格式化 JavaDoc 注释,我们一直必须使用 HTML。这在 1995 年无疑是一个不错的选择,但如今,Markdown 比 HTML 更受欢迎,更适合用于编写文档。

    JDK 增强提案 467允许我们从 Java 23 开始在 Markdown 中编写 JavaDoc 注释。

    以下示例Math.ceilMod(...)以常规符号显示了该方法的文档:

    /**
     * Returns the ceiling modulus of the {@code long} and {@code int} arguments.
     * <p>
     * The ceiling modulus is {@code r = x – (ceilDiv(x, y) * y)},
     * has the opposite sign as the divisor {@code y} or is zero, and
     * is in the range of {@code -abs(y) < r < +abs(y)}.
     *
     * <p>
     * The relationship between {@code ceilDiv} and {@code ceilMod} is such that:
     * <ul>
     *   <li>{@code ceilDiv(x, y) * y + ceilMod(x, y) == x}</li>
     * </ul>
     * <p>
     * For examples, see {@link #ceilMod(int, int)}.
     *
     * @param x the dividend
     * @param y the divisor
     * @return the ceiling modulus {@code x – (ceilDiv(x, y) * y)}
     * @throws ArithmeticException if the divisor {@code y} is zero
     * @see #ceilDiv(long, int)
     * @since 18
     */
    

    该示例包含格式化的代码、段落标记、项目符号列表、链接以及 JavaDoc 特定信息,例如@param和@return。

    要使用 Markdown,我们需要用三个斜杠(///)开始 JavaDoc 注释的所有行。Markdown 中的相同注释如下所示:

    /// Returns the ceiling modulus of the `long` and `int` arguments.
    ///
    /// The ceiling modulus is `r = x – (ceilDiv(x, y) * y)`,
    /// has the opposite sign as the divisor `y` or is zero, and
    /// is in the range of `-abs(y) < r < +abs(y)`.
    ///
    /// The relationship between `ceilDiv` and `ceilMod` is such that:
    ///
    /// – `ceilDiv(x, y) * y + ceilMod(x, y) == x`
    ///
    /// For examples, see [#ceilMod(int, int)].
    ///
    /// @param x the dividend
    /// @param y the divisor
    /// @return the ceiling modulus `x – (ceilDiv(x, y) * y)`
    /// @throws ArithmeticException if the divisor `y` is zero
    /// @see #ceilDiv(long, int)
    /// @since 18
    

    这既更容易编写,也更容易阅读。

    具体来说,有哪些变化?

  • 源代码以...标记而不是{@code …}。
  • HTML 段落字符<p>已被空行替换。
  • 枚举项以连字符引入。
  • 链接用 标记[…],而不是{@link …}。
  • JavaDoc 特定的细节(例如@param@return)保持不变。
  • 支持以下文本格式:

    /// **This text is bold.**
    /// *This text is italic.*
    /// _This is also italic._
    /// `This is source code.`
    ///
    /// ```
    /// This is a block of source codex.
    /// ```
    ///
    ///     Indented text
    ///     is also rendered as a code block.
    ///
    /// ~~~
    /// This is also a block of source code
    /// ~~~
    

    支持枚举列表和编号列表:

    /// This is a bulleted list:
    /// – One
    /// – Two
    /// – Three
    ///
    /// This is a numbered list:
    /// 1. One
    /// 1. Two
    /// 1. Three
    

    您还可以显示简单的表格:

    /// | Binary | Decimal |
    /// |--------|---------|
    /// |     00 |       0 |
    /// |     01 |       1 |
    /// |     10 |       2 |
    /// |     11 |       3 |
    

    您可以按照如下方式集成到其他程序元素的链接:

    /// Links:
    /// – ein Modul: [java.base/]
    /// – ein Paket: [java.lang]
    /// – eine Klasse: [Integer]
    /// – ein Feld: [Integer#MAX_VALUE]
    /// – eine Methode: [Integer#parseInt(String, int)]
    

    如果链接文本和链接目标不同,可以将链接文本放在前面的方括号中:

    /// Links:
    /// – [ein Modul][java.base/]
    /// – [ein Paket][java.lang]
    /// – [eine Klasse][Integer]
    /// – [ein Feld][Integer#MAX_VALUE]
    /// – [eine Methode][Integer#parseInt(String)]
    

    最后但同样重要的一点是,如果在代码或代码块中使用 JavaDoc 标签(例如@param、@throws等),则不会对其进行评估。

    2. Java 23 中的新预览功能

    Java 23 引入了两个新的预览功能。您不应在生产代码中使用这些功能,因为它们仍可能发生变化(或者,就像字符串模板的情况一样,可能会在短时间内再次被删除)。

    javac您必须通过 VM 选项在命令中明确启用预览功能–enable-preview –source 23。对于该java命令来说,–enable-preview这就足够了。

    2.1 模块导入声明(预览)– JEP 476

    从 Java 1.0 开始,java.lang包中的所有类都会自动导入到每个.java 文件中。这就是为什么我们可以不加语句地使用诸如Object、String、Integer、Exception、Thread等类import。

    我们也一直能够导入完整的软件包。例如,导入java.util.*意味着我们不必单独导入List、Set、Map、ArrayList、HashSetHashMap等类。

    JDK 增强提案 476现在允许我们导入完整的模块- 更准确地说,是模块导出的包中的所有类

    例如,我们可以按如下方式导入完整的java.base模块,并使用此模块中的类(在示例List、Map、Collectors、Stream中),无需进一步导入:

    import module java.base;
    
    public static Map<Character, List<String>> groupByFirstLetter(String... values) {
      return Stream.of(values).collect(
          Collectors.groupingBy(s -> Character.toUpperCase(s.charAt(0))));
    }
    

    要使用import module,导入类本身不需要位于模块中。

    2.1.1 模糊的类名

    如果有两个导入的类具有相同的名称,例如以下示例中的Date,则会发生编译器错误:

    import module java.base;
    import module java.sql;
    
    . . .
    Date date = new Date();  // Compiler error: "reference to Date is ambiguous"
    . . .
    

    解决方案很简单:我们还必须直接导入所需的Date类:

    import module java.base;
    import module java.sql;
    import java.util.Date;  // ⟵ This resolves the ambiguity
    
    . . .
    Date date = new Date();
    . . .
    
    2.1.2 过渡导入

    如果导入的模块传递性地导入另一个模块,那么我们也可以使用传递性导入模块的导出包的所有类,而无需显式导入。

    例如,java.sql模块以传递方式导入java.xml模块:

    module java.sql {
      . . .
      requires transitive java.xml;
      . . .
    }
    

    因此,在以下示例中,我们不需要SAXParserFactory和SAXParser的任何显式导入,也不需要java.xml`模块的显式导入:

    import module java.sql;
    
    . . .
    SAXParserFactory factory = SAXParserFactory.newInstance();
    SAXParser saxParser = factory.newSAXParser();
    . . .
    
    2.1.3 JShell 中的自动模块导入

    JShell会自动导入10个常用包。此JEP将使JShell在将来导入完整的java.base模块。

    通过调用JShell一次不使用–enable-preview和一次使用–enable-preview,然后输入/imports命令,可以很好地演示这一点:

    $ jshell
    |  Welcome to JShell -- Version 23-ea
    |  For an introduction type: /help intro
    
    jshell> /imports
    |    import java.io.*
    |    import java.math.*
    |    import java.net.*
    |    import java.nio.file.*
    |    import java.util.*
    |    import java.util.concurrent.*
    |    import java.util.function.*
    |    import java.util.prefs.*
    |    import java.util.regex.*
    |    import java.util.stream.*
    
    jshell> /exit
    |  Goodbye
    
    $ jshell --enable-preview
    |  Welcome to JShell -- Version 23-ea
    |  For an introduction type: /help intro
    
    jshell> /imports
    |    import java.base
    

    当启动JShell而不启用预览(--enable-preview)时,您将看到十个导入的包;当使用--enable-preview启动它时,您将只看到java.base模块的导入。

    2.1.4 隐式声明类中的自动模块导入

    java.base从 Java 23 开始,隐式声明的类也会自动导入完整的模块。

    2.2 模式中的原始类型、instanceof 和 switch(预览)– JEP 455

    使用instanceof和switch,我们可以检查某个对象是否属于某种类型,如果是,则将该对象绑定到该类型的变量,执行特定的程序路径,并在该程序路径中使用新变量。

    例如,以下代码块(自Java 16起已获准使用)检查对象是否为至少包含 5 个字符的字符串,如果是,则以大写形式打印。如果对象是整数,则计算该数字的平方并打印。否则,按原样打印对象。

    if (obj instanceof String s && s.length() >= 5) {
      System.out.println(s.toUpperCase());
    } else if (obj instanceof Integer i) {
      System.out.println(i * i);
    } else {
      System.out.println(obj);
    }
    

    从Java 21开始,我们可以使用switch更清楚地做到这一点:

    switch (obj) {
      case String s when s.length() >= 5 -> System.out.println(s.toUpperCase());
      case Integer i                     -> System.out.println(i * i);
      case null, default                 -> System.out.println(obj);
    }
    

    然而,到目前为止,这只适用于对象。instanceof根本不能与基元数据类型一起使用,只在可以将基元类型byte、short、char和int的变量与常量匹配的范围内进行切换,例如:

    int x = ...
    switch (x) {
      case 1, 2, 3 -> System.out.println("Low");
      case 4, 5, 6 -> System.out.println("Medium");
      case 7, 8, 9 -> System.out.println("High");
    }
    

    得益于JDK增强提案455,所有基元类型现在也可以在Java 23的模式匹配中使用,包括instanceof和switch。
    JDK增强提案455在Java 23中引入了两个更改:

  • 首先,所有基元类型现在都可以在switch表达式和语句中使用,包括long、float、double和boolean。
  • 其次,我们还可以在模式匹配中使用所有原始类型,包括instanceof和switch。
  • 在这两种情况下,即对于通过long、float、double和boolean进行切换,以及与原始变量进行模式匹配,切换(与所有新的切换功能一样)必须是详尽的,即涵盖所有可能的情况。

    2.2.1 来自 Java 23:模式匹配中的原始类型

    使用原始模式时,确切的含义与使用对象时不同,因为原始类型没有继承:
    是一个基本类型的变量(即byte、short、int、long、float、double、charboolean),B是这些基本类型之一。然后,如果a的精确值也可以存储在B类型的变量中,则B的实例结果为真。
    为了帮助你更好地理解这是什么意思,这里有一个简单的例子:

    int value = ...
    if (value instanceof byte b) {
      System.out.println("b = " + b);
    }
    

    代码应按如下方式读取:如果变量值的值也可以存储在字节变量中,则将此值分配给字节变量b并打印出来。
    例如,对于值=5,情况就是这样,但对于值=1000则不然,因为字节只能存储-128到127之间的值。
    与对象一样,对于基本类型,您还可以使用&&直接在check实例中添加进一步的检查。例如,以下代码仅打印正字节值(即1到127):

    int value = ...
    if (value instanceof byte b && b > 0) {
      System.out.println("b = " + b);
    }
    

    让我们为value分配一个具体值,并将其与所有数值基元类型进行比较(不允许将数值类型与布尔值进行比较,这会导致编译器错误):

    int value = 65;
    if (value instanceof byte b)   System.out.println(value + " instanceof byte:   " + b);
    if (value instanceof short s)  System.out.println(value + " instanceof short:  " + s);
    if (value instanceof int i)    System.out.println(value + " instanceof int:    " + i);
    if (value instanceof long l)   System.out.println(value + " instanceof long:   " + l);
    if (value instanceof float f)  System.out.println(value + " instanceof float:  " + f);
    if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
    if (value instanceof char c)   System.out.println(value + " instanceof char:   " + c);
    

    此代码打印以下内容:

    65 instanceof byte:   65
    65 instanceof short:  65
    65 instanceof int:    65
    65 instanceof long:   65
    65 instanceof float:  65.0
    65 instanceof double: 65.0
    65 instanceof char:   A
    

    因此,值65可以与所有其他基本类型(布尔值除外)一起存储。您可以看到,作为float和double,值显示为一位小数,作为char,显示为字符“A”(其ASCII码为65)。
    如果我们将值更改为100000,我们将得到以下输出:

    100000 instanceof int:    100000
    100000 instanceof long:   100000
    100000 instanceof float:  100000.0
    100000 instanceof double: 100000.0
    

    因此,值100000可以存储在int、long、float和double类型的变量中,但不能存储在byte、short和char类型的变量。它们的数字范围最多只能达到12732767和65535。
    当值=16_777_217时,它变得有趣:

    16777217 instanceof int:    16777217
    16777217 instanceof long:   16777217
    16777217 instanceof double: 1.6777217E7
    

    所以16777217可以存储在int、long和double中,但不能存储在float中?
    确实如此!运行以下代码:

    float f = 16_777_217;
    System.out.printf("f = %.1f%n", f);
    

    结果出乎意料:

    f = 16777216.0
    

    这是因为浮点型浮点精度有限,例如可以存储16.777.216、16.777.218和16.777.220,但不能存储介于两者之间的值16.777.217和16.777.219。
    下面是一个在instanceof之前使用浮点数的示例:

    float value = 3.5F;
    if (value instanceof byte b)   System.out.println(value + " instanceof byte: " + b);
    if (value instanceof short s)  System.out.println(value + " instanceof short: " + s);
    if (value instanceof int i)    System.out.println(value + " instanceof int: " + i);
    if (value instanceof long l)   System.out.println(value + " instanceof long: " + l);
    if (value instanceof float f)  System.out.println(value + " instanceof float: " + f);
    if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
    if (value instanceof char c)   System.out.println(value + " instanceof char: " + c);
    

    此代码块打印以下内容:

    3.5 instanceof float:  3.5
    3.5 instanceof double: 3.5
    

    当然,因为一个有小数点的数字当然只能用浮点数和双精度表示。
    如果我们将值设置为100000.0F,结果如下:

    100000.0 instanceof int:    100000
    100000.0 instanceof long:   100000
    100000.0 instanceof float:  100000.0
    100000.0 instanceof double: 100000.0
    

    浮点数100000.0也可以存储在int或a中,只要它没有小数点。
    布尔值只能与boolean进行比较;布尔值与另一种类型的任何比较都会导致编译器错误。然而,这对我们没有多大帮助,因为布尔值与布尔值的比较总是得到true。

    2.2.2 带switch的基本类型模式

    我们不仅可以在instanceof中使用原始模式,还可以在switch中使用:

    double value = ...
    switch (value) {
      case byte   b -> System.out.println(value + " instanceof byte:   " + b);
      case short  s -> System.out.println(value + " instanceof short:  " + s);
      case char   c -> System.out.println(value + " instanceof char:   " + c);
      case int    i -> System.out.println(value + " instanceof int:    " + i);
      case long   l -> System.out.println(value + " instanceof long:   " + l);
      case float  f -> System.out.println(value + " instanceof float:  " + f);
      case double d -> System.out.println(value + " instanceof double: " + d);
    }
    

    以下是一些值的示例以及相应值将导致的输出:

  • 0可以表示为字节(-128到127)。
  • 10000可以表示为短(-32768到32767)。
  • 50000可以表示为一个char(0到65535)。
  • 1000000可以表示为整数(-2147483648到2147483647)。
  • 1000000000000可以表示为一个长(大约负9万亿到正9万亿)。
  • 0.125可以表示为浮点数。
  • 另一方面,0.126只能表示为双精度。
  • 同样,对于switch中的原始类型模式,我们可以使用“guards”,即用when将布尔表达式附加到模式上:

    double value = ...
    switch (value) {
      case 1 -> ...
      case 2 -> ...
      case int i when i < 100 -> ...
      case int i -> ...
      default -> ...
    }
    
    2.2.3 支配型和支配型

    对于具有原始类型的切换,我们必须遵守支配和被支配类型的原则,就像对象类型一样。支配类型是指可以表示支配类型的所有值以及更多值的类型。
    例如,byte以int为主,因为每个字节也可以存储为int。看看以下代码(在以下示例中,我使用了Java 22中未命名的变量_ finalize):

    double value = ...
    switch (value) {
      case int    _ -> System.out.println(value + " instanceof int");
      case byte   _ -> System.out.println(value + " instanceof byte");
      case double _ -> System.out.println(value + " instanceof double");
    }
    

    在这种情况下,case字节标签永远不会匹配,因为每个字节也是一个int,因此已经由case int标签处理。一般来说,支配类型必须始终列在支配类型之前。因此,以下内容是可以的:

    double value = ...
    switch (value) {
      case byte   _ -> System.out.println(value + " instanceof byte");
      case int    _ -> System.out.println(value + " instanceof int");
      case double _ -> System.out.println(value + " instanceof double");
    }
    
    switch的疲劳检查

    与Java 21中添加的所有开关功能一样,以下规则适用:如果您使用其中一个新功能,则switch必须详尽无遗。这就是为什么前面的例子也包含一个case double。以下行为是不允许的:

    double value = ...
    switch (value) {
      case byte   _ -> System.out.println(value + " instanceof byte");
      case int    _ -> System.out.println(value + " instanceof int");
    }
    

    此开关不完整,因此无效,例如,没有标签与值3.5匹配。编译器将返回消息“switch语句未覆盖所有可能的输入值”
    以下switch将正常运行:

    short value = ...
    switch (value) {
      case byte   _ -> System.out.println(value + " instanceof byte");
      case int    _ -> System.out.println(value + " instanceof int");
    }
    

    虽然这里没有case short,但有一个case int标签,每个可能的short值都与之匹配。
    为了总结对这一新特性的描述,我们不禁要问,在哪里可以合理地使用原始模式。我还没有遇到任何用例,JEP示例也没有真正说服我。如果你知道一个好的用例,请通过评论功能告诉我。

    3. 重新提交的预览和孵化器功能

    Java 23 中再次推出了 7 个预览和孵化器功能,其中 3 个与 Java 22 相比没有变化:

    3.1 流收集器(第二预览版)– JEP 473

    自从 Java 8 引入 Stream API 以来,Java 社区一直抱怨中间流操作的范围有限。诸如“窗口”或“折叠”之类的操作非常缺乏,而且反复被要求。

    JDK 开发人员没有屈服于社区的压力而提供这些功能,而是有一个更好的想法:他们实现了一个 API,他们和所有其他 Java 开发人员可以使用它来自己实现中间流操作。

    这个新 API 被称为“流收集器”。它首次由JDK 增强提案 461在Java 22中引入,并由JDK 增强提案 473在 Java 23 中第二次作为预览版未经更改地呈现,以便收集来自社区的进一步反馈。

    例如,通过以下代码,我们可以实现并使用中间流操作“map”作为流收集器:

    public <T, R> Gatherer<T, Void, R> mapping(Function<T, R> mapper) {
      return Gatherer.of(
          Integrator.ofGreedy(
              (state, element, downstream) -> {
                R mappedElement = mapper.apply(element);
                return downstream.push(mappedElement);
              }));
    }
    
    public List<Integer> toLengths(List<String> words) {
      return words.stream()
          .gather(mapping(String::length))
          .toList();
    }
    

    您可以在有关流收集器的主要文章中确切了解流收集器的工作原理、有哪些限制,以及我们是否最终会得到期待已久的“窗口”和“折叠”操作。

    3.2 隐式声明的类和实例主要方法(第三个预览) – JEP 477

    当 Java 开发人员编写他们的第一个程序时,它通常看起来像这样(直到现在)

    public class HelloWorld {
      public static void main(String[] args) {
        System.out.println("Hello world!");
      }
    }
    

    Java 初学者会同时面临许多新概念:

    通过classes.

  • 使用可见性修饰符public.
  • 使用静态方法.
  • 使用未使用的方法参数.
  • 和System.out。
  • 如果我们可以摆脱所有这些并专注于基本要素,那不是很好吗 – 就像下面的截图所示?

    这正是“隐式声明的类和实例主方法”所实现的!
    从Java 23开始,以下代码是一个有效且完整的Java程序:

    void main() {
      println("Hello world!");
    }
    

    这是如何实现的?

    1. 指定类不再是强制性的。如果省略类规范,编译器将生成隐式类。
    2. 方法main()不必是public或static,也不必有参数
    3. 隐式类自动导入新类java.io.IO,其中包含静态方法print(…)、println(…)和readln(…)。
      有关更多详细信息、示例、需要遵守的限制以及main()重载多个方法时会发生什么情况,请参阅有关Java main() 方法的主要文章。

    这里描述的更改最初在Java 21中以“未命名类和实例主方法”的名称发布。在Java 22中,该功能的一些过于复杂的方面得到了简化,并且该功能已重命名为当前名称。

    在 Java 23 中,JDK 增强提案 477添加了自动导入的java.io.IO类,因此最终System.out也可以省略,这在 Java 22 的第二个预览版中还无法实现。

    请注意,该功能在 Java 23 中仍处于预览阶段,必须使用 VM 选项激活–enable-preview。

    3.3 结构化并发(第三次预览)– JEP 480

    结构化并发是一种现代方法,通过虚拟线程将任务分成几个子任务以并行执行。

    结构化并发为并行任务的开始和结束提供了清晰的结构,并有序地处理错误。如果不再需要某些子任务的结果,则可以干净地取消这些子任务。

    使用结构化并发的一个例子是实现一种race()方法,该方法启动两个任务并返回完成的任务的结果,而另一个任务则自动取消:

    public static <R> R race(Callable<R> task1, Callable<R> task2)
        throws InterruptedException, ExecutionException {
      try (var scope = new StructuredTaskScope.ShutdownOnSuccess<R>()) {
        scope.fork(task1);
        scope.fork(task2);
        scope.join();
        return scope.result();
      }
    }
    

    您可以在有关结构化并发的主要文章中找到更详细的描述、更多用例和大量示例。

    结构化并发是Java 21中的预览功能,并在Java 22中再次呈现,没有任何变化。Java 23 中也没有变化(由JDK 增强提案 480指定)——JDK 开发人员希望在最终确定该功能之前获得进一步的反馈。

    3.4 作用域值(第三次预览) – JEP 481

    作用域值可用于将值传递给远程方法调用,而无需将它们作为参数循环遍历调用链的所有方法。
    经典的例子是用户登录到要为其执行特定用例的web服务器。作为此类用例的一部分,许多方法都需要访问用户信息。使用Scoped Values,我们可以设置一个上下文,在该上下文中,所有方法都可以访问用户对象,而无需将其作为参数传递给所有这些方法。
    以下代码使用ScopedValue.where(…)创建上下文:

    public class Server {
      public final static ScopedValue<User> LOGGED_IN_USER = ScopedValue.newInstance();
      . . .
      private void serve(Request request) {
        . . .
        User loggedInUser = authenticateUser(request);
        ScopedValue.where(LOGGED_IN_USER, loggedInUser)
                   .run(() -> restAdapter.processRequest(request));
        . . .
      }
    }
    

    现在,run(…)方法中调用的方法,以及它直接或间接调用的所有方法,例如调用堆栈中称为deep的存储库方法,都可以按如下方式访问用户:

    public class Repository {
      . . .
      public Data getData(UUID id) {
        Data data = findById(id);
        User loggedInUser = Server.LOGGED_IN_USER.get();
        if (loggedInUser.isAdmin()) {
          enrichDataWithAdminInfos(data);
        }
        return data;
      }
      . . .
    }
    

    任何曾经使用过ThreadLocal变量的人都会认识到这种相似性。但是,与ThreadLocals相比,Scoped Values有几个优点。您可以在关于范围值的主文章中找到这些优势和全面的介绍。
    作用域值与Java 21中的结构化并发一起作为预览功能引入,并在Java 22中发送到第二轮预览,没有任何更改。

    在Java 23中,JDK增强建议481将ScopedValue类的以下两个静态方法合并为一个:

    // Java 22:
    public static <T, R> R getWhere (ScopedValue<T> key, T value, Supplier<? extends R> op)
    public static <T, R> R callWhere(ScopedValue<T> key, T value, Callable<? extends R> op)
    

    这些方法的不同之处仅在于,一个Supplier被传递给getWhere(…)(一个具有不声明异常的get()方法的函数接口),一个Callable被传递给callWhere(…)(一种具有声明抛出异常的call()方法)。
    假设我们想在作用域值的上下文中调用以下方法,其中SpecificException是一个已检查的异常:

    Result doSomethingSmart() throws SpecificException {
      . . .
    }
    

    在 Java 22 中,我们必须按如下方式调用此方法:

    // Java 22:
    try {
      Result result = ScopedValue.callWhere(USER, loggedInUser, this::doSomethingSmart);
    } catch (Exception e) { // ⟵ Catching generic Exception
      . . .
    }
    

    由于Callable.call()抛出了通用的Exception,我们必须捕获Exception,即使被调用的方法抛出了更具体的异常。

    在 Java 23 中,现在只有一种callWhere(…)方法:

    public static <T, R, X extends Throwable> R callWhere(
        ScopedValue<T> key, T value, ScopedValue.CallableOp<? extends R, X> op) throws X
    

    现在将a传递给方法,而不是 aSupplier或 a 。这是一个函数式接口,定义如下:CallableScopedValue.CallableOp

    @FunctionalInterface
    public static interface ScopedValue.CallableOp<T, X extends Throwable> {
        T call() throws X
    }
    

    这个新接口包含一个可能抛出的异常作为类型参数X。这允许编译器识别调用可能抛出哪种异常callWhere(…)——我们可以直接SpecificException在catch块中处理:

    // Java 23:
    try {
      Result result = ScopedValue.callWhere(USER, loggedInUser, () -> doSomethingSmart());
    } catch (SpecificException e) { // ⟵ Catching SpecificException
      . . .
    }
    

    如果doSomethingSmart()没有抛出异常或者抛出了RuntimeException,我们可以省略 catch 块:

    // Java 23:
    Result result = callWhere(USER, loggedInUser, this::doSomethingSmart);
    

    Java 23 中的这一变化使代码更具表现力,且更不容易出错。

    3.5 灵活的构造函数主体(第二次预览)– JEP 482

    假设您有如下类:

    public class ConstructorTestParent {
      private final int a;
    
      public ConstructorTestParent(int a) {
        this.a = a;
        printMe();
      }
    
      void printMe() {
        System.out.println("a = " + a);
      }
    }
    

    并且我们假设您有第二个扩展该类的类:

    public class ConstructorTestChild extends ConstructorTestParent {
      private final int b;
    
      public ConstructorTestChild(int a, int b) {
        super(a);
        this.b = b;
      }
    }
    

    现在,您要确保在调用父类super构造函数之前,构造函数ConstructorTestChild中的ab不为负数。

    以前不允许在构造函数之前进行相应的检查。这就是为什么我们不得不使用如下的扭曲方法:

    public class ConstructorTestChild extends ConstructorTestParent {
      private final int b;
    
      public ConstructorTestChild(int a, int b) {
        super(verifyParamsAndReturnA(a, b));
        this.b = b;
      }
    
      private static int verifyParamsAndReturnA(int a, int b) {
        if (a < 0 || b < 0) throw new IllegalArgumentException();
        return a;
      }
    }
    

    这既不优雅也不容易阅读。

    我们还假设您想要覆盖printMe()父类构造函数中调用的方法,以打印派生类的字段:

    public class ConstructorTestChild extends ConstructorTestParent {
      . . .
      @Override
      void printMe() {
        super.printMe();
        System.out.println("b = " + b);
      }
    }
    

    如果调用,此方法会打印什么new ConstructorTestChild(1, 2)?

    它不会打印a = 1和b = 2,但是:

    a = 1
    b = 0
    

    这是因为b此时尚未初始化。它仅在调用之后super(…)才初始化,即在构造函数之后,而构造函数又调用printMe()。

    有了“灵活的构造函数主体”,这两个问题就都成为过去了。

    将来,在调用父类构造函数super(...)之前,以及在调用重载构造函数this(...)之前 我们可以执行任何不访问当前构造的实例的代码,即不访问其字段(这在Java 22中已经通过JDK 增强提案 447实现)。

    此外,我们还可以初始化刚刚构造的实例的字段。这在 Java 23 中通过JDK 增强提案 482实现。

    这些更改现在允许将代码重写如下:

    public class ConstructorTestChild extends ConstructorTestParent {
      private final int b;
    
      public ConstructorTestChild(int a, int b) {
        if (a < 0 || b < 0) throw new IllegalArgumentException();  // ⟵ 现在运行这样写了,感谢天感谢地!!
        this.b = b;                                                // ⟵ 现在运行这样写了,感谢天感谢地!!
        super(a);
      }
    
      @Override
      void printMe() {
        super.printMe();
        System.out.println("b = " + b);
      }
    }
    

    调用new ConstructorTestChild(1, 2)now 会产生预期的输出:

    a = 1
    b = 2
    

    新代码更易于阅读且更安全,因为它降低了在派生类中重写方法访问未初始化字段的风险。

    您可以在有关灵活构造函数主体的主要文章中找到更多需要考虑的示例和限制。

    3.6 Class-File API(第二个预览版)– JEP 466

    Java Class-File API 是用于读写.class 文件(即已编译的 Java 字节码)的接口。它旨在取代JDK 中广泛使用的字节码操作框架ASM 。

    Class-File API 在Java 22中作为预览功能引入,并由JDK 增强提案 466在 Java 23 中进入第二轮预览,并进行了一些改进。

    由于可能只有少数 Java 开发人员会直接使用 Class-File API,但通常通过其他工具间接使用,因此我不会在这里详细描述新的接口,就像在 Java 22 文章中一样。

    如果您对 Class-File API 感兴趣,您可以在JDK 增强提案 466中找到所有详细信息。或者在文章下发表评论!如果出乎意料的是,有足够的兴趣,我很乐意写一篇关于 Class-File API 的文章。

    3.7 Vector API(第八个孵化器)– JEP 469

    Vector API 首次作为孵化器功能纳入 JDK 已有三年半时间。在 Java 23 中,它将保持孵化器阶段,不会发生任何变化,正如JDK 增强提案 469所规定的那样。

    Vector API 可以将如下向量计算映射到现代 CPU 的特殊指令中。这将使此类计算能够以极快的速度执行 – 只需一个 CPU 周期即可达到一定的向量大小!

    向量加法的示例

    一旦 Vector API 进入预览阶段,我就会对其进行详细描述。这大概就是当Vector API 所需的Project Valhalla功能也在预览阶段可用时的情况(根据 Valhalla 开发人员大约一年前的声明,应该“很快”就会出现这种情况)。

    4. 弃用和删除

    在本节中,您将找到已从JDK 中标记为弃用或完全删除的功能的概述。

    4.1 弃用 sun.misc.Unsafe 中的内存访问方法并将其删除 – JEP 471

    该类sun.misc.Unsafe于 2002 年随 Java 1.4 引入。其大多数方法允许直接访问内存 – 既可以访问 Java 堆,也可以访问不受堆控制的内存(即本机内存)。

    正如类名所示,这些操作大部分都是不安全的。如果使用不当,可能会导致未定义的行为、性能下降或系统崩溃。

    Unsafe 最初仅用于 JDK 内部目的,但在 Java 1.4 中,没有模块系统可以向我们开发人员隐藏此类,并且如果您想尽可能高效地实现某些操作(例如,比较和交换)或访问大于 2 GB 的堆外内存块(这是ByteBuffer的极限),则没有其他选择。

    然而,今天我们有了其他选择:

  • Java 9 引入了VarHandles,它可以直接且优化地访问堆内存,可以设置各种类型的内存屏障,并提供比较和交换等原子操作。
  • 在Java 22中,外部函数和内存 API已完成。此 API 允许调用本机库中的函数并管理本机内存(即堆外内存)。
  • 由于这些稳定、安全且性能卓越的替代方案的出现,JDK 开发人员决定在JDK 增强提案 471中将所有用于Unsafe访问堆上和堆外内存的方法标记为已弃用,并在 Java 23 中将其删除,并在未来的 Java 版本中删除它们。

    拆除工作分四个阶段进行:

    阶段 1:在 Java 23 中,这些方法被标记为已弃用并被删除,以便在使用时发出编译器警告。
    阶段 2:据推测,在Java 25中,使用这些方法也会导致运行时警告。
    阶段 3:据推测,在 Java 26 中,这些方法将抛出一个UnsupportedOperationException。
    阶段 4:删除方法。尚未决定在哪个版本中执行此操作。
    我们可以使用 VM 选项覆盖各个阶段的默认行为--sun-misc-unsafe-memory-access

  • –sun-misc-unsafe-memory-access=allow – 可以使用所有不安全的方法。会显示编译器警告,但运行时不会发出任何警告(第 1 阶段的默认设置)。
  • –sun-misc-unsafe-memory-access=warn – 第一次调用受影响的方法之一时,运行时会显示警告(第 2 阶段的默认设置)。
  • –sun-misc-unsafe-memory-access=debug – 每当调用受影响的方法之一时,都会在运行时发出警告和堆栈跟踪。
  • –sun-misc-unsafe-memory-access=deny – 受影响的方法抛出UnsupportedOperationException(第 3 阶段的默认设置)。
  • 在第2和第3阶段,只能激活前一阶段的行为,而在第4阶段,此VM选项将不再具有任何效果。

    在JEP 的sun.misc.Unsafe 内存访问方法及其替代部分中可以找到标记为已弃用的所有方法及其各自替代的完整列表。

    4.2 Thread.suspend/resume 和 ThreadGroup.suspend/resume 已被删除

    容易发生死锁的方法Thread.suspend()、Thread.resume()、ThreadGroup.suspend()和已在 Java 1.2 中标记为弃用。ThreadGroup.resume()

    在Java 14中,这些方法被声明为已弃用并被删除。

    自Java 19起,ThreadGroup.suspend()和resume()抛出了UnsupportedOperationException– ;自Java 20起,Thread.suspend()和 也抛出了 – resume()。

    在 Java 23 中,所有这些方法最终被删除。

    此更改没有 JEP;它在JDK-8320532下的错误跟踪器中注册。

    4.3 ThreadGroup.stop 已被删除

    另外,在 Java 1.2 中,ThreadGroup.stop()它被标记为已弃用,因为从一开始停止线程组的概念就没有得到很好的实现。

    在Java 16中,该方法被声明为已弃用且应被删除。

    从Java 19开始,ThreadGroup.stop()抛出一个UnsupportedOperationException。

    该方法最终在 Java 23 中被删除。

    此更改没有 JEP;它在JDK-8320786下的错误跟踪器中注册。

    5. Java 23 中的其他变化

    在本节中,你会发现大多数 Java 开发人员在日常工作中不会遇到的变化。当然,了解这些变化还是有好处的。

    5.1 ZGC:默认采用分代模式 – JEP 474

    Java 21引入了 Z 垃圾收集器(ZGC)的“分代模式”。在此模式下,ZGC 使用“弱分代假设”,将新旧对象存储在两个单独的区域中:“年轻代”和“老一代”。年轻代主要包含短寿命对象,需要更频繁地清理,而老一代包含长寿命对象,需要清理的频率较低。

    在 Java 21 中,必须使用 VM 选项激活代际模式-XX:+UseZGC -XX:+ZGenerational

    由于代际模式在大多数情况下都可以显著提高性能,因此根据JDK 增强提案 474的规定,该模式在 Java 23 中默认激活。

    这意味着 VM 选项-XX:+UseZGC会自动以分代模式激活 ZGC。

    您可以使用 停用代际模式-XX:+UseZGC -XX:-ZGenerational

    5.2 删除模块 jdk.random

    此更改未归类到“删除”下,因为实际上未删除任何内容。jdk.random模块中的所有类都已移至java.base模块中。

    如果您使用 Java 模块系统并且已requires jdk.random在某处指定,则可以在 Java 23 中删除此语句(java.base模块自动包含)。

    此更改没有 JEP;它在JDK-8330005下的错误跟踪器中注册。

    5.3 具有显式区域设置的控制台方法

    利用ConsoleJava 6引入的类,我们可以方便地将文本打印到控制台,并从控制台读取用户输入:

    Console console = System.console();
    
    var name = console.readLine("What's your name (by the way, π = %.4f)? ", Math.PI);
    var password = console.readPassword("Your password (by the way, e = %.4f)? ", Math.E);
    
    console.printf("Your name is %s%n", name); // `printf` and `format` do the same
    console.format("Your password starts with %c%n", password[0]);
    

    这些方法始终使用默认区域设置。根据语言设置,Pi打印为3.1415(带点)或31415(带逗号)。
    从Java 23开始,您可以将Locale指定为printf(…)、format(…),readLine(…)和readPassword(…)方法的附加参数:

    Console console = System.console();
    
    var name = console.readLine(Locale.US, "What's your name (π = %.4f)? ", Math.PI);
    var password = console.readPassword(Locale.US, "Your password (e = %.4f)? ", Math.E);
     
    console.printf(Locale.US, "Your name is %s%n", name);
    console.format(Locale.US, "Your password starts with %c%n", password[0]);
    

    在此示例中,Pi 现在始终以美国格式打印,即 3.1415。

    此更改没有 JEP;它在JDK-8330276下的错误跟踪器中注册。

    5.4 支持持续时间直至另一个瞬间

    要确定两个对象之间的持续时间Instant,以前必须使用Duration.between(…):

    Instant now = Instant.now();
    Instant later = Instant.now().plus(ThreadLocalRandom.current().nextInt(), SECONDS);
    Duration duration = Duration.between(now, later);
    

    由于这种方法不容易找到,因此Instant.until(…)引入了一种执行相同计算的新方法:

    Instant now = Instant.now();
    Instant later = Instant.now().plus(ThreadLocalRandom.current().nextInt(), SECONDS);
    Duration duration = now.until(later);
    

    此更改没有 JEP;它在JDK-8331202下的错误跟踪器中注册。

    5.5 Java 23 中所有变更的完整列表

    在本文中,您了解了 JDK 增强提案 (JEP) 带来的所有 Java 23 功能以及发行说明中的​​一些其他选定更改。您可以在Java 23 发行说明中找到所有更改的完整列表。

    6. 总结

    Java 23 为我们带来了三个新功能和许多更新的预览功能

  • 未来编写和阅读JavaDoc注释将更容易,因为我们现在也可以使用Markdown

  • 我们还可以使用import module导入整个模块,而不是像以前那样只导入类和包,从而使.java文件import块更加清晰。

  • 基元类型模式通过基元类型扩展了Java模式匹配功能。然而,我无法想象我们会在代码中大量使用这种模式匹配(与Java在以前版本中添加的模式匹配功能相反)。

  • 在隐式声明的类中,我们现在可以编写println(…)而不是System.out.println(…)

  • ScopedValue.callWhere(…)现在传递了一个类型化的CallableOp,这样编译器就可以自动识别被调用的操作是否会抛出已检查的异常,如果是,是哪个异常。这意味着我们不再需要处理通用的Exception,而是处理实际抛出的Exception。因此,可以省略单独的ScopedValue.getWhere(…)方法。

  • 在派生类的构造函数中,我们现在可以在调用super(…)之前初始化派生类的字段。如果父类的构造函数调用在派生类中被覆盖的方法并访问这些字段,这将很有帮助。

  • 任何使用Z垃圾回收器的人在升级到Java 23时都会自动受益于新一代模式,使大多数应用程序的性能明显提高。

  • 还有一个主要的整理:Thread.supled()Thread.resume()Thread Group.suped()ThreadGroup.resumme()ThreadGroup.stop()方法,这些方法多年来一直被标记为不推荐使用,最终在Java 23中被删除。所有不安全的内存访问方法都已标记为不推荐删除。

  • 您最期待 Java 23 的哪个功能?您最怀念哪个功能?通过评论区咱们一起讨论下吧!

    作者:龙殿殿主

    物联沃分享整理
    物联沃-IOTWORD物联网 » Java 23新特性揭秘:super不再是构造函数首条语句(附示例详解)

    发表回复