文章目录
- 一、简介
- 二、新特性
- 接口私有方法(JDK9)
- String存储结构的变化(JDK9)
- 快速创建只读集合(JDK9、10)
- 文本块(JDK13、14、15)
- 更直观的 NullPointerException 提示(JDK14)
- 密封类(JDK15、16、17)
- instanceof的模式匹配(JDK14、15、16)
- switch增强(JDK12、13、17)
- 垃圾收回相关优化(JDK9-17)
- JDK9
- JDK10
- JDK11
- JDK12
- JDK13
- JDK14
- JDK15
- JDK16
- 三、总结
一、简介
由于 SpringBoot 3 最低支持的 JDK 版本是 17 了,加上 JDK 17 是一个 LTS 版本(Long-Term Support,长期稳定可靠的版本),所以还是有必要学习下 JDK 9 - JDK17 的新特性的。写这篇文章帮助自己记忆,也方便后续查阅。
关于 JDK 各版本新特性可查看网址:https://openjdk.org/
二、新特性
下面是我认为比较重要的一些新特性,有些感觉平时不会用的语法糖就不讲了,有兴趣的可以自己去了解了解
接口私有方法(JDK9)
JEP 213: Milling Project Coin
在讲 JDK 9 新特性 —— 接口私有方法之前,我们先了解下在之前的 JDK 版本中接口的变化。
在 JDK 8 之前,我们接口里只存在常量、抽象方法
public interface MyInterface {
/**
* 常量(默认就是public abstract final修饰的)
*/
String CONSTANT = "常量";
/**
* 抽象方法(默认就是public abstract修饰的)
*/
void abstractMethod();
}
在 JDK 8 版本里,增加了默认方法和静态方法
默认方法可以定义方法的默认实现,使用 default
关键字修饰
public interface MyInterface {
/**
* 默认方法
*/
default void defaultMethod() {
System.out.println("这是默认方法");
}
}
public class MyClass implements MyInterface{
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.defaultMethod();
}
}
运行结果:
这是默认方法
因为实现类可以不重写接口中的默认方法,所以可以往现存接口中添加新的方法,不用强制那些已经实现了该接口的类也同时实现这个新加的方法,非常棒!
此外还可以在接口里定义静态方法,使用 static
关键字修饰
public interface MyInterface {
/**
* 静态方法
*/
static void staticMethod() {
System.out.println("这是静态方法");
}
}
public class MyTest{
public static void main(String[] args) {
MyInterface.staticMethod();
}
}
运行结果:
这是静态方法
有的同学可能已经发现问题了,如果我多个默认方法用到了同一段代码,岂不是要写很多重复代码,于是 JDK 9 在接口里增加了私有方法,可以将相同的代码提取到私有方法里
public interface MyInterface {
/**
* 默认方法
*/
default void defaultMethod() {
System.out.println("这是默认方法");
privateMethod();
}
/**
* 默认方法2
*/
default void defaultMethod2() {
System.out.println("这是默认方法2");
privateMethod();
}
/**
* 私有方法
*/
private void privateMethod() {
System.out.println("这是私有方法");
}
}
public class MyClass implements MyInterface{
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.defaultMethod();
myClass.defaultMethod2();
}
}
运行结果:
这是默认方法
这是私有方法
这是默认方法2
这是私有方法
String存储结构的变化(JDK9)
JEP 254: Compact Strings
String 的存储结构,由 char[]
改成了 byte[]
,并且增加了一个 coder
属性来表示使用的字符编码
JDK 9 之前
JDK 9 之后
为什么要这么改呢,我们看下文档里是怎么说的
大致的意思就是,原本的存储方式 char 是占2个字节(16位),实际应用程序中字符串又是使用最多的,而大多数的字符串只包含 Latin-1 字符,这个字符只需要1个字节的存储空间,而这种字符串相当于有一半的空间是浪费的。
改版之后,使用byte[],并增加了 coder 属性,这个 coder 有两个可能的值
- 0:表示字符串对象使用 Latin-1 编码(即只包含 ASCII 字符)
- 1:表示字符串对象使用 UTF-16 编码(即可以包含任意 Unicode 字符)
对于每个字符串,会先判断它是不是只有 Latin-1 字符。如果是,就按照1字节的规格进行分配内存。如果不是,就按照2字节的规格进行分配。
这样改的话,内存的使用率就提升了,GC的次数会相应的减少。
快速创建只读集合(JDK9、10)
JEP 269: Convenience Factory Methods for Collections
从 JDK 9 开始,集合(List/ Set/ Map
)都添加了 of
(JDK9添加)和 copyOf
(JDK10添加)方法,用来创建不可变的集合
public class MyTest {
public static void main(String[] args) {
List<String> list = List.of("Java", "Spring", "SpringBoot");
System.out.println(list);
Set<String> set = Set.of("Java", "Spring", "SpringBoot");
System.out.println(set);
Map<Integer, String> map = Map.of(1, "Java", 2, "Spring", 3, "SpringBoot");
System.out.println(map);
List<String> listCopy = List.copyOf(list);
System.out.println(listCopy);
Set<String> setCopy = Set.copyOf(set);
System.out.println(setCopy);
Map<Integer, String> mapCopy = Map.copyOf(map);
System.out.println(mapCopy);
}
}
运行结果:
[Java, Spring, SpringBoot]
[Spring, SpringBoot, Java]
{3=SpringBoot, 1=Java, 2=Spring}
[Java, Spring, SpringBoot]
[Spring, SpringBoot, Java]
{3=SpringBoot, 1=Java, 2=Spring}
JDK 的这个优化想法是不错的,但其实我们日常开发用到只读集合的场景并不多,但是可以按照这个思路,写一个快速创建可变集合的工具类
文本块(JDK13、14、15)
JEP 355: Text Blocks (Preview)
JEP 368: Text Blocks (Second Preview)
JEP 378: Text Blocks
JDK 13 开始预览,JDK 14 二次预览,JDK 15 成为正式版本
目的是简化多行字符串的创建和处理,在旧的 JDK 版本中,我们要创建一个多行字符串,需要用到转义字符和串联操作符来连接多个字符串,例如我们需要创建这样的字符串:
{
“name”: “张三”,
“age”: 18
}
之前我们的写法是这样的:
String text = "{\n" +
" \"name\": \"张三\",\n" +
" \"age\": 18\n" +
"}";
而用文本块,我们只需要以三个双引号 """
开始和结束,中间的内容就是字符串的具体内容,如下:
String text2 = """
{
"name": "张三",
"age": 18
}
""";
更直观的 NullPointerException 提示(JDK14)
JEP 358: Helpful NullPointerExceptions
空指针异常是日常开发中非常常见的异常,但是有个问题,我们不知道具体什么对象是空的,尤其在一行代码上出现多个对象可能为空时,只能通过 debug 代码才能看出来
而 JDK 14 解决了这个问题,优化了 NullPointerException 的提示,可以一眼就看出哪个对象是空的
文档中举的例子还是比较直观的
优化后的提示是类似这样的:
Exception in thread "main" java.lang.NullPointerException:
Cannot assign field "i" because "a" is null
at Prog.main(Prog.java:5)
密封类(JDK15、16、17)
JEP 360: Sealed Classes (Preview)
JEP 397: Sealed Classes (Second Preview)
JEP 409: Sealed Classes
JDK 15 开始预览,JDK 16 二次预览,JDK 17 成为正式版本
在 JAVA 里我们可以通过继承来扩展现有类或者实现接口。这种扩展的灵活性使得类的行为变得难以预测,可能导致不必要的错误或安全漏洞
所以就出现了密封类,密封类可以用来限制子类的范围,增强代码的安全性和可维护性
使用 sealed
关键字在类或者接口前进行修饰,使用 permits
关键字来指定可以继承的类
子类也必须用 sealed
、non-sealed
、final
中的一种关键字修饰
sealed
:用于声明密封类,只允许有限的子类继承。non-sealed
:用于声明普通类,可以被任何类所继承。final
:用于声明最终类,禁止继承。
public sealed class Animal permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}
由于父类是密封类,指定了继承类是 Dog、Cat,这时候如果我写一个 Bird 类继承 Animal 就会报错
instanceof的模式匹配(JDK14、15、16)
JEP 305: Pattern Matching for instanceof (Preview)
JEP 375: Pattern Matching for instanceof (Second Preview)
JEP 394: Pattern Matching for instanceof
instanceof的模式匹配从 JDK 14 开始预览,JDK 15 二次预览,JDK 16 成为正式版本
简化了 instanceof 的书写方式
在 JDK 14 之前,我们需要先判断类型,再做向下转型
if (obj instanceof String) {
String s = (String) obj;
// use s
}
而 JDK 14 之后,可以将判断和转型合并成一步
if (obj instanceof String s) {
// can use s here
}
另外,因为 instanceof 的新变量作用域只在if语句里,如果后面接 && (与操作)可以继续使用instanceof 的变量,如下:
if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}
但是由于短路操作,所以后面接||(或操作)就不能使用 instanceof 的变量,因为有可能 instanceof 不满足新变量未定义,如下:
if (obj instanceof String s || s.length() > 5) {.. s.contains(..) ..}
switch增强(JDK12、13、17)
JEP 325: Switch Expressions (Preview)
JEP 354: Switch Expressions (Second Preview)
JEP 361: Switch Expressions
JEP 406: Pattern Matching for switch (Preview)
- 在 JDK 7 版本之前 switch 语句只能使用整数(byte、short、int)和字符类型(char)作为表达式,并且只支持单个 case 分支,没有 break 会穿透到下一个 case。
- 在 JDK 7 版本里,增加了支持字符串类型(String)和枚举类型(enum)。
- 在 JDK 12 版本里,增加了 switch 表达式,用箭头(->)代替冒号(:)
- 在 JDK 13 版本里,增加了 yield 关键字
- 在 JDK 17 版本里,支持 switch 类型,简化原本 instanceof 的写法,并且支持 case null,对于枚举类和密封类会进行完整性校验,从而不用写 default 分支
在 JDK 12 之前,我们是这么写的
public static void main(String[] args) {
int month = 1;
switch (month) {
case 1, 2, 3, 4, 5, 6:
System.out.println("上半年");
break;
case 7, 8, 9, 10, 11, 12:
System.out.println("下半年");
break;
default:
System.out.println("不存在的月份");
break;
}
}
为了防止穿透,我们必须要写上 break,而在 JDK 12 之后,用 switch 表达式写,可以省略 break
public static void main(String[] args) {
int month = 1;
switch (month) {
case 1, 2, 3, 4, 5, 6 -> System.out.println("上半年");
case 7, 8, 9, 10, 11, 12 -> System.out.println("下半年");
default -> System.out.println("不存在的月份");
}
}
如果有返回值,可以这么写
public static void main(String[] args) {
int month = 1;
String text = switch (month) {
case 1, 2, 3, 4, 5, 6 -> "上半年";
case 7, 8, 9, 10, 11, 12 -> "下半年";
default -> "";
};
System.out.println(text);
}
在 JDK 13版本之后,我们也可以使用 yield 关键字来设置返回值
public static void main(String[] args) {
int month = 1;
String text = switch (month) {
case 1, 2, 3, 4, 5, 6:
yield "上半年";
case 7, 8, 9, 10, 11, 12:
yield "下半年";
default:
yield "";
};
System.out.println(text);
}
在 JDK 17 版本之后,对于一个 Object 向下转型时,可以摆脱 instanceof + cast 了
之前的写法
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
JDK 17 之后
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
另外支持 case null
static void test(Object o) {
switch (o) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}
对于枚举类和密封类会进行完整性校验,从而不用写 default 分支
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // Implicitly final
static void switchStatementComplete(S s) {
switch (s) { // Error - incomplete; missing clause for permitted class B!
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
如果少写一个 B 的情况,编译期会报错
枚举也是同样的道理
垃圾收回相关优化(JDK9-17)
JDK9
- 将G1设置为默认垃圾回收器:JEP 248: Make G1 the Default Garbage Collector
JDK10
- 废弃 CMS 垃圾回收器:JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector
- 引入G1垃圾回收器的并行Full GC功能:JEP 307: Parallel Full GC for G1
JDK11
- 革命性的 ZGC 登场:JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)
JDK12
-
引入 Shenandoah GC,低暂停时间的垃圾回收器:JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)
-
G1可以在收集的过程中中止:JEP 344: Abortable Mixed Collections for G1
-
G1自动返回未用堆内存给操作系统:JEP 346: Promptly Return Unused Committed Memory from G1
JDK13
- ZGC 将未使用的堆内存返回到操作系统:JEP 351: ZGC: Uncommit Unused Memory (Experimental)
JDK14
- 通过实现 NUMA 感知内存分配,提高大型机器上的G1性能:JEP 345: NUMA-Aware Memory Allocation for G1
- 删除 CMS 垃圾回收器:JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
- 将 ZGC 垃圾收集器移植到 macOS:JEP 364: ZGC on macOS (Experimental)
- 将 ZGC 垃圾收集器移植到 Windows:JEP 365: ZGC on Windows (Experimental)
- 废弃 ParallelScavenge + SerialOld GC 的组合:JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
JDK15
- ZGC 正式上线:JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)
- Shenandoah GC 正式上线:JEP 379: Shenandoah: A Low-Pause-Time Garbage Collector (Production)
JDK16
- ZGC 支持并发栈处理:JEP 376: ZGC: Concurrent Thread-Stack Processing
三、总结
JDK 9-17 增加了许多新的特性,对我们的日常开发有很大帮助,JDK 11推出的 ZGC 是一款性能比 G1 还要好的垃圾回收器,JDK 17 也是史上最快的 Java 版本。
Spring Boot 3.0 的最低支持版本已经提升到 JDK 17,并且不再向下兼容 JDK 8,开发者从 JDK 8 转移到 JDK 17 将是大势所趋,我们不能畏惧新的东西,保持学习的姿态,通过阅读文档和实践,不断的进步!