Java9到22的一些新特性


Java9新特性

接口里面可以声明私有方法了

在JDK9中新增了接口私有方法,我们可以在接口中声明private修饰的方法了。

1
2
3
4
5
6
public interface Test {
// 定义私有方法
private void solution() {
System.out.println("Hello Java!");
}
}

改进try with resource

Java7中新增了try with resource语法用来自动关闭资源文件,在IO流和JDBC部分使用的比较多。

使用方式是将需要自动关闭的资源对象的创建放到try后面的小括号中,在JDK9中我们可以将这些资源对象的创建代码放到小括号外面,然后将需要关闭的对象名放到try后面的小括号中即可,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
改进了try-with-resources语句,可以在try外进行初始化,在括号内填写引用名,即可实现资源自动关闭
*/
public class TryWithResource {
public static void main(String[] args) throws FileNotFoundException {
// JDK8以前
try (FileInputStream fileInputStream = new FileInputStream("");
FileOutputStream fileOutputStream = new FileOutputStream("")) {

} catch (IOException e) {
e.printStackTrace();
}

// JDK9
FileInputStream fis = new FileInputStream("");
FileOutputStream fos = new FileOutputStream("");
// 多资源用分号隔开
try (fis; fos) {
} catch (IOException e) {
e.printStackTrace();
}
}
}

不能使用下划线命名变量 __

下面语句在JDK9之前可以正常编译通过,但是在JDK9(含)之后编译报错,在后面的版本中会将下划线作为关键字来使用。

1
String _ = "Java";

String字符串的变化

写程序的时候会经常用到String字符串,在以前的版本中String内部使用了char数组存储,对于使用英语的人来说,一个字符用一个字节就能存储,使用char存储字符会浪费一半的内存空间,因此在JDK9中将String内部的char数组改成了byte数组,这样就节省了一半的内存占用。

String中增加了下面2个成员变量

  • COMPACT_STRINGS:判断是否压缩,默认是true,若为false,则不压缩,使用UTF16编码
  • coder用来区分使用的字符编码,分别为LATIN1(值为0)和UTF16(值为1)

byte数组如何存储中文呢?通过源码(StringUTF16类中的toBytes方法)得知,在使用中文字符串时,1个中文会被存储到byte数组中的两个元素上,即存储1个中文,byte数组长度为2,存储2个中文,byte数组长度为4

以如下代码为例进行分析:

1
String str = "好"

好对应的Unicode码二进制为0101100101111101,**分别取出高8位和低8位,放入到byte数组中{01011001,01111101}**,这样就利用byte数组的2个元素保存了1个中文。

当字符串中存储了中英混合的内容时,1个英文字符会占用2个byte数组位置

在获取字符串长度时,若存储的内容存在中文,是不能直接获取byte数组的长度作为字符串长度的,String源码中有向右移动1位的操作(即除以2),这样才能获取正确的字符串长度。

@Deprecated注解的变化

该注解用于标识废弃的内容,在JDK9中新增了2个内容:

  • String since() default “”:标识是从哪个版本开始废弃
  • boolean forRemoval() default false:标识该废弃的内容会在未来的某个版本中移除

jshell

在一些编程语言中,例如:python,Ruby等,都提供了REPL(Read Eval Print Loop 简单的交互式编程环境)。jshell就是Java语言平台中的REPL。

有的时候我们只是想写一段简单的代码,例如HelloWorld,按照以前的方式,还需要自己创建Java文件,创建class,编写main方法,但实际上里面的代码其实就是一个打印语句,此时还是比较麻烦的。在JDK9中新增了jshell工具,可以帮助我们快速的运行一些简单的代码。

从命令提示符里面输入jshell,进入到jshell之后输入:

1
System.out.println("HelloWorld");

image-20240911000358964

如果要退出jshell的话,输入/exit即可。

Java10新特性

局部变量类型推断 var

在JDK10以前声明变量的时候,我们会像下面这样:

