【注】本文译自: What’s New Between Java 11 and Java 17?

    9 月 14 日 Java 17 公布。是时候认真看看自上一个 LTS 版本(即 Java 11)以来的变动。咱们先简要介绍许可模型,而后重点介绍 Java 11 和 Java 17 之间的一些变动,次要是通过 例子。享受吧!

1. 介绍

    首先,让咱们认真看看 Java 许可和反对模型。Java 17 是一个 LTS(长期反对)版本,就像 Java 11 一样。Java 11 开始了一个新的公布节奏。Java 11 反对到 2023 年 9 月,扩大反对到 2026 年 9 月。此外,在 Java 11 中,Oracle JDK 不再收费用于生产和商业用途。每 6 个月公布一个新的 Java 版本,即所谓的非 LTS 公布,从 Java 12 直至并包含 Java 16。然而,这些都是生产就绪版本。与 LTS 版本的惟一区别是反对在下一个版本公布时完结。例如。 Java 12 的反对在 Java 13 公布时完结。当您想要放弃反对时,您或多或少必须降级到 Java 13。当您的某些依赖项尚未为 Java 13 做好筹备时,这可能会导致一些问题。大多数状况下,对于生产用处,公司将期待 LTS 版本。但即便如此,一些公司也不违心降级。最近 Snyk 的一项考察显示,只有 60% 的人在生产中应用 Java 11,而这间隔 Java 11 公布曾经过来了 3 年!60% 的公司仍在应用 Java 8。另一个值得注意的乏味事件是,下一个 LTS 版本将是 Java 21,它将在 2 年内公布。对于库在 Java 17 是否存在问题的一个很好的概述,能够在此处找到。

    随着 Java 17 的推出,Oracle 许可模式产生了变动。Java 17 是依据新的 NFTC(Oracle 收费条款和条件)许可公布的。因而,再次容许收费将 Oracle JDK 版本用于生产和商业用途。在同一个 Snyk 考察中,有人指出 Oracle JDK 版本在生产环境中仅被 23% 的用户应用。请留神,对 LTS 版本的反对将在下一个 LTS 版本公布一年后完结。看看这将如何影响降级到下一个 LTS 版本将会很乏味。

    Java 11 和 Java 17 之间产生了什么变动?能够在 OpenJDK 网站上找到 JEP(Java 加强提案)的残缺列表。在这里,您能够浏览每个 JEP 的详细信息。 无关自 Java 11 以来每个版本更改的残缺列表,Oracle 发行阐明提供了一个很好的概述。

    在接下来的局部中,将通过示例解释一些更改,但次要取决于您对这些新性能进行试验以相熟它们。这篇文章中应用的所有资源都能够在 GitHub 上找到。

    最初一件事是 Oracle 公布了 dev.java,所以不要遗记看一下。

2. Text Blocks(本文块)

    为了使 Java 更具可读性和更简洁,曾经进行了许多改良。文本块无疑使代码更具可读性。首先,咱们来看看问题。假如您须要一些 JSON 字符串到您的代码中并且您须要打印它。这段代码有几个问题:

  • 双引号的本义;
  • 字符串连贯,使其具备或多或少的可读性;
  • JSON 的复制粘贴是一项劳动密集型的工作(您的 IDE 可能会帮忙您解决该问题)。

      private static void oldStyle() {      System.out.println("""              *************              * Old Style *              *************""");      String text = "{\n" +                    "  \"name\": \"John Doe\",\n" +                    "  \"age\": 45,\n" +                    "  \"address\": \"Doe Street, 23, Java Town\"\n" +                    "}";      System.out.println(text);  }

    下面代码的输入是格局良好的 JSON。

{    "name": "John Doe",    "age": 45,    "address": "Doe Street, 23, Java Town"}

    文本块用三个双引号定义,其中结尾的三个双引号不能与起始的在同一行。首先,只需打印一个空块。为了可视化产生了什么,文本被打印在两个双管之间。

    private static void emptyBlock() {        System.out.println("""                ***************                * Empty Block *                ***************""");        String text = """                """;        System.out.println("|" + text + "|");    }

    输入是:

