3月19日 java22正式发布。总计12个新特性发布,4项语言改进、5项库Api改进、1项GC改进和1项java工具改进。这12个更新中,正式发布的有4项内容,其余则为预览或者孵化特性。尝试新特性的朋友可以在下面的链接下载最新版本jdk www.oracle.com/java/techno... 下面为本次更新的简要介绍,后文会有详细的使用示例,代码在 github.com/edfeff/java...
更新介绍
4项语言改进(重点关注,下面有详细示例)
- 未命名变量 (正式发布)
可以使用下划线_
来作为变量名字,用于必须声明但不使用的场景,比如for循环变量、模式匹配解构参数、try语句块以及异常捕获中。
- 子类构造函数中语句可以写在spuer()之前(预览)
子类构造函数的第一行代码可以不用为super()了,java语言之前会限制必须在第一行中调用父类构造函数,jvm规范中并没有此要求,此次放开后构造函数就更灵活了 :( 。
- 字符串模板(第二次预览)
此次为第二次预览,正式发布应该是java 24了,预计明年3月份。不过java的字符串模板和其他语言中的字符串模板用法不太一样。java使用字符串处理器的方式实现,不仅仅用于渲染字符串,还能生成任意类型的对象,详细示例见下文。
- 隐式类声明和实例Main方法(第二次预览)
作为降低java入门门槛的重要特性,本次更新将支持非public类、非静态方法作为启动类和main方法,可以直接使用 void main(){}
来启动java代码了,再配合下文的多文件运行
特性,java的使用更便捷了。
5项库API改进
- 外部函数API和内存API
与JVM外部的函数和数据进行互操作,比如调用本地库和访问本地内存。提供了比JNI更安全的一套API。详见 openjdk.org/jeps/454
- 类文件API
提供一套标准的解析、生成和转换Java类文件的API,并最终替换掉当前的ASM。(ASM: "???")从提案中的动机来看,是嫌弃ASM这些框架跟不上java的升级节奏了。
- 流收集API
加入一个非常有用的gather中间操作函数,可以参考此篇文 mp.weixin.qq.com/s/n7nJPFdU3...
- 结构化并发
Loom项目中的一个提案,用于简化并发编程的难度,使用过kotlin的协程就非常能理解这一点。传统方式上,当一个大任务切分成多个子任务交给线程池执行时,多个子任务会跑在不同线程上,主线程阻塞等待所有子任务的完成。这些子任务之间的线程是独立的,一个子任务的异常影响不到另一个子任务,外部想取消整个任务时操作必须非常谨慎。而结构化并发则会在大任务切分成子任务时,建立主线程和各个子线程的层次关系,主任务的取消会同时取消掉子任务,子任务的异常会反馈给主任务,在虚拟线程的场景中更加适合。
- 作用域值
也是Loom项目中的一个提案,在线程和子线程中共享不可变数据的一种API,更加适合虚拟线程和结构性并发组合使用。在框架或者长链路的调用中,都会选择使用ThreadLocal传递参数。但ThreadLocal存在一些设计缺陷: a. 线程中的任意地方都可以对ThreadLocal进行修改,导致数据变化的不可追溯。b.ThreadLocal的生命周期和线程一样长,内存泄漏风险大。c. ThreadLocal可以被子线程继承,子线程会拷贝一份父线程的变量,占用大量的内存。作用域值是解决海量虚拟线程中数据共享的一个方案。
- Vector Api
此Vector是用于利用底层硬件来加速向量计算的,不是很少被人使用的java.util.Vector。 不熟悉向量计算,更多信息可以参考zhuanlan.zhihu.com/p/676227467
1项GC改进
- 固定G1的Region
通过在 G1 中实现Region固定来减少GC延迟,这样在JNI的关键区域内不需要禁止GC。JNI中定义了获取对象指针和释放指针的函数,用于和底层语言(C,C++)互操作,在获取对象指针和释放对象指针之间的代码被称为关键区域。JVM在GC时,特别注意在关键区域中不能移动这些被操作的对象。之前的做法是线程处于关键区域时就禁止GC,而现在可以把上面的对象固定在它们的位置上,从而实现GC线程不用在JNI的关键区域暂停,可以减少延迟。
1项java工具改进
- 直接运行多文件源码
在jdk17中支持了直接通过java命令运行java源代码,不需要先使用javac编译,不过jdk17中仅仅支持单文件的运行,而此次改进将支持多文件运行,java会自动加载启动类关联的文件。
语言改进示例
未命名变量(JEP456)
当声明一个不需要使用的变量时,Java语言要求必须给变量取一个名字,而在java22中,可使用下划线_
来作为这个变量的名字,称之为 未命名变量
。(python、golang等微微一笑) 下面举几个例子帮助理解:
例1-计算Iterator的大小
有时我需要获得Iterable
的大小,在旧版本中我使用for循环时必须指定变量的名字,而在新版本中可以使用_
代替
java
/** 未命名变量示例1 */
import java.util.*;
public class Jep456_E1 {
public static void main(String[] args) {
oldCode();
newCode();
}
private static void oldCode() {
Iterable<Integer> list = Arrays.asList(1, 2, 3);
var total = 0;
for (var unused : list) {//变量unused从未使用
total++;
}
System.out.printf("old total=%d\n", total);
}
private static void newCode() {
Iterable<Integer> list = Arrays.asList(1, 2, 3);
var total = 0;
for (var _ : list) { //使用 _ 占位即可
total++;
}
System.out.printf("new total=%d\n", total);
}
}
例2-不关心的异常
在某些业务中我们会利用异常来做一些兜底逻辑,异常的原因不是我们关注的重点,此时可使用_
占位。 比如下面将字符串转成数字,失败时返回默认值0。
java
/**
* 未命名变量示例2
*/
public class Jep456_E2 {
public static void main(String[] args) {
System.out.println(oldGetStringValue("不能转成数字"));//0
System.out.println(newGetStringValue("不能转成数字"));//0
}
private static int oldGetStringValue(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException unused) { // 这里的 unused 未使用
return 0;
}
}
private static int newGetStringValue(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException _) { // 使用 _ 占位
return 0;
}
}
}
例3-Lambda语法强制要求变量名
在Stream中有些转换操作中不需要流中的变量,但是语法要求必须写变量名,这时可以使用_
代替。比如下面将小写字母List转成Map,Map的键是大写字母,值都是固定的布尔true。
java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 未命名变量示例3
*/
public class Jep456_E3 {
public static void main(String[] args) {
oldLambda();
newLambda();
}
private static void oldLambda() {
List<String> list = Arrays.asList("a", "b", "c");
var upperCharters = list.stream()
.collect(Collectors.toMap(String::toUpperCase, v -> true));
System.out.println(upperCharters);//{A=true, B=true, C=true}
}
private static void newLambda() {
List<String> list = Arrays.asList("a", "b", "c");
var upperCharters = list.stream()
.collect(Collectors.toMap(String::toUpperCase, _ -> true));
System.out.println(upperCharters);//{A=true, B=true, C=true}
}
}
例4-Switch表达式忽略具体值
在switch表达式中有时不需要关注具体的变量,而是需要变量的类型时,可以使用_
占位。 比如下面代码中,定义了密封类Color
和两个子类White
、Black
。在switch表达式中,变量color具体的值不重要,只需要根据类型来处理不同的逻辑即可,那么可使用_
来忽略变量名。
java
public class Jep456_E4 {
static sealed abstract class Color permits White, Black {
}
static final class White extends Color {
}
static final class Black extends Color {
}
public static void main(String[] args) {
oldSwitchCode();//White
newSwitchCode();//White
}
private static void oldSwitchCode() {
Color color = new White();
switch (color) {
//变量 unusedWhite 和 unusedBlack 都没有使用
case White unusedWhite -> System.out.println("White");
case Black unusedBlack -> System.out.println("Black");
}
}
private static void newSwitchCode() {
Color color = new White();
switch (color) {
//用 _ 占位
case White _ -> System.out.println("White");
case Black _ -> System.out.println("Black");
}
}
}
例5-在模式匹配中忽略不需要的值
java17中引入的模式匹配中,可以通过instanceof对参数进行匹配解构,在java22中继续改进这一特性,支持未命名变量占位。 比如下面的例子,ColoredPoint
由Point
和Color
组成,在模式匹配中我们可能只需要使用到其中的Point参数,或者只需要使用Point的x参数,那么可以用_
来忽略其他参数。
java
record Point(int x, int y) {
}
enum Color {RED, GREEN, BLUE}
record ColoredPoint(Point p, Color c) {
}
public class Jep456_E5 {
public static void main(String[] args) {
oldInstanceOfPattern();
newInstanceOfPattern();
}
private static void oldInstanceOfPattern() {
var cp = new ColoredPoint(new Point(3, 4), Color.GREEN);
if (cp instanceof ColoredPoint(Point p, Color c)) {
//1 只需要Point参数 p.x=3 p.y=4
System.out.printf("1 只需要Point参数 p.x=%d p.y=%d\n", p.x(), p.y());
}
if (cp instanceof ColoredPoint(Point p, Color c)) {
//2 只需要Color参数 color=GREEN
System.out.printf("2 只需要Color参数 color=%s\n", c);
}
if (cp instanceof ColoredPoint(Point(int x, int y), var c)) {
//3 只需要坐标参数 x=3 y=4
System.out.printf("3 只需要坐标参数 x=%d y=%d\n", x, y);
}
if (cp instanceof ColoredPoint(Point(int x, int y), var c)) {
//4 只需要坐标x参数 x=3
System.out.printf("4 只需要坐标x参数 x=%d\n", x);
}
}
private static void newInstanceOfPattern() {
var cp = new ColoredPoint(new Point(3, 4), Color.GREEN);
if (cp instanceof ColoredPoint(var p, _)) {
//1 只需要Point参数 p.x=3 p.y=4
System.out.printf("1 只需要Point参数 p.x=%d p.y=%d\n", p.x(), p.y());
}
if (cp instanceof ColoredPoint(_, var c)) {
//2 只需要Color参数 color=GREEN
System.out.printf("2 只需要Color参数 color=%s\n", c);
}
if (cp instanceof ColoredPoint(Point(var x, var y), _)) {
//3 只需要坐标参数 x=3 y=4
System.out.printf("3 只需要坐标参数 x=%d y=%d\n", x, y);
}
if (cp instanceof ColoredPoint(Point(var x, _), _)) {
//4 只需要坐标x参数 x=3
System.out.printf("4 只需要坐标x参数 x=%d\n", x);
}
}
}
参考链接
支持super前语句 (预览 JEP447)
之前的java版本中,Java语法的要求在子类构造函数中,super()语句必须为第一行代码。这个要求是java语法的要求,而不是jvm规范的要求。在此提案中,子类可以将super()语句放在非首行。 示例代码如下
java
public class Jep447_E1 {
public static void main(String[] args) {
new Son();
}
public static class Parent {
public Parent() {
System.out.println("parent init");
}
}
public static class Son extends Parent {
public Son() {
System.out.println("son init");
super();
}
}
}
执行命令为
bash
java.exe --enable-preview --source 22 Jep447_E1.java
输出结果为子类的代码先于父类的构造方法执行,
字符串模板(第二次预览 JEP459)
对于字符串模板,相信使用js、python的朋友很熟悉了,其他jvm语言也很早很早就有这个特性了,java的更新太慢了,模板字符串有那么难吗?为什么还不正式发布! 下面先看下其他语言的字符串模板,然后java中的字符串模板。 javascript的
javascript
let first = "hello"
let second = "world"
console.log(`${first} ${second.toLocaleUpperCase()}`) //hello WORLD
python的
python
first = "hello"
second = "world"
print(f"{first} {second.upper()}") # hello WORLD
groovy的
groovy
def first = "hello"
def second = "world"
println "$first ${second.toUpperCase()}" //hello WORLD
kotlin的
kotlin
fun main() {
val first = "hello"
val second = "world"
println("$first ${second.uppercase()}")
}
除此之外还有很多的语言都支持字符串模板,看下官方给出的对比示例 好了既然这个多语言都选择${}
或者其他方式的字符串插值来实现,那么Java呢? 当当当当!
javascript
public class Jep459_E1 {
public static void main(String[] args) {
var first = "hello";
var second = "world";
System.out.println(STR."\{first} \{second.toUpperCase()}"); //hello WORLD
}
}
STR.
+\{variable}
不是,能不能学习python、C#啊!看看groovy和kotlin啊!真的是有亿点点丑! STR
只是一个静态变量,来自java.lang.StringTemplate.STR
,可以用其他字符代替,比如下面的例子
javascript
import static java.lang.StringTemplate.STR;
public class Jep459_E2 {
public static void main(String[] args) {
var f = STR;
var first = "hello";
var second = "world";
System.out.println(f."\{first} \{second.toUpperCase()}");
}
}
java的字符串模板不仅仅用于生成字符串,可以生成任意类型对象。这里的原理是java会把字符串模板按照变量占位符拆分,拆成字符串片段数组 和变量值数组 ,并封装成StringTemplate
类型,回调 StringTemplate.Processor
的process
方法。 比如下面的例子中,我们的字符串模板是"A \{a} B \{b}"
,当sample
的process
方法被回调时,参数st
的字符串片段fragments
为[A , B , ]
,变量值values
为[1, 2]
,我们可以使用这两个数组转成任意类型的对象返回。
javascript
public class Jep459_E3 {
record Sample() implements StringTemplate.Processor<Object, Exception> {
@Override
public Object process(StringTemplate st) throws Exception {
System.out.println(st.fragments()); //[A , B , ]
System.out.println(st.values());//[1, 2]
return st.interpolate();
}
}
public static void main(String[] args) throws Exception {
var sample = new Sample();
var a = 1;
var b = 2;
Object s = sample."A \{a} B \{b}";
System.out.println(s); //A 1 B 2
}
}
官方的QueryBuilder的示例
javascript
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Jep459_E4 {
record QueryBuilder(Connection conn) implements StringTemplate.Processor<PreparedStatement, SQLException> {
public PreparedStatement process(StringTemplate st) throws SQLException {
//1. 使用 ? 占位符连接sql片段
String query = String.join("?", st.fragments());
//2. 构造PreparedStatement对象,防止sql注入问题
PreparedStatement ps = conn.prepareStatement(query);
//3. 按照类型填充参数
int index = 1;
for (Object value : st.values()) {
switch (value) {
case Integer i -> ps.setInt(index++, i);
case Float f -> ps.setFloat(index++, f);
case Double d -> ps.setDouble(index++, d);
case Boolean b -> ps.setBoolean(index++, b);
default -> ps.setString(index++, String.valueOf(value));
}
}
//4. 返回PreparedStatement对象
return ps;
}
}
public static void main(String[] args) throws SQLException {
Connection conn = null;//todo
var DB = new QueryBuilder(conn);
String name = "wppcafe";
PreparedStatement ps = DB."SELECT * FROM Person p WHERE p.last_name = \{name}";
ResultSet rs = ps.executeQuery();
}
}
这样看来,Java的字符串模板确实更灵活了一些,但还是丑!
隐式类声明和实例Main方法(第二次预览 JEP463)
这个提案主要是降低java初学者的学习门槛,简化hello world
的写法。
- 隐式类声明: 单个java文件可以不用声明class,直接写字段和方法
- 实例main方法: main入口方法不再要求必须静态和入参
一般的java的入门程序如下,里面有3点是是初学者难以理解的
- 第一点
public class HelloWorld
是什么意思? - 第二点
public static void main(String[] args
是什么意思? - 第三点
System.out.println("Hello, World!")
是什么意思?
因为这个入门程序里面包含了java的很多核心概念,比如类``访问控制``静态方法``入口方法``函数参数``数组``静态引用
。
javascript
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
那么这个提案可以简化到什么程度呢?看例子
- 类public 修饰符不需要了
- 方法public修饰符不需要了
- 方法static声明不需要了
- main方法入参不需要了
javascript
class Jep463_E1 {
void main() {
System.out.println("hello world");
}
}
还能不能再简化呢?也可以
- 类不需要了(直接main方法)
javascript
void main() {
System.out.println("hello world");
}
还可以使用变量和定义方法
javascript
String name = "World";
String hello(String name) {
return "Hello " + name;
}
void main() {
System.out.println(hello(name));
}
这样看来入门java就简单多了,上面的这个代码就使用了隐式类声明
,上面的所有代码被包裹在一个匿名类中,里面声明了实例main方法
,再配合java 17中的单文件运行
,直接 java hello.java
就把程序跑起来了!
库API改进示例
类文件API(预览 JEP457)
写了一个字节码版本的helloworld如下,这部分api了解即可
javascript
import java.io.IOException;
import java.lang.classfile.ClassFile;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
void main() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
final var className = "Hello";
byte[] bytes = ClassFile.of().build(ClassDesc.of(className), classBuilder -> {
classBuilder.withMethod("<init>", MethodTypeDesc.ofDescriptor("()V"), ClassFile.ACC_PUBLIC, methodBuilder -> {
methodBuilder.withCode(codeBuilder -> {
codeBuilder.aload(codeBuilder.receiverSlot());
codeBuilder.invokespecial(ClassDesc.ofDescriptor("Ljava/lang/Object;"), "<init>", MethodTypeDesc.ofDescriptor("()V"));
codeBuilder.getstatic(ClassDesc.ofDescriptor("Ljava/lang/System;"), "out", ClassDesc.ofDescriptor("Ljava/io/PrintStream;"));
codeBuilder.ldc("hello world");
codeBuilder.invokevirtual(ClassDesc.ofDescriptor("Ljava/io/PrintStream;"), "println", MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V"));
codeBuilder.return_();
});
});
});
ClassLoader classLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (name.equals(className)) {
return defineClass(className, bytes, 0, bytes.length);
}
return super.findClass(name);
}
};
Class<?> helloClass = classLoader.loadClass(className);
Object o = helloClass.newInstance();
}
结构化并发(第二次预览 JEP 462)
例1-kotlin的结构化并发
看个kotlin的例子比较容易理解, mainTask 内部拆成两个subTask,一个耗时1s,另一个耗时3s
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val mainTask: Job
var subTask1: Job? = null
var subTask2: Job? = null
//父协程
mainTask = launch {
//3个子协程
subTask1 = launch {
//1秒
println("subTask1 start")
delay(1000L)
println("subTask1 finish")
}
subTask2 = launch {
//3秒
println("subTask2 start")
delay(3000L)
println("subTask2 finish")
}
}
delay(500L)
//遍历父协程的子协程集
mainTask.children.forEachIndexed { index, task ->
when (index) {
0 -> println("subTask1 === task is ${subTask1 === task}")
1 -> println("subTask2 === task is ${subTask2 === task}")
}
}
//等待父协程执行完成
mainTask.join()
println("finish")
}
输出结果为
kotlin
subTask1 start
subTask2 start
subTask1 === task is true
subTask2 === task is true
subTask1 finish
subTask2 finish
finish
当我需要取消主任务时如下,我只需要调用mainTask.cancel(),两个子任务就会自动取消,这在使用线程池时难度比较大。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val mainTask: Job
var subTask1: Job? = null
var subTask2: Job? = null
//父协程
mainTask = launch {
//3个子协程
subTask1 = launch {
//1秒
println("subTask1 start")
delay(1000L)
println("subTask1 finish")
}
subTask2 = launch {
println("subTask2 start")
delay(3000L)
println("subTask2 finish")
}
}
delay(500L)
//取消主任务
mainTask.cancel()
println("finish")
}
输出结果为
kotlin
subTask1 start
subTask2 start
finish
例2-java的结构化并发
和kotlin的例子一样,mainTask 内部拆成两个subTask,一个耗时1s,另一个耗时3s
kotlin
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
public class Jep462_E3 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
mainTask();
}
public static void mainTask() throws InterruptedException, ExecutionException {
var scope = new StructuredTaskScope.ShutdownOnFailure();
var subTask1 = scope.fork(() -> {
System.out.println("subTask1 start");
Thread.sleep(1000);
System.out.println("subTask1 finish");
return null;
});
var subTask2 = scope.fork(() -> {
System.out.println("subTask2 start");
Thread.sleep(3000);
System.out.println("subTask2 finish");
return null;
});
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
scope.join().throwIfFailed();
scope.close();
System.out.println("finish");
}
}
输出为
kotlin
subTask1 start
subTask2 start
subTask1 finish
subTask2 finish
finish
当我需要取消主任务时如下,我只需要调用scope.shutdown()
kotlin
import java.time.Instant;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.TimeoutException;
public class Jep462_E4 {
public static void main(String[] args) {
mainTask();
}
public static void mainTask() {
var scope = new StructuredTaskScope.ShutdownOnFailure();
var subTask1 = scope.fork(() -> {
System.out.println("subTask1 start");
Thread.sleep(1000);
System.out.println("subTask1 finish");
return null;
});
var subTask2 = scope.fork(() -> {
System.out.println("subTask2 start");
Thread.sleep(3000);
System.out.println("subTask2 finish");
return null;
});
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
scope.shutdown();
System.out.println("finish");
}
}
结果如下
kotlin
subTask1 start
subTask2 start
finish
作用域值(第二次预览 JEP464)
在框架中我需要跨多层传递一个参数,但是又不想被用户的代码访问到,就可以使用ScopedValue。比如下面的示例,我需要把用户的处理器MyHandler加入到框架中,又不想MyHandler访问到框架内传输的上下文数据。
kotlin
public class Jep464_E1 {
public static void main(String[] args) {
var application = new MyHandler();
var framework = new Framework(application);
framework.service(new Request(), new Response());
}
static class FrameworkContext {
private long startTime;
private long spentTime;
}
interface Handler {
void handle(Request request, Response response);
}
static class MyHandler implements Handler {
@Override
public void handle(Request request, Response response) {
//这里是无法读取CONTEXT的,限制了用户代码的访问
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
record Request() {
}
record Response() {
}
static class Framework {
private final Handler handler;
private final static ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance(); // (1)
Framework(Handler handler) {
this.handler = handler;
}
void service(Request request, Response response) {
var context = createContext(request);
ScopedValue.where(CONTEXT, context) // (2)
.run(() -> { //CONTEXT 在此处后可以访问
preRequest();
handler.handle(request, response);
postRequest();
//CONTEXT 在此后不可以访问,限定了context的访问访问
});
}
private void preRequest() {
var context = CONTEXT.get();
context.startTime = System.currentTimeMillis();
}
private void postRequest() {
var context = CONTEXT.get();
context.spentTime = System.currentTimeMillis() - context.startTime;
System.out.println("request spent " + context.spentTime + " ms");
}
FrameworkContext createContext(Request request) {
return new FrameworkContext();
}
}
}
Java工具改进示例
直接运行多文件源码(Jep 458)
使用两个java文件的示例,一个为启动类,一个为工具类 启动类 Jep458_E1.java
kotlin
/**
* 未命名变量示例3
*/
public class Jep458_E1 {
public static void main(String[] args) {
Util.hello();
}
}
工具类 Util.java
kotlin
public class Util {
public static void hello() {
System.out.println("hello");
}
}
使用java命令启动
kotlin
java.exe Jep458_E1.java
运行结果 (忽略中间的Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8)