抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

前言

有段时间没在工作中使用Java语言来开发项目了,冲浪了一下才发现Java21的正式版本都已经发布了。遥想以前用Java11就感觉已经感觉是紧跟技术潮流了😭,下面就来学习每个版本都更新了什么吧~

img

PS:由于能力受限,主要留意每个版本已经正式发布同时跟开发密切相关的新语法和新特性,虚拟机啥优化的以后慢慢研究

Java11

Java 11 是在 2018 年 9 月发布的长期支持(LTS)版本。主要包含了一些新语法糖、API更新以及性能改进

  • JEP 323: 局部变量语法的增强:我们可以在lambda 参数中使用局部变量语法的 var,用来简洁代码
  • 引入了一个新的 HTTP 客户端 API:该API支持HTTP2.0协议和Websocket协议,相对于之前原生的HttpURLConnection更简单易用,用起来就和OkHttp库类似
  • String类新增方法:strip(),stripLeading(), stripTrailing(), repeat(), isBlank(), lines()
  • Files类新增方法:增加了 readString()writeString() 方法,能够以字符串的形式读写文件内容

局部变量语法增强:

Java10引入了弱类型语言常见的var类型,可以用于局部变量的类型推断,我们可以不用显式声明局部变量的类型,编译器可以根据局部变量的初始化表达式来自动推断出类型。例如:

1
2
3
var list = new ArrayList<String>(); // 自动推断为 ArrayList<String>
var stream = list.stream(); // 自动推断为 Stream<String>
var object = "hello"; // 自动推断为 String

而局部变量语法增强则是允许在lambda表达式中也使用var关键字

1
2
3
4
// 使用var在lambda表达式中声明参数类型
// 需要注意的使用var关键字,所有的 lambda 参数都必须都使用或都不使用var,不能混用。
Consumer<String> consumer = (var str)-> System.out.println(str);
consumer.accept("hello"); // hello

HttpClient实现

个人感觉就是像OkHttp库的用法,只不过变成原生库了,用起来还是不错的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("<https://www.baidu.com>"))
.GET()
.build();

// 同步请求
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

// 异步请求
httpClient.sendAsync(request,HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}

具体实现可以查看官方文档:https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html

String类的新方法

引入了开发中工具类框架常用的一些字符串方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void testStringNewFeature(){
// isBlank 当字符串只包含空白字符(空格、制表符等),或者是空字符串时返回true,否则返回false
System.out.println("".isBlank()); // true
System.out.println(" ".isBlank()); // true
System.out.println("\\t".isBlank()); // true
System.out.println("hello".isBlank()); // false

// lines 返回字符串流,每个元素为一行数据
// Hello
// World
// 2024
String str = "Hello\\nWorld\\n2024";
str.lines()
.forEach(System.out::println);

// strip,stripLeading,stripTrailing 这三个方法分别去除字符串开头和结尾的空白字符,相比于trim能够处理更多空白字符
System.out.println("\\t \\u3000 hello \\n".trim()); // "  hello"
System.out.println("\\t \\u3000 hello \\n".strip()); // "hello"
System.out.println(" hello ".stripLeading()); // "hello "
System.out.println(" hello ".stripTrailing()); // " hello"

// repeat 返回n个原始字符串拼接的结果
System.out.println("人类的本质是复读机".repeat(3)); // 人类的本质是复读机人类的本质是复读机人类的本质是复读机
}

Files新方法

这个用起来很爽,终于不用写一堆BufferedReader类的模板代码或者引入hutool等工具类了

1
2
3
4
5
6
7
8
static void testFilesNewFeature() throws IOException {
Path path = Path.of("./testFilesNewFeature.txt");

// 字符集默认为UTF8
Files.writeString(path,"Hello 2024!",StandardCharsets.UTF_8, StandardOpenOption.CREATE);
String content = Files.readString(path, StandardCharsets.UTF_8);
System.out.println(content); // Hello 2024!
}

Java12、13

感觉Java12、13对于平常开发来说没有什么改变,大部分都是一些预览特性,因此跳过

Java14

Java14是2020年3月发布的新版本,对于开发来说主要是新语法糖和报错信息的优化

  • JEP 358: 可帮助性空指针异常(Helpful NullPointerExceptions)
  • JEP 361: Switch 表达式

JEP 358: 可帮助性空指针异常

相信大家都有写过链式调用,例如user.address.name.toUpperCase(),当如果此时这一行报空指针异常,我们并不知道是那个地方为null,JEP358特性就改进了空指针异常的消息,现在可以提供更详细上下文信息,帮助我们准确地定位问题所在。

例如name为空,从会出现如下提示信息,而不是简单的NullPointerExceptions