1
2
3
4
String oldName = "jack";
int oldAge = 10;
long oldMoney = 88888888L;
Object oldObj = new Object();

上面我们声明的时候使用了4种不同类型的变量,在JDK10中前面的类型都可以使用var来代替,JVM会自动推断该变量是什么类型的,例如可以这样写:

1
2
3
4
var newName = "jack";
var newAge = 10;
var newMoney = 88888888L;
var newObj = new Object();

注意:

当然这个var的使用是有限制的,仅适用于局部变量,增强for循环的索引,以及普通for循环的本地变量;它不能使用于方法形参,构造方法形参,方法返回类型等。

除了上面的新特性之外,还对JVM进行了一些优化,这里就不罗列了。

Java11新特性

直接运行 java

在以前的版本中,我们在命令提示下,需要先编译,生成class文件之后再运行,例如:

1
2
javac HelloWorld.java
java HelloWorld

在java 11中,我们可以这样直接运行

1
java HelloWorld.java

去掉了 javac 的环节,可以直接使用java来运行代码,而且如果你的代码有其他对象的引用,也是没有任何问题的。

可以帮助我们做一个级联的无感知编译。

lambda表达式中的变量类型推断

JDK11中允许在lambda表达式的参数中使用var修饰

函数式接口:

1
2
3
4
@FunctionalInterface
public interface MyInterface {
void m1(String a, int b);
}

测试类:

1
2
3
4
5
6
7
// 支持lambda表达式参数中使用var
MyInterface mi = (var a,var b)->{
System.out.println(a);
System.out.println(b);
};

mi.m1("monkey",1024);

Java12新特性

升级的switch语句

在JDK12之前的switch语句中,如果没有写break,则会出现case穿透现象,下面是对case穿透的一个应用,根据输入的月份打印相应的季节。

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
int month = 3;
switch (month) {
case 3:
case 4:
case 5:
System.out.println("spring");
break;
case 6:
case 7:
case 8:
System.out.println("summer");
break;
case 9:
case 10:
case 11:
System.out.println("autumn");
break;
case 12:
case 1:
case 2:
System.out.println("winter");
break;
default:
System.out.println("wrong");
break;
}

在JDK12之后我们可以省略全部的break和部分case,这样使用

1
2
3
4
5
6
7
8
int month = 3;
switch (month) {
case 3,4,5 -> System.out.println("spring");
case 6,7,8 -> System.out.println("summer");
case 9,10,11 -> System.out.println("autumn");
case 12, 1,2 -> System.out.println("winter");
default -> System.out.println("wrong");
}

这个是预览功能,如果需要编译和运行的话需要使用下面命令,预览功能在2个版本之后会成为正式版,即如果你使用的是JDK14以上的版本,正常的编译和运行即可。否则需要使用预览功能来编译和运行

1
2
3
4
5
编译:
javac --enable-preview -source 12 Test.java

运行:
java --enable-preview Test

Java13新特性

升级的switch语句

JDK13中对switch语句又进行了升级,可以switch的获取返回值

示例:

1
2
3
4
5
6
7
8
9
10
int month = 3;
String result = switch (month) {
case 3,4,5 -> "spring";
case 6,7,8 -> "summer";
case 9,10,11 -> "autumn";
case 12, 1,2 -> "winter";
default -> "wrong";
};

System.out.println(result);

对于JDK15之后的版本可以直接编译和运行,否则需要使用下面命令执行该预览功能

1
2
3
4
5
编译:
javac --enable-preview -source 13 Test.java

运行:
java --enable-preview Test

文本块的变化 Python的插值表达式 fstring

在JDK13之前的版本中如果输入的字符串中有换行的话,需要添加换行符

1
2
String s = "Hello\nWorld\nLearn\nJava";
System.out.println(s);

JDK13之后可以直接这样写:

1
2
3
4
5
6
7
String s = """
Hello
World
Learn
Java
""";
System.out.println(s);