||||

有问题的 JSON 局部当初能够写成如下,这样可读性更好。不须要本义双引号,它看起来就像会被打印。

    private static void jsonBlock() {        System.out.println("""                **************                * Json Block *                **************""");        String text = """                {                  "name": "John Doe",                  "age": 45,                  "address": "Doe Street, 23, Java Town"                }                """;        System.out.println(text);    }

    输入当然是雷同的。

{    "name": "John Doe",    "age": 45,    "address": "Doe Street, 23, Java Town"}

    在后面的输入中,没有后面的空格。然而,在代码中,后面有空格。如何确定剥离后面的空格? 首先,将结尾的三个双引号向左挪动更多。

    private static void jsonMovedBracketsBlock() {        System.out.println("""                *****************************                * Json Moved Brackets Block *                *****************************""");        String text = """                  {                    "name": "John Doe",                    "age": 45,                    "address": "Doe Street, 23, Java Town"                  }                """;        System.out.println(text);    }

    输入当初在每行之前打印两个空格。这意味着结尾的三个双引号示意文本块的开始。

{    "name": "John Doe",    "age": 45,    "address": "Doe Street, 23, Java Town"}123

    当你将结尾的三个双引号向右挪动时会产生什么?

    private static void jsonMovedEndQuoteBlock() {        System.out.println("""                ******************************                * Json Moved End Quote Block *                ******************************""");        String text = """                  {                    "name": "John Doe",                    "age": 45,                    "address": "Doe Street, 23, Java Town"                  }                       """;        System.out.println(text);    }

    后面的间距当初由文本块中的第一个非空格字符决定。

{    "name": "John Doe",    "age": 45,    "address": "Doe Street, 23, Java Town"}

3. Switch 表达式

    Switch 表达式将容许您从 switch 返回值并在赋值等中应用这些返回值。此处显示了一个经典的 switch,其中,依据给定的 Fruit 枚举值,须要执行一些操作。成心疏忽了 break。

    private static void oldStyleWithoutBreak(FruitType fruit) {        System.out.println("""                ***************************                * Old style without break *                ***************************""");        switch (fruit) {            case APPLE, PEAR:                System.out.println("Common fruit");            case ORANGE, AVOCADO:                System.out.println("Exotic fruit");            default:                System.out.println("Undefined fruit");        }    }

    应用 APPLE 调用该办法。

oldStyleWithoutBreak(Fruit.APPLE);

    这将打印每个 case,因为没有 break 语句,case 就生效了。

Common fruitExotic fruitUndefined fruit

    因而,有必要在每个 case 中增加一个 break 语句,以避免这种生效。

    private static void oldStyleWithBreak(FruitType fruit) {        System.out.println("""                ************************                * Old style with break *                ************************""");        switch (fruit) {            case APPLE, PEAR:                System.out.println("Common fruit");                break;            case ORANGE, AVOCADO:                System.out.println("Exotic fruit");                break;            default:                System.out.println("Undefined fruit");        }    }

    运行此办法会为您提供所需的后果,但当初代码的可读性稍差。

