Java 17 有什么新功能?
微信搜索关注《Java学研大本营》,加入读者群,分享更多精彩
Java 17 于 2021 年 9 月发布,仅比之前的 LTS Java 11 晚了 3 年。它带来了很多新的变化,比如重新实现了整个 TCP 和 UDP Socket API 以及引入了两个新的垃圾收集器:ZGC 和 Shenandoha。
此版本中引入的语言功能包括:
-
切换表达式
-
文本块和格式化字符串
-
instanceof 的模式匹配
-
记录
-
密封类
-
本地类型
-
内部类静态成员 此外还引入了 jpackage,这是一个用于创建本地安装程序的有趣命令行工具。
切换表达式
与经典的 Switch 相比,Switch 表达式只执行激活的 case 内的代码,并且可以返回一个值。
public class SwitchExpressions {
private static final boolean LOVE_COLD = false;
public enum Season {SPRING, SUMMER, AUTUMN, WINTER}
public static String seasonMessage(Season season) {
return switch (season) {
case SPRING, AUTUMN -> "It is a good season.";
case SUMMER -> "Beach time!";
case WINTER -> {
if (LOVE_COLD) {
yield "Snow time!";
}
throw new RuntimeException("Ice cubes everywhere!");
}
};
}
}
返回的值类型必须在所有情况下和默认分支中都兼容。它可以直接在表达式 分支中返回(第 8-9 行)或在块分支中使用yield关键字(第 10-15 行)。
在块分支中,yield 与方法体中的 return 具有相同的功能。这意味着,如果某个条件流运算符用于在块分支内生成多个分支,则每个分支都必须产生一个值或抛出异常。
与普通 Switch 不同,switch 表达式始终需要默认分支,除非输入类型是枚举并且涵盖了所有情况,或者没有返回值。
文本块和格式化字符串
现在使用字符串更容易了。
public class TextBlocks {
public static void main(String[] args) {
var block = """
This Text has \
two lines.
"Text" is automatically escaped.""";
System.out.println(block);
/*
|This Text has two lines.
| "Text" is automatically escaped.
*/
var formatted = "A string can be formatted using %s method".formatted(".formatted()");
System.out.println(formatted);
/*
A string can be formatted using .formatted() method
*/
}
}
文本块允许编写不需要转义的字符串,例如 JSON。最左边字符之前的所有空格都会被自动删除。
另一方面,Formatted 是一种新的字符串实例方法,它允许直接格式化字符串而不使用String.format(...).
instanceof 的模式匹配
instanceof 的模式匹配与 if 语句一起用于减少样板文件并提高可读性。
public class PatternMatchingInstanceof {
public static void main(String[] args) {
Object o = "I'm a string.";
// old way
if (o instanceof String && !((String) o).isBlank()) {
System.out.println(((String) o).toUpperCase());
}
// new way
if (o instanceof String s && !s.isBlank()) {
System.out.println(s.toUpperCase());
}
}
}
新语法允许定义匹配类型的变量并使用它。新变量的范围由编译器计算并取决于检查的完成方式。在这种情况下,从检查后的第 9 行开始,到 if 正文结束的第 11 行结束。
public class GuessANumber {
private static final Random random = new Random();
public static void main(String[] args) {
Object o = random.nextInt(10) + 1;
while (!guessANumber(o)) {}
System.out.println("I won.");
}
private static boolean guessANumber(Object o) {
if (!(o instanceof Integer i)) {
throw new IllegalArgumentException("I can guess only numbers.");
}
if (i < 1 || i > 10) {
throw new IllegalArgumentException("I can guess only numbers between 1 and 10.");
}
var guess = random.nextInt(10) + 1;
System.out.println("Trying with " + guess);
return i.equals(guess);
}
}
在第二种情况下,在第 15 行之后,可以使用变量i ,因为o只能是整数。
记录
记录用于删除样板,给定 POJO 类:
final class POJOPoint {
private final double x;
private final double y;
POJOPoint(double x, double y) {
this.x = x;
this.y = y;
}
public double x() {
return x;
}
public double y() {
return y;
}
@Override
public boolean equals(Object o) {
return true; // my dummy example implementation
}
@Override
public int hashCode() {
return 0; // my dummy example implementation
}
@Override
public String toString() {
return "Point{x=%f, y=%f}".formatted(x, y);
}
}
可以替换为:
record RecordPoint(double x, double y) {
}
Records 允许以紧凑的方式定义一个不可变的类:
-
规范的所有参数构造函数
-
访问器方法(field()而不是getField())
-
equals(),hashCode()和toString()默认实现
然而,可以为任何这些方法和规范构造函数本身提供自定义实现。
public record Point(double x, double y, String label) implements Serializable {
public static final long serialVersionUID = 1;
public Point {
if (label == null || label.isBlank()) {
throw new IllegalArgumentException("Label cannot be blank.");
}
label = label.toUpperCase();
}
public Point(double x, double y) {
this(x, y, "POINT");
}
@Override
public String toString() {
return "%s(%f, %f)".formatted(label, x, y);
}
public static Point origin() {
return new Point(0, 0 , "origin");
}
public double distanceFrom(Point p) {
double xDistance = Math.pow(x - p.x, 2);
double yDistance = Math.pow(y - p.y, 2);
return Math.sqrt(xDistance + yDistance);
}
}
此外,记录可以:
-
实现任何接口,但不能扩展类(第 1 行)
-
在调用规范构造函数之前定义一个紧凑的构造函数来操作数据(第 4-9 行)
-
定义任何不同于 all args 的构造函数,但必须使用 all args 调用this(...)(第 11-13 行)
-
添加静态成员或方法(第 2 行,第 20-22 行)
-
添加实例方法,但不添加其他实例成员(第 24-28 行)
对于使用 Lombok 的人来说,它基本上是一个带有@Value注释的类。
密封类
密封类/接口限制了哪些其他类/接口可以直接实现/扩展它们。
sealed interface Shape permits Triangle, Rectangle, Polygon {
}
final class Triangle implements Shape {
}
sealed class Rectangle implements Shape permits Square {
}
final class Square extends Rectangle {
}
non-sealed class Polygon implements Shape{
}
permit子句中定义的每个类/接口都必须扩展/实现密封的类/接口。默认情况下,专利和子项必须在同一个包中,但在模块化应用程序中,可以在同一模块内的任何位置定义。
扩展/实现密封类/接口的类/接口必须是以下之一:
-
final:不允许子类。
-
密封:必须定义允许的子类/子接口。
-
non-sealed:这相当于一个普通的类/接口,没有任何限制。
本地类型
本地意味着在方法体内部,在 Java 11 中已经可以定义类,在 Java 17 中这种可能性已扩展到接口和枚举。
public class LocalTypes {
public static void main(String[] args) {
interface PrimaryColor { boolean isPrimary(); }
enum Color implements PrimaryColor {
RED(true), GREEN(true), BLUE(true),
BLACK(false), WHITE(false), YELLOW(false);
private final boolean primary;
Color(boolean primary) { this.primary = primary; }
@Override
public boolean isPrimary() { return primary; }
}
Stream.of(Color.values())
.filter(PrimaryColor::isPrimary)
.forEach(System.out::println);
}
}
需要指出的是,即使现在有可能,但这并不意味着必须这样做。在编写代码时,始终考虑可读性和可维护性是很重要的。
内部类静态成员
内部类,作为顶级类和嵌套类,现在可以定义静态成员(字段和方法)。
public class InnerClassStaticMembers {
public static void main(String[] args) {
Nested.print();
Inner.print();
}
class Inner {
static String message = "Static inside inner is new!";
static void print() {
System.out.println(message);;
}
}
static class Nested {
static String message = "Static inside nested is old!";
static void print() {
System.out.println(message);;
}
}
}
在 Java 17 之前,内部类和嵌套类都可以声明实例成员,但只有嵌套类可以声明静态成员。至于局部类型,内部类和嵌套类在某些情况下可能很方便,但它们的使用应该反映一个原因,它们不应该仅仅因为它们可以使用而使用。
jpackage
这个新的命令行工具为独立的 Java 应用程序创建安装程序。换句话说,给定一个 jar 应用程序和一个 JRE(Java 运行时环境),它将为不同的操作系统创建一个本机安装程序:
-
视窗:exe、msi
-
Linux:rpm、deb
-
Mac:pkg,dmg
让我们以模式匹配中的GuessANumber示例为例,假设包是my.example。作为第一步,我们需要将源代码编译并打包到一个 jar 文件中:
javac my/example/*.java
jar cfe GuessANumber.jar my.example.GuessANumber my/example/*.class
这相当于mvn compile package在 maven 项目中运行。
现在应用程序已准备就绪,可以使用以下命令创建安装程序:
jpackage --input . \
--main-jar GuessANumber.jar \
--main-class my.example.GuessANumber
安装程序和 JRE 类型由当前环境自动推断。给定一个 linux,创建一个 deb 文件。有了它,就可以安装包了,安装的可执行文件可以在/opt/guessanumber/bin/GuessANumber
找到。
垃圾收集器
从 Java 9 开始,默认的垃圾收集器是G1。它不能在应用程序运行时工作,但可以根据用户需要修剪暂停。更短的暂停为 GC 提供更多的 CPU 时间,更大的暂停让应用程序工作更长时间而不会停止。然而,如果工作量随着时间的推移而增加,性能就会下降。
Shenandoha是在 java 12 中引入的,它相对于G1的优势在于它能够与应用程序线程同时执行其工作。当它能够使用 Brooks 指针与应用程序同时重定位对象时,这是可能的。Brook 指针是堆中的每个对象都具有指向该对象的引用。
ZGC的目标是低延迟、可扩展性和易用性。作为Shenandoha,它允许主应用程序在执行垃圾收集的同时继续运行。此外,它可以将 Java 堆从数百 MB 扩展到 TB,保持低暂停时间。可预测的暂停简化了开发过程:无需设计避免垃圾收集的方法或垃圾收集器的性能专业知识。
结论
一个接一个地发布 Java 正试图成为一种更现代、更轻量级的编程语言。它需要与在 JVM 行 Kotlin 上运行的其他现代语言保持更新,同时,不要失去习惯它并且不想改变他们所知道的一切的旧开发人员。
从我的角度来看,它做得很好,添加了非常有趣的新工具,有助于以更优雅和可读的方式构建代码。我很期待看到下一个 LTS 中会发生什么。
参考
-
OCP Oracle Certified Professional Java SE 17 开发人员学习指南:考试 1Z0–829
-
jpackage 指南
-
了解 jdks 新的超高速垃圾收集器
推荐书单
《项目驱动零起点学Java》
《项目驱动零起点学Java》共分 13 章,围绕 6 个项目和 258 个代码示例,分别介绍了走进Java 的世界、变量与数据类型、运算符、流程控制、方法、数组、面向对象、异常、常用类、集合、I/O流、多线程、网络编程相关内容。《项目驱动零起点学Java》总结了马士兵老师从事Java培训十余年来经受了市场检验的教研成果,通过6 个项目以及每章的示例和习题,可以帮助读者快速掌握Java 编程的语法以及算法实现。扫描每章提供的二维码可观看相应章节内容的视频讲解。
《项目驱动零起点学Java》贯穿6个完整项目,经过作者多年教学经验提炼而得,项目从小到大、从短到长,可以让读者在练习项目的过程中,快速掌握一系列知识点。
马士兵,马士兵教育创始人,毕业于清华大学,著名IT讲师,所讲课程广受欢迎,学生遍布全球大厂,擅长用简单的语言讲授复杂的问题,擅长项目驱动知识的综合学习。马士兵教育获得在线教育“名课堂”奖、“最受欢迎机构”奖。
赵珊珊,从事多年一线开发,曾为国税、地税税务系统工作。拥有7年一线教学经验,多年线上、线下教育的积累沉淀,培养学员数万名,讲解细致,脉络清晰。
精彩回顾
微信搜索关注《Java学研大本营》
访问【IT今日热榜】,发现每日技术热点