Java 8 vs Java 17 新特性

从 1995 年 JDK Beta 发布至今,Java 已迭代了 18 个大版本。其中 Java 8、11、17 为长期支持(LTS,Long-term support)的版本。 根据 JRebel 2022 年提供的报告,大部分人还在使用 Java 8,其次是 Java 11,但随着 Java 17(2021.09) 的发布,Java 将迎来新的格局。

Java 现状

jdk 使用情况

Java 8 位居榜首,其次是 Java 11,Java 7 或更早版本只占了 5%。

jdk17 升级趋势

综合来看,62% 的人打算在未来 12 个月内迁移到 JDK 17。8% 的人则表示不会升级

Java 9

接口中支持 private 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface InterfacePrivate {
/**
* JDK 9 支持 private 方法。
*/
private void p() {
}

/**
* JDK 8 支持 default、static 方法。
*/
default void d() {
}

/**
* JDK 7 版本之前,接口类只能定义一些抽象方法与常量,子类必须重写接口类中定义的全部方法。
*/
abstract void a();
}

各个版本的 JDK 接口对比

Java 7 及以前 Java 8 Java 9
常量
抽象方法
默认方法
静态方法
私有方法
私有静态方法

模块化

JDK 9 引入了一个新的特性叫做 JPMS(Java Platform Module System),项目代号为 Jigsaw

背景

我们知道在 Java 9 之前代码都是按包进行组织的,而模块则是在包的概念上又增加了一个更高层的抽象,它将多个逻辑、功能上相关的包及资源文件(如 xml 文件)组织成一个可重用的逻辑单元,这个逻辑单元就称为模块。听起来是不是和 gradle 或者 maven 中的模块很像

通常情况下模块中包含着一个模块描述符文件(module-info.class),用来指定模块的名字、依赖(每个模块必须显式声明依赖的模块)、对外暴露的包(其余的包则对外不可见)、模块提供的服务、模块使用的服务以及允许哪些模块可以对其进行反射等配置信息。模块最终都会打包成 jar 包来分发和使用,那么模块打包的 jar 包和普通的 jar 包有什么区别呢?模块和普通的 jar 包几乎是一样的,只不过比普通的 jar 包在根目录下多了一个模块描述符文件(module-info.class)而已。

模块化的目标

根据 JSR 376 规范的描述,模块化要实现以下几个目标:

  • 可靠的配置
    • 模块应该提供一种机制来 显式的声明模块间的依赖,从而可以寻着依赖路径提取出所有模块的一个子集来支撑系统的运行。
  • 强封装
    • 模块化机制要求模块中的包(package)只有在 显式的导出 后才可以被其他模块使用,并且其他模块必须 显式的声明 它需要这个模块的包之后才能使用这些包。这种机制可以提高安全性,攻击者能够访问的类越少越安全。
    • 此外,模块的强封装也有助于我们思考如何组织代码,获得更合理的设计。
  • 增强可扩展性和可维护性
    • 之前 Java 平台是作为一个整体部署到操作系统之上的,其中包含了不计其数的包和类,无论我们是否使用,它们都在那里。如果某些类需要更新或打补丁,则要替换整个 Java 平台,难以维护和扩展。
  • 可定制的运行环境
    • Java 9 将平台划分成了 95 个模块,通过使用模块化技术,我们可以定制运行环境,只包含硬件和应用需要的包和类即可。
    • 例如,设备不支持图形界面的话,我们就可以创建一个不包含 GUI 模块的运行环境,这将极大的降低运行环境的大小,节省不少空间。

JDK 8 和 JDK 17 的 jar 包对比

模块的类型

Java 9 的模块可以分为 3 种类型

  • 命名模块(Named Module)
    • 命名模块也称应用模块(Application Module),模块的根目录中存在声明文件 module-info.java
  • 无名模块(Unnamed Module)
    • 无名模块是在没有声明文件 module-info.java 的情况下构建的 jar。这意味着在 Java 8 和更早版本构建的所有 jar 都是无名模块。无名模块可以读取到其他所有的模块,并且也会将自身所有的包暴露出去,这为当前应用程序使用 Java 9 提供了极大的便利。
    • 虽然无名模块导出了自身所有的包,但这并不意味着命名模块可以读取无名模块,因为命名模块在 module-info.java 中无法声明对无名模块的依赖,无名模块导出所有包的目的在于让其他无名模块可以加载这些类
  • 自动模块(Automatic Module)
    • 如果命名模块想要读取没有 module-info.java 的 jar 怎么办?自动模块就是为了解决这个问题,你唯一需要做的就是将 jar 文件放在模块路径而不是类路径中。一个没有 module-info.java 的 jar 一旦放入模块路径,它就会自动成为一个自动模块。