Common fruit

    这能够通过应用 Switch 表达式来解决。用箭头 (->) 替换冒号 (:) 并确保在大小写中应用表达式。Switch 表达式的默认行为是没有失败,因而不须要 break。

    private static void withSwitchExpression(FruitType fruit) {        System.out.println("""                **************************                * With switch expression *                **************************""");        switch (fruit) {            case APPLE, PEAR -> System.out.println("Common fruit");            case ORANGE, AVOCADO -> System.out.println("Exotic fruit");            default -> System.out.println("Undefined fruit");        }    }

    这曾经不那么啰嗦了,后果是雷同的。

    Switch 表达式也能够返回一个值。在下面的示例中,您能够返回 String 值并将它们调配给变量 text。在此之后,能够打印 text 本变量。不要遗记在最初一个案例括号后增加一个分号。

    private static void withReturnValue(FruitType fruit) {        System.out.println("""                *********************                * With return value *                *********************""");        String text = switch (fruit) {            case APPLE, PEAR -> "Common fruit";            case ORANGE, AVOCADO -> "Exotic fruit";            default -> "Undefined fruit";        };        System.out.println(text);    }

    而且,更短的是,下面的内容能够用一个语句重写。这是否比下面的更具可读性取决于您。

    private static void withReturnValueEvenShorter(FruitType fruit) {        System.out.println("""                **********************************                * With return value even shorter *                **********************************""");        System.out.println(            switch (fruit) {                case APPLE, PEAR -> "Common fruit";                case ORANGE, AVOCADO -> "Exotic fruit";                default -> "Undefined fruit";            });    }

    当您须要在 case 中做不止一件事件时,您会怎么做? 在这种状况下,您能够应用方括号来示意 case 块,并在返回值时应用关键字 yield。

    private static void withYield(FruitType fruit) {        System.out.println("""                **************                * With yield *                **************""");        String text = switch (fruit) {            case APPLE, PEAR -> {                System.out.println("the given fruit was: " + fruit);                yield "Common fruit";            }            case ORANGE, AVOCADO -> "Exotic fruit";            default -> "Undefined fruit";        };        System.out.println(text);    }

    输入当初有点不同,执行了两个打印语句。

the given fruit was: APPLECommon fruit

    您能够在“旧” switch 语法中应用 yield 关键字也很酷。这里不须要 break。

    private static void oldStyleWithYield(FruitType fruit) {        System.out.println("""                ************************                * Old style with yield *                ************************""");        System.out.println(switch (fruit) {            case APPLE, PEAR:                yield "Common fruit";            case ORANGE, AVOCADO:                yield "Exotic fruit";            default:                yield "Undefined fruit";        });    }

4. Records(记录)

    Records 将容许您创立不可变的数据类。目前,您须要例如 应用 IDE 的主动生成函数创立 GrapeClass 以生成构造函数、getter、hashCode、equals 和 toString,或者您能够应用 Lombok 达到同样的目标。最初,您会失去一些样板代码,或者您的我的项目最终会依赖 Lombok。

public class GrapeClass {    private final Color color;    private final int nbrOfPits;    public GrapeClass(Color color, int nbrOfPits) {        this.color = color;        this.nbrOfPits = nbrOfPits;    }    public Color getColor() {        return color;    }    public int getNbrOfPits() {        return nbrOfPits;    }    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        GrapeClass that = (GrapeClass) o;        return nbrOfPits == that.nbrOfPits && color.equals(that.color);    }    @Override    public int hashCode() {        return Objects.hash(color, nbrOfPits);    }    @Override    public String toString() {        return "GrapeClass{" +                "color=" + color +                ", nbrOfPits=" + nbrOfPits +                '}';    }}

    应用上述 GrapeClass 类执行一些测试。创立两个实例,打印它们,比拟它们,创立一个正本并也比拟这个。

    private static void oldStyle() {        System.out.println("""                *************                * Old style *                *************""");        GrapeClass grape1 = new GrapeClass(Color.BLUE, 1);        GrapeClass grape2 = new GrapeClass(Color.WHITE, 2);        System.out.println("Grape 1 is " + grape1);        System.out.println("Grape 2 is " + grape2);        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));        GrapeClass grape1Copy = new GrapeClass(grape1.getColor(), grape1.getNbrOfPits());        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));    }

    测试的输入是:

Grape 1 is GrapeClass{color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1}Grape 2 is GrapeClass{color=java.awt.Color[r=255,g=255,b=255], nbrOfPits=2}Grape 1 equals grape 2? falseGrape 1 equals its copy? true

    GrapeRecord 具备与 GrapeClass 雷同的性能,但要简略得多。您创立一个记录并指出字段应该是什么,而后您就实现了。