这样的字符串更加一目了然。

Java14新特性

java 14 新增了很多特性,我们针对较为突出的特性进行说明。JDK12和JDK13中预览版的switch特性,在JDK14中已经是正式的语法了。

instanceof模式匹配

该特性可以减少强制类型转换的操作,简化了代码,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestInstanceof{
public static void main(String[] args){

// JDK14之前的写法
Object obj = new Integer(1);
if(obj instanceof Integer){
Integer i = (Integer)obj;
int result = i + 10;
System.out.println(i);
}

// JDK14新特性 不用再强制转换了
// 这里相当于是将obj强制为Integer之后赋值给i了
if(obj instanceof Integer i){
int result = i + 10;
System.out.println(i);
}else{
// 作用域问题,这里是无法访问i的
}
}
}

这个是预览版的功能所以需要使用下面命令编译和运行

1
2
3
4
5
编译:
javac --enable-preview -source 14 TestInstanceof.java

运行:
java --enable-preview TestInstanceof

友好的空指针(NullPointerException)提示

JDK14中添加了对于空指针异常友好的提示,便于开发者快速定位空指针的对象。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Machine{
public void start(){
System.out.println("启动");
}
}

class Engine{
public Machine machine;
}

class Car{
public Engine engine;

}

public class TestNull{
public static void main(String[] args){
// 这里会报出空指针,但是哪个对象是null呢?
new Car().engine.machine.start();
}
}

我们在运行上面代码的时候,错误信息就可以明确的指出那个对象为null了。此外,还可以使用下面参数来查看:

1
java -XX:+ShowCodeDetailsInExceptionMessages TestNull

这样编译器会明确的告诉开发者哪个对象是null。

record类型

之前在编写javabean类的时候,需要编写成员变量,get方法,构造方法,toString方法,hashcode方法,equals方法。这些方法通常会通过开发工具来生成,在JDK14中新增了record类型,通过该类型可以省去这些代码的编写。

JDK14编写User

1
public record User(String name,Integer age){}

通过反编译命令可以看到该字节码文件中的内容,User类是继承了Record类型:

1
javap -p -private user

编写测试类:

1
2
3
4
5
6
7
public class TestUser{
public static void main(String[] args){
User u = new User("jack",15);
System.out.println(u);
System.out.println(u.name());
}
}

这个是预览版的功能所以需要使用下面命令编译和运行

1
2
3
4
5
编译:
javac --enable-preview -source 14 TestUser.java

运行:
java --enable-preview TestUser

记录类型有自动生成的成员,包括:

  • 状态描述中的每个组件都有对应的private final字段。
  • 状态描述中的每个组件都有对应的public访问方法。方法的名称与组件名称相同。
  • 一个包含全部组件的公开构造器,用来初始化对应组件。
  • 实现了equals()和hashCode()方法。equals()要求全部组件都必须相等。
  • 实现了toString(),输出全部组件的信息。

Java15新特性

java 15中更新了一些新的内容,这里仅列出对于写代码方面的新特性。

Sealed Classes

密封类和接口,作用是限制一个类可以由哪些子类继承或者实现。

  1. 如果指定模块的话,sealed class和其子类必须在同一个模块下。如果没有指定模块,则需要在同一个包下。
  2. sealed class指定的子类必须直接继承该sealed class。
  3. sealed class的子类要用final修饰。
  4. sealed class的子类如果不想用final修饰的话,可以将子类声明为sealed class。

Animal类,在指定允许继承的子类时可以使用全限定名

1
2
3
4
public sealed class Animal permits Cat, Dog{// 多个子类之间用,隔开

public void eat(){}
}

Cat类

1
2
3
4
5
public final class Cat extends Animal{
public void eat(){
System.out.println("123");
}
}

Dog类

1
2
public sealed class Dog extends Animal
permits Husky {}

Husky类

1
2
public final class Husky extends Dog{
}

