集美大学 计算机 郑如滨

教学博客

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Java 17 新增的语法特性

说明:本文代码详见《面向实践的Java程序设计教程》教材的代码仓库

JEP与Project Amber

在Java语言的发展过程中,如果需要添加新的语法特性,可以先由发起者以JEP(Java Enhancement Proposal,Java增强提案)文档的形式提出,然后通过初步评估、创建JEP、社区反馈、JEP评审、批准或拒绝、预览等一系列步骤,才可能正式发布。
Open JDK推出的Project Amber项目是用来探索和孵化已经被JEP接受的较小的Java语言特性。本文中所介绍的Java新特性,通常会在后面标上JEP,这是用来说明该语法特性是属于哪个JEP。在搜索引擎中输入JEP编号,即可找到对应的JEP文档。

引言

Java 17于2021年9月14日发布,新增了多个语法特性与类。其中,最值得关注的特性包括:Record、Text Blocks、Pattern Matching for instanceof、Sealed Classes等。这些特性有的是在Java 17发布,有的则是在Java 17之前Java 11之后发布。本文将详细介绍这些新特性,并提供相应的示例代码。

1. Records [JEP 395]

Java 16发布的Record 类型是一种新的数据类型,用于表示不可变的数据,它允许我们创建一个不可变的类型。Record类型只包含数据字段访问器方法,不包含可变状态或自定义方法。
通过record关键字,可以轻松地创建不可变的数据传输对象(DTO)。DTO的英文全称是Data Transfer Object,它是一种用于数据传输的简单POJO(Plain Old Java Object)。Record类型适用于存储纯粹的值类型数据,如坐标点和只读的日志记录。当你需要一个类型仅用来存储数据的时候,就使用Record类型。

例子:现在需要定义一个表示点的record类型Point,该类型包含两个字段x和y,以及相应的访问器方法x()y()。可以使用如下代码:

public class PointRecordMain {
    public static void main(String[] args) {
        // 定义了record类型,也可以单独定义到一个文件中
        record Point(int x, int y) {}; 
  
        // record类型的使用
        Point origin = new Point(0, 0);
        Point dest = new Point(0, 0);
        System.out.println(origin); // 调用origin自动生成的toString()方法
        System.out.println(origin.x()); // 获得x的属性值
        System.out.println(origin.y());
        System.out.println(origin.equals(dest)); // 比较所有属性的值,返回true
        System.out.println(origin == dest); // 返回false,因为比较的是引用地址
    }
}

程序输出:

Point[x=0, y=0]
0
0
true
false

代码分析:

Point虽然没有定义任何的构造方法、getter方法、equals方法等,但是编译器会自动生成这些方法。
toString()方法:返回包含所有属性的字符串表示形式。
x()、y()访问器方法:回对应属性的值。
equals方法:会比较所有属性的值是否相等,类似于使用Objects.equals()方法。这使得record类型实例调用equals方法时,会比较所有属性的值,而不是引用地址。
实际上,使用record关键字定义的类型直接继承自java.lang.Record类,如果在一个文件中有多个record类型,将会生成对应的多个.class文件。可以使用javap 对应的字节码文件.class来查看验证。

record类型可以定义在一个单独的文件中,也可以如例程所示在需要使用的方法中直接定义。record类型还支持定义多个构造方法,以及默认构造方法,详见本书所附代码仓库中的PointRecordMain.java

参考资料:

JDK有用的新特性-Java Record

2. switch表达式与yield[JEP 361]

在Java 17中,引入了switch表达式(Switch Expressions)、yield关键字与->符号。
可以在标签case(label case)中使用yield关键字返回一个值。
示例代码如下所示:

public class SwitchMain {
    public static void main(String[] args) {
        String day = "Monday";

        String r = switch (day) { // switch表达式
        case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> {
            System.out.println("进入分支1");
            yield "工作日"; // 相当于return "工作日"
        }
        case "Saturday", "Sunday" -> "休息日"; // 无需break与yield,直接返回
        default -> throw new IllegalArgumentException("输入错误");
        };

        System.out.println(day + "是" + r);
    }
}

程序输出:

进入分支1
Monday是工作日

在这段代码中,switch表达式返回了一个字符串,并将其赋给变量r。在case分支中,使用yield关键字来返回一个值。这样,switch表达式就可以返回一个值,并且无需使用break来进行中断。如果->后没有大括号,则表示返回一个表达式。代码case "Saturday", "Sunday" -> "休息日";中虽然没有使用yield关键字,但是它返回了字符串"休息日"。

注意:switch块中的->右侧可以是代码块、表达式或者是手动抛出的异常。

3. 用于instanceof的模式匹配[JEP 394]

在Java 16中,就可以使用模式匹配(Pattern Matching)和instanceof关键字来处理类型检查和匹配,可以无需进行显式的类型转换。如下代码所示:

record Person(String name, int age) {
};

public class PatternMatchMain {
    public static void main(String[] args) {
        Object obj = new Person("李耀", 18);

        if (obj instanceof Person p) { // 如为true,则将obj转化为Person类型然后赋给模式变量p
            // 无需强制类型转换,直接使用转换后的p
            System.out.println(p.age());
        } else if (obj instanceof String s) {
            // 如果是字符串则调用其toLowerCase()
            System.out.println(s.toLowerCase());
        }

        // instanceof语句后拼接其他条件
        if (obj instanceof Person p && p.age() > 16) {
            System.out.println("age = " + p.age() + " > " + 16);
        }
    }
}