模块声明

安装完 JDK 9 之后可以通过 java --list-modules 命令来列出所有的模块,这些模块都位于 $JAVA_HOME/jmods 目录下。

jdk 17 的模块,@ 后面是版本号

模块指令

java.naming 模块的声明文件

  • requires 指令。
    • 该指令用于指定当前模块的依赖模块,每个模块必须显式的声明依赖模块。
  • exports 和 exports to 指令。
    • exports 指令用于指定当前模块的导出包,而 exports to 指令则限定哪些模块可以访问该导出包。
  • uses 指令。
    • 该指令用于说明当前模块所使用的服务,使当前模块成为服务的消费者。
  • provides with 指令。
    • 该指令用于说明当前模块提供了某个服务的实现,使当前模块成为服务的提供者。

Java 11

支持类型推断

  • 局部变量支持类型推断,字段不支持类型推断。
  • 类型推断特别适合 for 循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 类型推断特别适合 for 循环。
*/
public class ForTypeInference {
public static void main(String[] args) {
for (var s : Spiciness.values()) {
System.out.println(s);
}
}
}

enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}

Java 14

switch 增强

switch 中可以使用 ->,这样就不用在每个分支后面增加 break。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 在 colons() 中,为了防止继续向下执行,每个 case 语句(除了最后的 default)后面都要加上 break。
* 而 arrows() 中将冒号替换为箭头之后,便不再需要 break 语句了。
*
* 注意:不能在一个 switch 中同时用冒号和箭头。
*
*/
public class ArrowInSwitch {
public static void main(String[] args) {
range(0, 4).forEach(i -> colons(i));
range(0, 4).forEach(i -> arrows(i));
}

static void colons(int i) {
switch (i) {
case 1:
System.out.println(1);
break;
case 2:
System.out.println(2);
break;
case 3:
System.out.println(3);
break;
default:
System.out.println(0);
}
}

static void arrows(int i) {
switch (i) {
case 1 -> System.out.println(1);
case 2 -> System.out.println(2);
case 3 -> System.out.println(3);
default -> System.out.println(0);
}
}
}

switch 一直以来都只是一个语句,不会生成结果。现在使用新的关键字 yield 可以从 switch 中返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* jdk14 使得 switch 还可以作为一个表达式来使用,因此它可以得到一个值。使用新的关键字 yield 可以从 switch 中返回结果。
* 如果一个 case 需要多个语句,就将他们放在一对花括号中。
*/
public class SwitchExpression {
public static void main(String[] args) {
for (var s : new String[]{"i", "j", "k"}) {
System.out.format("%s %d %d \n", s, colon(s), arrow(s));
}
}

static int colon(String s) {
var result = switch (s) {
case "i":
yield 1;
case "j":
yield 2;
default:
yield 0;
};
return result;
}

static int arrow(String s) {
var result = switch (s) {
case "i" -> 1;
case "j" -> 2;
default -> {
System.out.println("default");
yield 0;
}
};
return result;
}
}

Java 15

文本块

jdk 15 中添加了文本块(text block),这是从 python 借鉴而来的一个新特性。"""你好""" 这种不换行的写法是不支持的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 我们使用三引号来表示包含换行符的文本块。文本块可以让我们更轻松地创建多行文本。
*/
public class TextBlocks {
public static final String OLD="Yesterday, upon the stair,\n"+"I met a man who wasn't there\n";
public static final String NEW = """
Yesterday, upon the stair,
I met a man who wasn't there
""";
//不支持的写法。
//public static final String NO_SUPPORT = """你好""";
public static void main(String[] args) {
System.out.println(OLD.equals(NEW));
}
}

为了支持文本块,String 类里添加了一个新的 formatted() 方法。formatted() 是一个成员方法,是为了仿照 String.format() 这个静态方法而设计的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 文本块的结果是一个常规的字符串,所以其它字符串能做的事情,它也可以做。
*/
public class DataPoint {
private String location;
private Double temperature;

public DataPoint(String loc, Double temp) {
location = loc;
temperature = temp;
}

/**
* 支持格式化输出
*/
@Override
public String toString() {
return """
location: %s
temperature: %.2f
""".formatted(location, temperature);
}

public static void main(String[] args) {
var hill = new DataPoint("hill", 45.2);
var dale = new DataPoint("dale", 65.2);
System.out.println(hill);
System.out.println(dale);
}
}