Test类

1
2
3
4
5
6
7
public class Test{
public static void main(String[] args){
Cat c = new Cat();
c.eat();
Dog d = new Dog();
}
}

Java16新特性

这里只介绍一些跟开发关联度较大的特性,除此之外JDK16还更新了许多其他新特性,感兴趣的同学可以去Oracle官网查看

包装类构造方法的警告

使用包装类的构造方法在编译的时候会出现警告,不建议再使用包装类的构造方法。下面代码在javac编译之后会出现警告。

1
Integer i = new Integer(8);

不建议使用包装类作为锁对象,倘若使用包装类作为锁对象,在编译时会出现警告。

1
2
3
4
Integer i = 8;
synchronized(i){

}

新增日时段

在DateTimeFormatter.ofPattern传入B可以获取现在时间对应的日时段,上午,下午等

1
System.out.println(DateTimeFormatter.ofPattern("B").format(LocalDateTime.now()));

在之前JDK版本中作为预览功能的Record类,模式匹配的instanceof,打包工具jpackage,已成为正式版。JDK16对GC,JVM运行时内存等内容有一些变化,例如:ZGC并发栈处理弹性meta space

Java17新特性

java17是一个LTS(long term support)长期支持的版本,根据计划来看java17会支持到2029年(java8会支持到2030年,OMG),同时Oracle提议下一个LTS版本是java21,在2023年9月发布,这样讲LST版本的发布周期由之前的3年变为了2年。这里只介绍一些跟开发关联度较大的特性,除此之外JDK17还更新了一些其他新特性,感兴趣的同学可以从这里查看:https://www.oracle.com/news/announcement/oracle-releases-java-17-2021-09-14/

switch语法的变化(预览)

在之前版本中新增的instanceof模式匹配的特性在switch中也支持了,即我们可以在switch中减少强转的操作。比如下面的代码:

Rabbit和Bird均实现了Animal接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Animal{}

class Rabbit implements Animal{
//特有的方法
public void run(){
System.out.println("run");
}
}

class Bird implements Animal{
//特有的方法
public void fly(){
System.out.println("fly");
}
}

新特性可以减少Animal强转操作代码的编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Switch01{
public static void main(String[] args) {
Animal a = new Rabbit();
animalEat(a);
}

public static void animalEat(Animal a){
switch(a){
//如果a是Rabbit类型,则在强转之后赋值给r,然后再调用其特有的run方法
case Rabbit r -> r.run();
//如果a是Bird类型,则在强转之后赋值给b,然后调用其特有的fly方法
case Bird b -> b.fly();
//支持null的判断
case null -> System.out.println("null");
default -> System.out.println("no animal");
}
}

}

该功能在java17中是预览的,编译和运行需要加上额外的参数:

1
2
javac --enable-preview -source 17 Switch01.java
java --enable-preview Switch01

去除了AOT和JIT

AOT(Ahead-of-Time)是java9中新增的功能,可以先将应用中中的字节码编译成机器码。