record GrapeRecord(Color color, int nbrOfPits) {}

    一个记录能够在它本人的文件中定义,然而因为它十分紧凑,所以在须要的中央定义它也是能够的。下面用记录重写的测试变成如下:

    private static void basicRecord() {        System.out.println("""                ****************                * Basic record *                ****************""");        record GrapeRecord(Color color, int nbrOfPits) {}        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);        GrapeRecord grape2 = new GrapeRecord(Color.WHITE, 2);        System.out.println("Grape 1 is " + grape1);        System.out.println("Grape 2 is " + grape2);        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));        GrapeRecord grape1Copy = new GrapeRecord(grape1.color(), grape1.nbrOfPits());        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));    }

    输入与下面雷同。重要的是要留神记录的正本应该以雷同的正本完结。增加额定的性能,例如 grape1.nbrOfPits() 为了做一些解决并返回与初始 nbrOfPits 不同的值是一种不好的做法。尽管这是容许的,但您不应该这样做。

    构造函数能够通过一些字段验证进行扩大。请留神,将参数调配给记录字段产生在构造函数的开端。

    private static void basicRecordWithValidation() {        System.out.println("""                ********************************                * Basic record with validation *                ********************************""");        record GrapeRecord(Color color, int nbrOfPits) {            GrapeRecord {                System.out.println("Parameter color=" + color + ", Field color=" + this.color());                System.out.println("Parameter nbrOfPits=" + nbrOfPits + ", Field nbrOfPits=" + this.nbrOfPits());                if (color == null) {                    throw new IllegalArgumentException("Color may not be null");                }            }        }        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);        System.out.println("Grape 1 is " + grape1);        GrapeRecord grapeNull = new GrapeRecord(null, 2);    }

    上述测试的输入向您展现了此性能。 在构造函数外部,字段值依然为 null,但在打印记录时,它们被调配了一个值。验证也做它应该做的事件,并在色彩为 null 时抛出 IllegalArgumentException。

Parameter color=java.awt.Color[r=0,g=0,b=255], Field color=nullParameter nbrOfPits=1, Field nbrOfPits=0Grape 1 is GrapeRecord[color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1]Parameter color=null, Field color=nullParameter nbrOfPits=2, Field nbrOfPits=0Exception in thread "main" java.lang.IllegalArgumentException: Color may not be null    at com.mydeveloperplanet.myjava17planet.Records$2GrapeRecord.<init>(Records.java:40)    at com.mydeveloperplanet.myjava17planet.Records.basicRecordWithValidation(Records.java:46)    at com.mydeveloperplanet.myjava17planet.Records.main(Records.java:10)

5. Sealed Classes(密封类)

    密封类将让您更好地管制哪些类能够扩大您的类。密封类可能更像是一个对库所有者有用的性能。一个类在 Java 11 final 中或者能够扩大。如果您想管制哪些类能够扩大您的超类,您能够将所有类放在同一个包中,并赋予超类包可见性。当初一切都在您的管制之下,然而,不再可能从包内部拜访超类。让咱们通过一个例子来看看这是如何工作的。

    在包
com.mydeveloperplanet.myjava17planet.nonsealed 中创立一个具备公共可见性的抽象类 Fruit。在同一个包中,创立了最终的类 Apple 和 Pear,它们都扩大了 Fruit。

public abstract class Fruit {}public final class Apple extends Fruit {}public final class Pear extends Fruit {}

    在包
com.mydeveloperplanet.myjava17planet 中创立一个带有 problemSpace 办法的 SealedClasses.java 文件。如您所见,能够为 Apple、 Pear 和 Apple 创立实例,能够将 Apple 调配给 Fruit。除此之外,还能够创立一个扩大 Fruit 的 Avocado 类。