更详细的 NullPointException 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 更详细的 NullPointerException 信息。
*
*/
public class BetterNullPointerReports {
public static void main(String[] args) {
C[] cs = {new C(new B(new A(null))),
new C(new B(null)),
new C(null)
};
for (C c : cs) {
try {
System.out.println(c.b.a.s);
} catch (NullPointerException e) {
System.out.println(e);
}

}
}
}

class A {
String s;

A(String s) {
this.s = s;
}
}

class B {
A a;

B(A a) {
this.a = a;
}
}

class C {
B b;

C(B b) {
this.b = b;
}
}

执行结果

Java 16

增加 record 关键字

要让一个类的对象可以用作 Map(或 Set )中的键,需要重写 equals() 和 hashCode() 方法。写对很难,如果以后要修改这个类的话,还很容易破坏它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* JDK16 增加了 record 关键字。record 定义的是希望成为数据传输对象的类。
* 当使用 record 关键字时,编译器会自动生成:
* 1.不可变字段。
* 2.一个全参的构造器。
* 3.每个元素都有的访问方法。
* 4.equals()
* 5.hashCode()
* 6.toString()
* 对于大多数 record,我们只需要给它一个名字和参数,不需要在定义体中添加任何东西。
* 不能向 record 中添加字段,只能将其定义在头部。不过可以加入静态的方法、字段和初始化器。
*/
public class CopyRecord {
public static void main(String[] args) {
var r1 = new R(1, 2.0, 'r');
var r2 = new R(r1.a(), r1.b(), r1.c());
System.out.println(r1.equals(r2));
}
}

record R(int a, double b, char c) {

}

instanceof 增强

一旦确定类型后,就永远不需要对其转型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* jdk16 中增加了针对 instanceof 的模式匹配(Pattern Matching for instanceof)。
*/
public class SmartCasting {
public static void main(String[] args) {
dumb("dumb");
smart("smart");
}

static void dumb(Object x) {
if (x instanceof String) {
String s = (String) x;
if (s.length() > 0) {
System.out.format("%d %s %n", s.length(), s.toLowerCase());
}
}
}

static void smart(Object x) {

if (x instanceof String s && s.length() > 0) {
System.out.format("%d %s %n", s.length(), s.toLowerCase());
}
}

/**
* 在 if 智能转型表达式中只能使用 &&。使用 || 则意味着可能 x 是个 instanceof String,或者 s.length > 0。
* 这意味着 x 可能不是 String,在这种情况下,Java 就不会将 x 智能转型生成 s,因此 s 在 || 右侧是不可用的。
*
* @param x
*/
static void wrong(Object x) {
//|| 永远不会生效。
//if (x instanceof String s || s.length() > 0) {}
}
}

Java 17

密封类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 密封类。
* 只有指定的类才能继承 BaseClass。
*/
sealed class BaseClass permits C1, C2, SubBaseClass {
}

/**
* 继承密封类的必须是 final 或 non-sealed。
*/
public final class C1 extends BaseClass {
}

/**
* 继承密封类的必须是 final 或 non-sealed。
*/
public final class C2 extends BaseClass {
}


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1. 一个 sealed 的基类无法阻止 non-sealed 的子类的使用,因此可以随时开放限制。
* 2. non-sealed 类可以允许任意数量的子类。
*/
public non-sealed class SubBaseClass extends BaseClass {
}

/**
* non-sealed 类可以被子类继承。
*/
public class SC1 extends SubBaseClass {
}

密封接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 密封接口。
* jdk 16 的 record 也可以用作接口的密封实现。因为 record 是隐式的 final,所以它们不需要在前面加 final 关键字。
*/
public sealed interface BaseInterface permits I1,R1 {
}

/**
* record 是隐式的 final,所以不需要再前面加 final 关键字。
*/
public record R1(String type) implements BaseInterface {
}


代码库链接

小结

细细想来,连 JDK 8(2014.3)的发布都已经是 8 年前的事情,更别说 JDK 5(2004.9),是时候拥抱变化,迎接 JDK 17 了。

引用