程序输出:

18
age = 18 >16

代码obj instanceof Person p中的Person p就是类型模式(type pattern),p是模式变量(pattern variable)。如果obj instanceof Person返回true,那么obj就会被转化为Person类型并赋值给变量p。

注意:模式变量只是一个特殊的局部变量,有其作用域,不能在作用域外使用。哪些算是作用域外?无需逐步分析,直接让Java编译器帮我们判定即可。

4. 文本块[JEP 378]

在Java 17中的文本块(Text Blocks)是一种新的字符串字面量,它允许我们在编写多行字符串时更加方便和易于阅读。文本块使用三个反引号(```)来包裹字符串,并且可以在字符串中包含换行符、制表符和其他特殊字符。如下代码所示:

public class TextBlockMain {
    public static void main(String[] args) {
        String textBlock = """
                这是一个"多行字符串"
                可以包含制表符\t我前面是制表符
                还可以包含换行符\n我前面是换行符
                  我前面是2个空格。
                接下来是一个Person对象的json字符串
                {
                  "name": "John Doe",
                  "age": 30,
                }
                                """;
        System.out.println(textBlock);
    }
}

程序输出:

这是一个"多行字符串"
可以包含制表符    我前面是制表符
还可以包含换行符
我前面是换行符
  我前面是2个空格。
接下来是一个Person对象的json字符串
{
  "name": "John Doe",
  "age": 30,
}

文本块常用于编写多行注释和文档字符串,还可用来编写SQL查询语句、JSON字符串等。
在实际编写文本块的时候,可以先在一个文本编辑软件中编写好文本块,然后复制到Java代码中。

更多文本块的用法可参考文章:

【升级到Java 17】文本块

5. 密封类[JEP 409]

在Java 17中,密封类(sealed class)允许我们限制一个类的继承范围或一个接口的实现范围,确保它只能被指定的子类继承或指定的类实现
密封类或密封接口可以有一个或多个允许的子类,这些子类必须是事先指定的,并且必须在同一个模块中声明。比如,某些类你只想让本模块中的其他类继承,而不想让其他模块中的类继承,就可以使用密封类来限制继承范围。

直接继承或实现一个sealed的类型的类只能声明为final, sealed或non-sealed,参考代码如下:

// 只允许Bird, Aircraft, UFO实现Flyable接口
sealed interface Flyable permits Bird, Aircraft, UFO {
}

/*直接继承或实现一个sealed的类型的类只能声明为final, sealed或non-sealed*/

// 1. 声明为final。其不能有子类。
final class Bird implements Flyable{
}

// 2. 声明为non-sealed。未封闭,其可以有子类
non-sealed class Aircraft implements Flyable{
}

class Helicopter extends Aircraft{
}

// 3. 声明为sealed。封闭,代表其只能拥有permits声明的子类。
sealed class UFO implements Flyable permits CircleUFO, RectUFO{
}

final class CircleUFO extends UFO{
}

final class RectUFO extends UFO{
}

在上述代码中,Flyable接口被声明为sealed,这意味着它只能被Bird, Aircraft, UFO三个类实现。Bird类被声明为final,这意味着它不能有子类。Aircraft类被声明为non-sealed,这意味着它允许有子类。UFO类被声明为sealed,这意味着它只能被CircleUFO, RectUFO两个类实现。

Java 17的其他主要改进

Java 17还有一些其他值得注意的改进,见下文:

稳定版的ZGC垃圾回收器

ZGC垃圾回收器(Z Garbage Collector)是JVM的一种低延迟垃圾回收器,在Java 11作为实验性特性引入。在Java 17中,ZGC成为稳定版,这意味着它将包含在JDK中,并且可以用于生产环境。ZGC作为JVM可以达到亚毫秒级停顿,不过需要手动开启。

主流Java开发框架支持

Spring Framework 6Spring Boot 3框架需要基于Java 17。

更清晰的NullPointerException异常信息提示

Java 17中对于NullPointerException异常信息提示进行了改进,以前的提示只能定位到行,Java 17中则能定位到具体某个变量。
以如下代码进行说明:

public class NullPointerExceptionMessageDemo {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length());
    }
}

运行后,会得到如下异常信息提示:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
可以看到,异常信息提示中的because "str" is null已经明确指出str是null。

Stream接口新增toList()方法

在Java 17中Stream接口正式增加了toList()方法,可以方便地将Stream转换为List,而无需使用Collectors.toList()方法。

如下代码所示:

import java.util.List;
// import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamToListDemo {
    public static void main(String[] args) {
        Stream<Integer> scoreStream = Stream.of(98, 97, 90);
        List<Integer> scoreList = scoreStream.toList();
        // Java 11中需要使用Collectors收集器
        // List<Integer> list = scoreStream.collect(Collectors.toList());
        
        for (Integer e : scoreList) {
            System.out.print(e+" ");
        }
    }
}
posted on 2024-06-13 18:02  zhrb  阅读(286)  评论(0编辑  收藏  举报