Graal编译器作为使用java开发的JIT(just-in-time )即时编译器在java10中加入(注意这里的JIT不是之前java中的JIT,在JEP 317中有说明https://openJDK.java.net/jeps/317)。

以上两项功能由于使用量较少,且需要花费很多精力来维护,因此在java17中被移除了。当然你可以通过Graal VM来继续使用这些功能。

Java18新特性

这里只介绍一些跟开发关联度较大的特性,除此之外JDK18还更新了许多其他新特性,感兴趣的同学可以去Oracle官网查看:

https://www.oracle.com/java/technologies/javase/18-relnote-issues.html#NewFeature

默认使用UTF-8字符编码

从JDK18开始,默认使用UTF-8字符编码。我们可以通过如下参数修改其他字符编码:

1
-Dfile.encoding=UTF-8

将被移除的方法

在JDK18中标记了Object中的finalize方法,Thread中的stop方法将在未来被移除。

简单的web服务器

可以通过jwebserver命令启动JDK18中提供的静态web服务器,可以利用该工具查看一些原型,做简单的测试。在命令提示符中输入jwebserver命令后会启动,然后在浏览器中输入:http://127.0.0.1:8000/ 即可看到当前命令提示符路径下的文件了。

@snippet注解

以前在文档注释中编写代码时需要添加code标签,使用较为不便,通过**@snippet注解可以更方便的将文档注释中的代码展示在api文档中。**

Java19新特性

Virtual Threads (Preview)(虚拟线程)

该特性在java19中是预览版,虚拟线程是一种用户态下的线程,类似go语言中的goroutines 和Erlang中的processes,虚拟线程并非比线程快,而是提高了应用的吞吐量,相比于传统的线程是由操作系统调度来看,虚拟线程是我们自己程序调度的线程。如果你对之前java提供的线程API比较熟悉了,那么在学习虚拟线程的时候会比较轻松,传统线程能运行的代码,虚拟线程也可以运行。虚拟线程的出现,并没有修改java原有的并发模型,也不会替代原有的线程虚拟线程主要作用是提升服务器端的吞吐量。

吞吐量的瓶颈

服务器应用程序的伸缩性受利特尔法则(Little’s Law)的制约,与下面3点有关

  1. 延迟:请求处理的耗时
  2. 并发量:同一时刻处理的请求数量
  3. 吞吐量:单位时间内处理的数据数量

比如一个服务器应用程序的延迟是50ms,处理10个并发请求,则吞吐量是200请求/秒(10 / 0.05),如果吞吐量要达到2000请求/秒,则处理的并发请求数量是100。按照1个请求对应一个线程的比例来看,要想提高吞吐量,线程数量也要增加。

java中的线程是在操作系统线程(OS thread)进行了一层包装,而操作系统中线程是重量级资源,在硬件配置确定的前提下,我们就不能创建更多的线程了,此时线程数量就限制了系统性能,为了解决该问题,虚拟线程就出现了。

image-20240911004144255

与虚拟地址可以映射到物理内存类似,java是将大量的虚拟线程映射到少量的操作系统线程,多个虚拟线程可以使用同一个操作系统线程,其创建所耗费的资源也是极其低廉的,无需系统调用和系统级别的上下文切换,且虚拟线程的生命周期短暂,不会有很深的栈的调用,一个虚拟线程的生命周期中只运行一个任务,因此我们可以创建大量的虚拟线程,且虚拟线程无需池化。

虚拟线程的应用场景

在服务器端的应用程序中,可能会有大量的并发任务需要执行,而虚拟线程能够明显的提高应用的吞吐量。下面的场景能够显著的提高程序的吞吐量

  • 至少几千的并发任务量
  • 任务为io密集型

下面代码中为每个任务创建一个线程,当任务量较多的时候,你的电脑可以感受到明显的卡顿(如果没有,可以增加任务数量试下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ExecutorService实现了AutoCloseable接口,可以自动关闭了
try (ExecutorService executor = Executors.newCachedThreadPool()) {
// 向executor中提交1000000个任务
IntStream.range(0, 1000000).forEach(
i -> {
executor.submit(() -> {
try {
// 睡眠1秒,模拟耗时操作
Thread.sleep(Duration.ofSeconds(1));
System.out.println("执行任务:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}

});
});
} catch (Exception e) {
e.printStackTrace();
}

将上面的代码改成虚拟线程之后,电脑不会感受到卡顿了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// newVirtualThreadPerTaskExecutor为每个任务创建一个虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1000_000).forEach(i -> {
executor.submit(() -> {
try {
// 睡眠1秒,模拟耗时操作
Thread.sleep(Duration.ofSeconds(1));
System.out.println("执行任务:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
}

平台线程和虚拟线程

平台线程(platform thread):指java中的线程,比如通过Executors.newFixedThreadPool()创建出来的线程,我们称之为平台线程。

虚拟线程并不会直接分配给cpu去执行,而是通过调度器分配给平台线程,平台线程再被调度器管理。java中虚拟线程的调度器采用了工作窃取的模式进行FIFO的操作,调度器的并行数默认是JVM获取的处理器数量(通过该方法获取的数量Runtime.getRuntime().availableProcessors()),调度器并非分时(time sharing)的。在使用虚拟线程编写程序时,不能控制虚拟线程何时分配给平台线程,也不能控制平台线程何时分配给cpu。

以前任务和平台线程的关系:

image-20240911004612979

使用虚拟线程之后,任务-虚拟线程-调度器-平台线程的关系,1个平台线程可以被调度器分配不同的虚拟线程:

image-20240911004633649

创建虚拟线程的方式

java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual对象进行创建的,我们后面再来讨论这个对象,下面先看下创建虚拟线程的三种方式:

1.通过Thread.startVirtualThread直接创建一个虚拟线程

1
2
3
4
5
6
7
8
9
10
// 创建任务
Runnable task = () -> {
System.out.println("执行任务");
};

// 创建虚拟线程将任务task传入并启动
Thread.startVirtualThread(task);

// 主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);

2.使用Thread.ofVirtual()方法创建

1
2
3
4
5
6
7
8
9
10
11
// 创建任务
Runnable task = () -> {
System.out.println(Thread.currentThread().getName());
};

// 创建虚拟线程命名为诺手,将任务task传入
Thread vt1 = Thread.ofVirtual().name("诺手").unstarted(task);
vt1.start();// 启动虚拟线程

// 主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);

也可以在创建虚拟线程的时候直接启动

1
2
3
4
5
6
7
8
9
10
// 创建任务
Runnable task = () -> {
System.out.println(Thread.currentThread().getName());
};

// 创建虚拟线程命名为诺手,将任务task传入并启动
Thread vt1 = Thread.ofVirtual().name("诺手").start(task);

// 主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);

3.通过ExecutorService创建,为每个任务分配一个虚拟线程,下面代码中提交了100个任务,对应会有100个虚拟线程进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
通过ExecutorService创建虚拟线程
ExecutorService实现了AutoCloseable接口,可以自动关闭了
*/
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 向executor中提交100个任务
IntStream.range(0, 100).forEach(i -> {
executor.submit(() -> {
// 睡眠1秒
try {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
}

现在平台线程和虚拟线程都是Thread的对象,那该如何区分该对象是平台线程还是虚拟线程?可以利用Thread中的isVirtual()方法进行判断,返回true表示虚拟线程:

1
2
3
4
5
6
7
8
// 创建任务
Runnable task = () -> {
System.out.println("执行任务");
};

// 创建虚拟线程将任务task传入并启动
Thread vt = Thread.startVirtualThread(task);
System.out.println(vt.isVirtual());

Java21新特性

java20中没有太大的变化,这里主要聊下java21的新特性,21是继java17之后,最新的LTS版本,该版本中虚拟线程称为了正式版,对虚拟线程不了解的同学可以看下之前的java19中的介绍。接下来我们看下java21中一些新特性。

字符串模板 STR

字符串模板可以让开发者更简洁的进行字符串拼接(例如拼接sql,xml,json等)。该特性并不是为字符串拼接运算符+提供的语法糖,也并非为了替换SpringBuffer和StringBuilder。

这个和Python的插值表达式很类似,可以做变量的替换,多行文本的变量替换,还可以做一些计算 。

利用STR模板进行字符串与变量的拼接:

1
2
3
4
String sport = "basketball";
String msg = STR."i like \{sport}";

System.out.println(msg);// i like basketball

这个特性目前是预览版,编译和运行需要添加额外的参数:

1
2
javac --enable-preview -source 21 Test.java
java --enable-preview Test

在js中字符串进行拼接时会采用下面的字符串插值写法

1
2
let sport = "basketball"
let msg = `i like ${sport}`

看起来字符串插值写法更简洁移动,不过若在java中使用这种字符串插值的写法拼接sql,可能会出现sql注入的问题,为了防止该问题,java提供了字符串模板表达式的方式

上面使用的STR是java中定义的模板处理器,它可以将变量的值取出,完成字符串的拼接。在每个java源文件中都引入了一个public static final修饰的STR属性,因此我们可以直接使用STR,STR通过打印STR可以知道它是java.lang.StringTemplate,是一个接口。

在StringTemplate中是通过调用interpolate方法来执行的,该方法分别传入了两个参数:

  • fragements:包含字符串模板中所有的字面量,是一个List
  • values:包含字符串模板中所有的变量,是一个List

而该方法又调用了JavaTemplateAccess中的interpolate方法,经过分析可以得知,它最终是通过String中的join方法将字面量和变量进行的拼接

scoped values 传递参数时无需声明形参

ThreadLocal的问题

scoped values 是一个隐藏的方法参数,只有方法可以访问scoped values,它可以让两个方法之间传递参数时无需声明形参。例如在UserDao类中编写了saveUser方法,LogDao类中编写了saveLog方法,那么在保存用户的时候需要保证事务,此时就需要在service层获取Connection对象,然后将该对象分别传入到两个Dao的方法中,但对于saveUser方法来说并不是直接使用Connection对象,却又不得不在方法的形参中写上该对象,其实仅从业务上来看,该方法中只要传入User对象就可以了。

int saveUser(Connection connection,User user);

对于上面的问题,开发者通常会使用ThreadLocal解决,但由于ThreadLocal在设计上的瑕疵,导致下面问题:

  1. 内存泄漏,在用完ThreadLocal之后若没有调用remove,这样就会出现内存泄漏。
  2. 增加开销,在具有继承关系的线程中,子线程需要为父线程中ThreadLocal里面的数据分配内存。
  3. 混乱的可变,任何可以调用ThreadLocal中get方法的代码都可以随时调用set方法,这样就不易辨别哪些方法是按照什么顺序来更新的共享数据。

随着虚拟线程的到来,内存泄漏问题就不用担心了,由于虚拟线程会很快的终止,此时会自动删除ThreadLocal中的数据,这样就不用调用remove方法了。但虚拟线程的数量通常是多的,试想下上百万个虚拟线程都要拷贝一份ThreadLocal中的变量,这会使内存承受更大的压力。为了解决这些问题,scoped values就出现了。

ScopeValue初体验

在java21中新增了ScopeValue类,为了便于多个方法使用,通常会将该类的对象声明为static final ,每个线程都能访问自己的scope value,与ThreadLocal不同的是,它只会被write 1次且仅在线程绑定的期间内有效。

下面代码模拟了送礼和收礼的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test{
private static final ScopedValue<String> GIFT = ScopedValue.newInstance();

public static void main(String[] args) {
Test t = new Test();
t.giveGift();
}

//送礼
public void giveGift() {
/*
在对象GIFT中增加字符串手机,当run方法执行时,
会拷贝一份副本与当前线程绑定,当run方法结束时解绑。
由此可见,这里GIFT中的字符串仅在收礼方法中可以取得。
*/
ScopedValue.where(GIFT, "手机").run(() -> receiveGift());
}

//收礼
public void receiveGift() {
System.out.println(GIFT.get()); // 手机
}

}

多线程操作相同的ScopeValue

不同的线程在操作同一个ScopeValue时,相互间不会影响,其本质是利用了Thread类中scopedValueBindings属性进行的线程绑定。

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
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test{
private static final ScopedValue<String> GIFT = ScopedValue.newInstance();

public static void main(String[] args) {
Test t = new Test();

ExecutorService pool = Executors.newCachedThreadPool();

for (int i = 0; i < 10; i++) {
pool.submit(()->{
t.giveGift();
});
}

pool.shutdown();
}

//向ScopedValue中添加当前线程的名字
public void giveGift() {
ScopedValue.where(GIFT, Thread.currentThread().getName()).run(() -> receiveGift());
}

public void receiveGift() {
System.out.println(GIFT.get());
}

}

record pattern 解构

通过该特性可以解构record类型中的值,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test{
public static void main(String[] args) {
Student s = new Student(10, "jordan");
printSum(s);
}

static void printSum(Object obj) {
//这里的Student(int a, String b)就是 record pattern
if (obj instanceof Student(int a, String b)) {
System.out.println("id:" + a);
System.out.println("name:" + b);
}
}

}
record Student(int id, String name) {}

switch格式匹配

之前的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test{
public static void main(String[] args) {
Integer i = 10;
String str = getObjInstance(i);
System.out.println(str);
}

public static String getObjInstance(Object obj) {
String objInstance = "";
if(obj == null){
objInstance = "空对象"
} else if (obj instanceof Integer i) {
objInstance = "Integer 对象:" + i;
} else if (obj instanceof Double d) {
objInstance = "Double 对象:" + d;
} else if (obj instanceof String s) {
objInstance = "String 对象:" + s;
}
return objInstance;
}

}

新的写法,代码更加简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test{
public static void main(String[] args) {
Integer i = 10;
String str = getObjInstance(i);
System.out.println(str);
}

public static String getObjInstance(Object obj) {

return switch(obj){
case null -> "空对象";
case Integer i -> "Integer 对象:" + i;
case Double d -> "Double对象:" + d;
case String s -> "String对象:" + s;
default -> obj.toString();
};
}

}

可以在switch中使用when

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
public class Test{
public static void main(String[] args) {
yesOrNo("yes");
}

public static void yesOrNo(String obj) {

switch(obj){
case null -> {System.out.println("空对象");}
case String s
when s.equalsIgnoreCase("yes") -> {
System.out.println("确定");
}
case String s
when s.equalsIgnoreCase("no") -> {
System.out.println("取消");
}
//最后的case要写,否则编译回报错
case String s -> {
System.out.println("请输入yes或no");
}

};

}

}

Unnamed Classes and Instance Main Methods

对于初学者来说,写的第一个HelloWorld代码有太多的概念,为了方便初学者快速编写第一段java代码,这里提出了无名类和实例main方法,下面代码可以直接运行编译,相当于是少了类的定义,main方法的修饰符和形参也省略掉了

1
2
3
void main() {
System.out.println("Hello, World!");
}

Structured Concurrency

该特性主要作用是在使用虚拟线程时,可以使任务和子任务的代码编写起来可读性更强,维护性更高,更加可靠。

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
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
import java.util.function.Supplier;

public class Test {

public static void main(String[] args) throws ExecutionException, InterruptedException {
Food f = new Test().handle();
System.out.println(f);
}

Food handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> yaoZi = scope.fork(() -> "新鲜大腰子烤好了");// 烤腰子的任务
Supplier<String> drink = scope.fork(() -> "奶茶做好了");// 买饮料的任务

scope.join() // 将2个子任务都加入
.throwIfFailed(); // 失败传播

// 当两个子任务都成功后,最终才能吃上饭
return new Food(yaoZi.get(), drink.get());
}
}

}

record Food(String yaoZi, String drink) {
}

Java22新特性

无名变量 __

这个和Go的下划线挺类似的,一般使用在try-catch中,直接将e,变为下划线。主要是区分出需要使用的变量和不需要使用的变量,因为不是所有的返回值都是你需要的,这样可以使代码逻辑更加清晰

以下场景可以使用Unnamed Variables

  • 局部变量

  • try-with-resource

  • 循环头中声明的变量

  • catch中声明的变量

  • lambda表达式中的参数

参考文章:1.java9新特性 (yuque.com)


文章作者: zoloy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zoloy !
评论
  目录