1
2
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "user.address.name" is null
at java14.Main.main(Main.java:8)

JEP 361: Switch 表达式

当写过GolangKotlin等语言的switch语法时,再回过头来看Java14前的Switch实在是太繁琐了同时也有很多局限性。

  1. 只支持基于整数或枚举类型的case
  2. 每次都要显性去写break语句,如果忘记了就会导致穿透下去
  3. 不支持返回值,要么每个case里写return或者赋值给一个变量最后在return回去

Java14对Switch做了重大的改进

  1. switch现在可以作为表达式对变量进行赋值操作
  2. 同时使用箭头标签可以避免fall through的问题,每个case执行完毕后自动结束switch语句,无需显性写break语句
  3. 可以使用yield语句直接返回case分支的值
  4. 同时支持多个case值的合并,无需在写一个空case让他fall through了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
var dayNumber = new Random().nextInt(1,8);
// switch表达式直接赋值
String dayStr = switch (dayNumber){
case 1-> "Monday"; // 如果只有一行无需声明yield
case 2->"Tuesday";
case 3->"Wednesday";
case 4->{
System.out.println("Thursday"); // 多行用{}包裹
yield "Thursday"; // 使用yield返回case值
}
case 5,6,7->{ // 相同case、可以将值合并
System.out.println("Weekend!");
yield "Weekend";
}
default -> "Unknown";
};
}

Java15

Java15发布于2020年9月,对于开发来说新特性我认为就是文本块了

  • JEP 378: Text Blocks(正式特性)

JEP 378: Text Blocks(正式特性)

例如Python的三个单引号或者双引号,JS的反引号就可以定义多行字符串

1
2
3
4
5
6
7
8
9
10
11
12
// python
text = """
This is a
multi-line string
in Python.
"""
// Javascript
let text = `
This is a
multi-line string
in JavaScript.
`;

而Java15以前就只能通过字符串拼接的方式或者StringBuilder等方式来实现,不仅返回而且不雅观,而JEP378特性就实现了类似其他语言已有的【文本快】特性。

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
// Java15 以前
String text = "This is a\n" +
"multi-line string\n" +
"in Java.";

StringBuilder sb = new StringBuilder();
sb.append("This is a\n");
sb.append("multi-line string\n");
sb.append("in Java.");
String text = sb.toString();

//Java15 以后
String textBlock = """
This is a
multi-line string
in Java.
""";
/*
This is a
multi-line string
in Java.
*/
String sql = """
select * from \
table \
Where year = 2024 \
""";
// \用于阻止插入换行符,如下实际还是一行字符串
// select * from table Where year = 2024

Java16

Java16发布于2021年3月,该版本有两个新语法特性,还是很有用滴

  • JEP 394: instanceof 模式匹配
  • JEP 395: Records

JEP 395: Records

Record类型是一种特殊的类,类似于Enum类,用来声明一个受限类型的类,只用于存储数据、作为一个简单的数据聚合类。

它就是我们平常开发总会创建的JavaBeans,没有啥业务逻辑,只是用来将数据聚合在一起在各个层级之间传输,record类型做了和Lombok的@Data注解相似的工作。

当我们用record类型声明一个类时,编译器会做如下工作

  • 构造器:一个公共的构造器,其参数与 record 组件相匹配。
  • 访问器方法(Getters):每个 record 组件都会有一个对应的访问器方法(在 Point 的例子中是 x()y()),返回组件的值。
  • equals() 方法:一个覆盖 Object 类中的 equals() 方法,根据 record 组件的值进行比较。
  • hashCode() 方法:一个覆盖 Object 类中的 hashCode() 方法,根据 record 组件的值来计算哈希码。
  • toString() 方法:一个覆盖 Object 类中的 toString() 方法,按照 record 类型的名称和组件的值来生成字符串表示。
  • record 自动继承 java.lang.Record
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
46
47
48
49
50
51
// 源码
public record Point(int x,int y) {
// record类型也可以自定义方法或者重写自动生成的方法
public void say(){
System.out.println("just record");
}

public String toString(){
return String.format("Point:x=%d,y=%d",x,y);
}

public static void main(String[] args) {
Point point = new Point(20, 24);
point.say(); // just record
System.out.println(point); // Point:x=20,y=24
}
}

// 反编译后
public final class Point extends java.lang.Record {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int x() {
return x;
}

public int y() {
return y;
}

public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return this.x == other.x && this.y == other.y;
}

public int hashCode() {
return Objects.hash(x, y);
}

public String toString() {
return String.format("Point:x=%d,y=%d",x,y);
}
}