public abstract sealed class FruitSealed permits AppleSealed, PearSealed {}public non-sealed class AppleSealed extends FruitSealed {}public final class PearSealed extends FruitSealed {}

    假如您不心愿有人扩大 Fruit。 在这种状况下,您能够将 Fruit 的可见性更改为默认可见性(删除 public 关键字)。在将 Apple调配给 Fruit 和创立 Avocado 类时,上述代码将不再编译。后者是须要的,但咱们的确心愿可能将一个 Apple 调配给一个 Fruit。这能够在带有密封类的 Java 17 中解决。

在包
com.mydeveloperplanet.myjava17planet.sealed 中,创立了 Fruit、Apple 和 Pear 的密封版本。惟一要做的就是将 sealed 关键字增加到 Fruit 类中,并应用 permits 关键字批示哪些类能够扩大此 Sealed 类。子类须要指明它们是 final、 sealed 还是 non-sealed。超类无法控制子类是否能够扩大以及如何扩大。

public abstract sealed class FruitSealed permits AppleSealed, PearSealed {}public non-sealed class AppleSealed extends FruitSealed {}public final class PearSealed extends FruitSealed {}

    在 sealedClasses 办法中,依然能够将 AppleSealed 调配给 FruitSealed,但 Avocado 不容许扩大 FruitSealed。 然而,容许扩大 AppleSealed 因为这个子类被批示为非密封的。

    private static void sealedClasses() {        AppleSealed apple = new AppleSealed();        PearSealed pear = new PearSealed();        FruitSealed fruit = apple;        class Avocado extends AppleSealed {};    }

6. instanceof 的模式匹配

    通常须要查看对象是否属于某种类型,如果是,首先要做的是将对象强制转换为该特定类型的新变量。能够在以下代码中看到一个示例:

private static void oldStyle() {System.out.println(""" ************* * Old Style * *************""");Object o = new GrapeClass(Color.BLUE, 2);if (o instanceof GrapeClass) {GrapeClass grape = (GrapeClass) o;System.out.println("This grape has " + grape.getNbrOfPits() + " pits."); } }

    输入是:

This grape has 2 pits.

    应用 instanceof 的模式匹配,下面的能够改写如下。如您所见,能够在 instanceof 查看中创立变量,并且不再须要用于创立新变量和转换对象的额定行。

    private static void patternMatching() {        System.out.println("""                ********************                * Pattern matching *                ********************""");        Object o = new GrapeClass(Color.BLUE, 2);        if (o instanceof GrapeClass grape) {            System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");        }    }

    输入当然与下面雷同。

    认真查看变量的范畴很重要。它不应该是不置可否的。在上面的代码中,&& 之后的条件只会在 instanceof 查看后果为 true 时进行评估。 所以这是容许的。将 && 更改为 || 不会编译。

    private static void patternMatchingScope() {        System.out.println("""                *******************************                * Pattern matching scope test *                *******************************""");        Object o = new GrapeClass(Color.BLUE, 2);        if (o instanceof GrapeClass grape && grape.getNbrOfPits() == 2) {            System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");        }    }

    上面的代码显示了另一个无关范畴的示例。如果对象不是 GrapeClass 类型,则抛出 RuntimeException。在这种状况下,永远不会达到打印语句。在这种状况下,也能够应用 grape 变量,因为编译器必定晓得 grape 存在。

    private static void patternMatchingScopeException() {        System.out.println("""                **********************************************                * Pattern matching scope test with exception *                **********************************************""");        Object o = new GrapeClass(Color.BLUE, 2);        if (!(o instanceof  GrapeClass grape)) {            throw new RuntimeException();        }        System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");    }

7.有用的空指针异样

    有用的 NullPointerException 将为您节俭一些贵重的剖析工夫。以下代码导致 NullPointerException。

public class HelpfulNullPointerExceptions {    public static void main(String[] args) {        HashMap<String, GrapeClass> grapes = new HashMap<>();        grapes.put("grape1", new GrapeClass(Color.BLUE, 2));        grapes.put("grape2", new GrapeClass(Color.white, 4));        grapes.put("grape3", null);        var color = ((GrapeClass) grapes.get("grape3")).getColor();    }}

    对于 Java 11,输入将显示 NullPointerException 产生的行号,但您不晓得哪个链式办法解析为 null。你必须通过调试的形式找到本人。

Exception in thread "main" java.lang.NullPointerException        at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)

    在 Java 17 中,雷同的代码会产生以下输入,其中精确显示了 NullPointerException 产生的地位。

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.mydeveloperplanet.myjava17planet.GrapeClass.getColor()" because the return value of "java.util.HashMap.get(Object)" is null    at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)

 8. 精简数字格局反对

    NumberFormat 中增加了一个工厂办法,以便依据 Unicode 规范以紧凑的、人类可读的模式格式化数字。 SHORT 格局款式如上面的代码所示:

        NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);        System.out.println(fmt.format(1000));        System.out.println(fmt.format(100000));        System.out.println(fmt.format(1000000));

    输入是:

1K100K1M

    LONG 格局款式:

fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);System.out.println(fmt.format(1000));System.out.println(fmt.format(100000));System.out.println(fmt.format(1000000));

    输入是:

1 thousand100 thousand1 million荷兰语替换英语的 LONG 格局:
fmt = NumberFormat.getCompactNumberInstance(Locale.forLanguageTag("NL"), NumberFormat.Style.LONG);System.out.println(fmt.format(1000));System.out.println(fmt.format(100000));System.out.println(fmt.format(1000000));

    输入是:

1 duizend100 duizend1 miljoen

9. 增加了日周期反对

    增加了一个新模式 B 用于格式化 DateTime,该模式依据 Unicode 规范批示日期时间段。

    应用默认的中文语言环境,打印一天中的几个时刻:

System.out.println(""" ********************** * Chinese formatting * **********************""");DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");System.out.println(dtf.format(LocalTime.of(8, 0)));System.out.println(dtf.format(LocalTime.of(13, 0)));System.out.println(dtf.format(LocalTime.of(20, 0)));System.out.println(dtf.format(LocalTime.of(23, 0)));System.out.println(dtf.format(LocalTime.of(0, 0)));

    输入是:

上午下午早晨早晨午夜

    当初应用荷兰语本地环境:

System.out.println(""" ******************** * Dutch formatting * ********************""");dtf = DateTimeFormatter.ofPattern("B").withLocale(Locale.forLanguageTag("NL"));System.out.println(dtf.format(LocalTime.of(8, 0)));System.out.println(dtf.format(LocalTime.of(13, 0)));System.out.println(dtf.format(LocalTime.of(20, 0)));System.out.println(dtf.format(LocalTime.of(0, 0)));System.out.println(dtf.format(LocalTime.of(1, 0)));

    输入如下。请留神,英国之夜从 23 点开始,荷兰之夜从 01 点开始。可能是文化差异;-)。

’s ochtends’s middags’s avondsmiddernacht’s nachts

10. Stream.toList()

    为了将 Stream 转换为 List,您须要应用 collect 的 Collectors.toList() 办法。这十分简短,如上面的示例所示。

    private static void oldStyle() {        System.out.println("""                        *************                        * Old style *                        *************""");        Stream<String> stringStream = Stream.of("a", "b", "c");        List<String> stringList =  stringStream.collect(Collectors.toList());        for(String s : stringList) {            System.out.println(s);        }    }

    在 Java 17 中,增加了一个 toList 办法来替换旧的行为。

    private static void streamToList() {        System.out.println("""                        *****************                        * stream toList *                        *****************""");        Stream<String> stringStream = Stream.of("a", "b", "c");        List<String> stringList =  stringStream.toList();        for(String s : stringList) {            System.out.println(s);        }    }

11. 论断

    在本文中,您疾速浏览了自上一个 LTS 版本 Java 11 以来增加的一些性能。当初由您开始思考迁徙到 Java 17 的打算,以及理解无关这些新性能的更多信息以及您如何 能够将它们利用到您的日常编码习惯中。提醒:IntelliJ 会帮你解决这个问题!