JDK8 VS JDK17
- JDK17-性能提升:无需对代码做任何优化,只需升级且让程序适配JDK17,你的程序就会因JDK底层技术的更新而获得相应的性能提升
- JDK17-语法新特性:JDK9到JDK17中间发布的新特性能够使我们开发的代码更加简洁和健壮
- JDK17-是商业免费的LTS版本:LTS(
long-term support
)长期支持 - JDK17-生态框架版本升级:例如
Spring6.0
&Spring Boot3.0
支持的最小版本是JDK17
新语言特性
本地变量类型推断
类型推断是很多编程语言都具有的特性,编译器可以在我们不指定具体变量类型的时候根据上下文推断出该变量的实际类型。类型推断可以使我们的程序更加灵活更加简洁
在Java 10之前版本中,我们想定义定义局部变量时。我们需要在赋值的左侧提供显式类型,并在赋值的右边提供实现类型:
MyObject value = new MyObject();
在Java 10中,提供了本地变量类型推断的功能,可以通过var声明变量:
var value = new MyObject();
本地变量类型推断将引入“var”关键字,而不需要显式的规范变量的类型。
var可用于以下类型的变量:
// 设置了初始值的局部变量
var list = new ArrayList<String>(); // infers ArrayList<String>
// for 循环索引
for (var counter = 0; counter < 10; counter++) {...} // infers int
总结
- 所谓的本地变量类型推断,也是一种语法糖。虽然我们在代码中使用
var
进行了定义,但是对于虚拟机来说是不认识这个var
的,而是在编译时期进行解糖,类型推断出真正的类型来替代var
,而且也不会影响程序运行时的性能。 var
关键字提高了代码的简洁性,但是降低了代码的可读性和可维护性。所以我们在使用的时候要严格遵守命名规范和将局部变量的作用域尽可能最小化
record 类:简化数据类的创建
record
是一种特殊类型的类。可用来创建不可变类。它适用于存储纯粹的值类型数据,如接口传输数据、坐标点和只读的日志记录。
public record Student(Integer id, String name) {
}
public void test(String[] args) {
//创建Record对象
Student student = new Student(1001,"lisi");
System.out.println("student = " + student);
//public方法,获取属性值,只读,没有set,get方法
Integer id = student.id();
String name = student.name();
System.out.println("id = " + id);
}
好处
- 减少样板代码。在引入记录类型之前,定义一个简单的数据载体类需要编写大量的样板代码,包括字段、构造函数、getter方法以及equals()、hashCode()和toString()方法。记录类型自动为所有字段生成这些方法,极大地减少了必须手动编写的代码量。
- 提升代码的可读性和可维护性。
- 记录类型的实例是不可变的。这意味着一旦实例被创建,它的状态就不能改变。不可变性有助于创建更安全、更易于理解和维护的代码,尤其是在多线程环境中。
- 促进函数式编程。记录类型的不可变性和简洁性支持了函数式编程范式。在函数式编程中,不可变数据结构是核心概念之一,记录类型自然而然地融入了这种编程风格。
Instanceof Pattern Matching:提供更强大的模式匹配功能
instanceof
关键字主要用来判断指定对象是否是某个类的实例。通常情况下我们方法定义的是一个接口类型或者父类,我们可以根据instanceof
判断传入的对象具体属于哪个子类,方便我们做出对应的处理。 下面是 JDK8 我们代码中的处理:
Person person = new Student();
if (person instanceof Student) {
Student student = (Student) person;
// TODO
}
旧语法的使用过程分为三步:
person instanceof Student
判断对象是否属于当前类型(Student) person
将对象强制类型转换为当前类型Student student
声明一个新的变量接受转换后的对象
这样不仅代码冗余臃肿,且容易出错 而在 JDK16 之后使用新instanceof
语法可以将以上代码改写为下面:
Person person = new Student();
if (person instanceof Student student) {
// TODO
}
新语法将这三步融合为一步,即在instanceof
判断类型之后增加了变量名称,这样如果instanceof
的结果为 true ,对象将会被强转为当前类型的对象并赋值给我们后面跟的变量名,这样不仅代码更加简洁增强可读性,同时也降低了出错的概率
模式变量的作用范围
instanceof
后面跟的变量的作用范围很好理解,就是当instanceof
返回为true后的判断条件和对应的代码块。我们可以判断一下下面四个代码块能够通过编译器检查:
// 变量仅支持在 && 符之后传递,因为 || 后面的语句不保证前面一定成功
if (person instanceof Student student && student.getAge() < 18){ // right
student.study();
}
if (person instanceof Student student || student.getAge() < 18){ // error
student.study();
}
// 在符合判断条件的代码块中可以使用
if (person instanceof Student student) { // right
student.study();
}
if (!(person instanceof Student student)) { //right
// ...
} else {
student.study();
}
Switch表达式:增强了Switch语句
不必为break每个 case 块定义一个语句,我们可以简单地使用箭头语法。
boolean isWeekend = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
case SATURDAY, SUNDAY -> true;
default -> throw new IllegalStateException("Illegal day entry :: " + day);
};
int size = 3;
String cn = switch (size) {
case 1 -> "壹";
case 2 -> "贰";
case 3, 4 -> "叁";
default -> "未知";
};
System.out.println(cn);
使用yield,我们现在可以有效地从 switch 表达式返回值,并能够更容易实现策略模式。
public class SwitchTest {
public static void main(String[] args) {
var me = 4;
var operation = "平方";
var result = switch (operation) {
case "加倍" -> {
yield me * 2;
}
case "平方" -> {
yield me * me;
}
default -> me;
};
System.out.println(result);
}
}
总之,改进的 switch 表达式的语法比之前更加清晰和紧凑,它减少了重复的代码和避免了易错的 break 语句,使代码变得更简洁易读
文本块
文本块(Text Blocks)是 JDK15 引入的一项特性,它提供了一种更自然、易读的多行字符串表达形式,以避免在 Java 代码中编写多行字符串时容易产生的转义字符问题和格式化问题。文本块尝试消除转义字符和连接符等影响,使得文字对齐和必要的占位符更加清晰,从而简化多行字符串的代码书写 例如使用字符串输出 HTML 语句:
String stringBlock =
"<!DOCTYPE html>\n" +
"<html>\n" +
" <body>\n" +
" <h1>\"Hello World!\"</h1>\n" +
" </body>\n" +
"</html>\n";
而使用文本块我们可以简化为:
String textBlock = """
<!DOCTYPE html>
<html>
<body>
<h1>"Hello World!"</h1>
</body>
</html>
""";
System.out.println(
"Here is the text block:\n" + textBlock);
很明显,不需要转义双引号或添加回车。通过使用文本块,嵌入的 JSON 更易于编写,更易于阅读和维护。
更多的API
- isBlank():如果字符串为空或字符串仅包含空格(包括制表符),则返回 true。注意与isEmpty() 不同,isEmpty()仅在长度为 0 时返回 true。
- lines():将字符串拆分为字符串流,每个字符串包含一行。
- strip() :分别从开头和结尾;
- stripLeading()/stripTrailing()仅开始和仅结束删除空格。
- repeat(int times):返回一个字符串,该字符串采用原始字符串并按指定的次数重复该字符串。
- readString():允许从文件路径直接读取到字符串。
- writeString(Path path):将字符串直接写入指定路径处的文件。
- indent(int level):缩进字符串的指定量。负值只会影响前导空格。
- transform(Function f):将给定的 lambda 应用于字符串。
Sealed Classes
在 JDK17 之前,我们如果不想一个类被继承和修改有两种方法:私有类和使用final
修饰类,而私有类只能内部使用,final
修饰符又完全限制了类的扩展性,所有类都不能继承被修饰的类,所以我们的类就只有两个选择,要么完全开放,要么完全封闭。JDK17 引入sealed
修饰符来解决这个问题 下面我们先来看以下sealed
修饰符的用法:
sealed interface User permits Student, Teacher, Admin {}
final class Student implements User{}
sealed class Teacher implements User{}
no-sealed class Admin implements User{}
permits 语句放在 extends 和 implements 语句之后声明 我们称 sealed 修饰的类为封闭类, permits 后面跟着的类叫做许可类 许可类的声明需要满足下面三个条件:
- 许可类必须和封闭类处于同一模块或同一包空间里,即在编译的时候封闭类必须可以访问它的许可类
- 许可类必须是封闭类的直接扩展类
许可类必须声明是否继续保持封闭
- 声明为
final
:关闭扩展性 - 声明为
sealed
:受限制的扩展性 - 声明为
non-sealad
:不受限制的扩展性
- 声明为
类的扩展性总结
限定类的扩展性方法有以下四种:
- 私有类
- 使用
final
修饰符 - 使用
sealed
修饰符 - 不限制扩展性
在我们日常的接口设计和编码实践中,为了保持类的扩展可控,应该尽量按照序号由低到高的优先级实现
库级别的改动
库的改进
Stream API的增强:新的Stream方法和操作
JDK9中Stream中增加了ofNullable、dropWhile、takeWhile、iterate
等方法
- takeWhile :遍历返回元素,遇到不满足的结束
- dropWhile :遍历跳过元素,遇到不满足的结束
- ofNullable :支持创建全 null 的 Stream,避免空指针
- iterate :可以重载迭代器
// takeWhile() 方法示例
List<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6)
.takeWhile(n -> n < 4)
.collect(Collectors.toList());
System.out.println("takeWhile 示例:" + numbers); // 输出:[1, 2, 3]
// dropWhile() 方法示例
List<Integer> numbers2 = Stream.of(1, 2, 3, 4, 5, 6)
.dropWhile(n -> n < 4)
.collect(Collectors.toList());
System.out.println("dropWhile 示例:" + numbers2); // 输出:[4, 5, 6]
// ofNullable() 方法示例
String name = null;
List<String> names = Stream.ofNullable(name)
.collect(Collectors.toList());
System.out.println("ofNullable 示例:" + names); // 输出:[]
// iterate() 方法的重载示例
List<Integer> evenNumbers = Stream.iterate(0, n -> n < 10, n -> n + 2)
.collect(Collectors.toList());
System.out.println("iterate 重载示例:" + evenNumbers); // 输出:[0, 2, 4, 6, 8]
HTTP客户端API-响应式流实现的HttpClient
Java 使用HttpURLConnection进行HTTP通信已经很长一段时间了。但随着时间的推移,要求变得越来越复杂,应用程序的要求也越来越高。在 Java 11之前,开发人员不得不求助于功能丰富的库,如Apache HttpComponents
或OkHttp
等。
我们看到Java 9发布包含一个HttpClient实现作为实验性功能。它随着时间的推移而发展,现在是 Java 11 的最终功能。现在 Java 应用程序可以进行 HTTP 通信,而无需任何外部依赖。
作为JDK11中正式推出的新Http连接器,支持的功能还是比较新的,主要的特性有:
- 完整支持HTTP 2.0 或者HTTP 1.1
- 支持 HTTPS/TLS
- 有简单的阻塞使用方法
- 支持异步发送,异步时间通知
- 支持WebSocket
- 支持响应式流
HTTP2.0其他的客户端也能支持,而HttpClient使用CompletableFuture作为异步的返回数据。WebSocket的支持则是HttpClient的优势。响应式流的支持是HttpClient的一大优势。
HttpClient中的NIO模型、函数式编程、CompletableFuture异步回调、响应式流让HttpClient拥有极强的并发处理能力,所以其性能极高,而内存占用则更少。
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com"))
.build();
// 同步
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// 异步
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
工具和开发环境改进
更清晰的NPE
空指针异常NullPointExceptions
简称 NPE ,是运行时异常的一种,也是我们最常碰到的异常之一。
a.b.c.i == 99;
上面这段代码在JDK14之前如果出现空指针异常的提示是这样的
Exception in thread "main" java.lang.NullPointerException
at Prog.main(Prog.java:5)
但是在JDK14通过-XX:+ShowCodeDetailsInExceptionMessages
参数开启详细的异常信息后提示是这样的:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "c" because "a.b" is null
at Prog.main(Prog.java:5)
JDK14改进NullPointExceptions异常的可读性,使开发人员能够更好的定位null变量的信息 在JDK17中该特性默认开启
JShell:介绍交互式Java编程工具的用法和优点
JShell 是在 JDK9 引入的交互式编程环境工具,它允许你无需使用类或者方法包装来执行 Java 语句。它与 Python 的解释器类似,可以直接 输入表达式并查看其执行结果。我们可以输入/help intro
可以看到官方的介绍:
jshell> /help intro
|
| intro
| =====
|
| 使用 jshell 工具可以执行 Java 代码,从而立即获取结果。
| 您可以输入 Java 定义(变量、方法、类等等),例如:int x = 8
| 或 Java 表达式,例如:x + x
| 或 Java 语句或导入。
| 这些小块的 Java 代码称为“片段”。
|
| 这些 jshell 工具命令还可以让您了解和
| 控制您正在执行的操作,例如:/list
|
| 有关命令的列表,请执行:/help
|
jshell> int doubled(int i){ return i*2;}
| created method doubled(int)
jshell> doubled(6)
$3 ==> 12
jshell>/exit
| Goodbye