使用record类型时需要注意以下限制

  • record 不能拥有其他的实例字段,只能有构造方法里定义的字段
  • record 组件是 final 的,因此不能被赋予新值。
  • record 继承自 java.lang.Record,因此不能显式继承其他类。
  • record 可以实现接口。
  • record 类型是隐式 final 的,不能被继承。

JEP 394: instanceof 模式匹配

在 Java 16 之前,当我们使用 instanceof 检查一个对象的类型并将其转换为特定类型时,我们需要执行显式的类型转换。就算我们instanceof判断通过,总是还要多写一个步骤,如下:

1
2
3
4
if (obj instanceof String) {
String s = (String) obj;
// 使用 s...
}

JEP394新特性允许我们在instanceof语法总新增一个变量,当判断通过时,会自动被初始化为被检查的类型。

1
2
3
4
5
6
7
if (obj instanceof String s) {
// 使用 s...
}

if (obj instanceof String s && !s.isBlank()) {
// s是String类型同时内容不为空
}

有两个地方需要注意

  • 新增的变量是 final 类型的,所以新增后就不能对他进行赋值操作
  • 变量作用域限定在 if 语句或者逻辑与表达式中。所以只能在 instanceof 检查成功后的代码块中使用它。

Java17

Java17发布于2021年的9月,这是一个LTS即长期维护的版本,主要的新特性便是sealed密封类

  • JEP 409: Sealed Classes密封类用于实现对类层次结构的继承限制,可以加强编译时类型检查和提高系统安全性

JEP 409: Sealed Classes密封类

这个新语法特性,对于各种框架来说,可以加强类的继承限制,同时能够更明确表达这个类的继承意图

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
46
47
48
// 定义一个密封的基类 Shape 只能被Circle, Rectangle, Square类继承
public sealed class Shape permits Circle, Rectangle, Square {
// 公共的属性和方法
}

// 定义最终的子类 Circle 不能被继承
public final class Circle extends Shape {
private final double radius;

public Circle(double radius) {
this.radius = radius;
}

// 特定于 Circle 的方法
public double area() {
return Math.PI * radius * radius;
}
}

// 定义非密封的子类 Rectangle 就是普通类,可以被正常继承
public non-sealed class Rectangle extends Shape {
private final double width;
private final double height;

public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}

// 特定于 Rectangle 的方法
public double area() {
return width * height;
}
}

// 定义最终的子类 Square
public final class Square extends Shape {
private final double side;

public Square(double side) {
this.side = side;
}

// 特定于 Square 的方法
public double area() {
return side * side;
}
}

需要注意密封类的规则:

  • 所有允许的子类必须与密封类位于同一个模块或同一个包中。
  • 子类必须选择性地被声明为 final,sealed,non-sealed以表明它们在密封层次结构中的角色。
    • final:表明不能被继承
    • sealed:即为密封类
    • non-sealed:表明该类为普通类,可以正常被继承
  • 密封类本身不能是 abstract 的,除非它至少有一个非 abstract 的子类。

Java18、19、20

同样感觉都是预览特性,因此跳过

Java21

Java21是目前最新的版本,发布于2023年9月,其中最吸引人的当然是类似于Golang协程的JEP 444虚拟线程了。

  • JEP 444 Virtual Threads:Java21的虚拟线程与之前的 API 互相兼容,相比原先的Thread线程来说虚拟线程占用内存非常少,是一种轻量级线程,不应该被池化处理,同时与原先的API兼容。

JEP444 虚拟线程

虚拟线程的原理这里就不多介绍,本质上跟Golang的协程一样,都是解决高并发场景下线程管理的问题,不过一组虚拟线程还是基于一个内核线程,所以不能跨核调用。

先来看下怎么使用吧

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
// Thread.startVirtualThread方法接收Runnable接口参数,并立即启动虚拟线程
Thread.startVirtualThread(()->{
System.out.println("hello 2024");
});

// Thread.ofVirtual方法创建可以设置虚拟线程的属性
for (int i = 0; i < 100; i++) {
Thread.ofVirtual().name(String.format("VirtualThread-%d",i))
.start(()->{
String name = Thread.currentThread().getName();
System.out.println("Hello "+name);
});

}
// 使用Executors.newVirtualThreadPerTaskExecutor来使用虚拟线程
try(ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()){
for (int i = 0; i < 1000; i++) {
service.submit(()->{
System.out.println("hello");
});
}
}

// 使用isVirtual方法可以判断线程是否为虚拟线程
Thread thread = Thread.ofVirtual().name("virtual01").unstarted(() -> {
System.out.println("hello");
});
System.out.println(thread.isVirtual()); // true

虚拟线程有以下特点:

  • 由JVM管理的轻量级线程。
  • 不需要任何显式分配或调度。
  • 适合I/O密集型任务
  • 也可以用来实现异步操作。

参考资料