跳至主要內容

Java 21新特性


Java20中没有太大的变化,这里主要聊下Java21的新特性,21是继Java17之后,最新的LTS版本,该版本中虚拟线程称为了正式版,Java 19中是预览版

字符串模板

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

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

String sport = "basketball";
String msg = STR."i like \{sport}";

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

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

Javac --enable-preview -source 21 Test.Java
Java --enable-preview Test

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

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方法将字面量和变量进行的拼接

其他使用示例,在STR中可以进行基本的运算(支持三元运算)

int x = 10, y = 20;
String result = STR."\{x} + \{y} = \{x + y}";
System.out.println(result);//10 + 20 = 30

调用方法:

String result = STR."获取一个随机数: \{Math.random()}";
System.out.println(result);

获取属性:

String result = STR."int最大值是: \{Integer.MAX_VALUE}";
System.out.println(result);

查看时间:

String result = STR."现在时间: \{new SimpleDateFormat("yyyy-MM-dd").format(new Date())}";
System.out.println(result);

计数操作:

int index = 0;
String result = STR."\{index++},\{index++},\{index++}";
System.out.println(result);

获取数组数据:

String[] cars = {"bmw","benz","audi"};
String result = STR."\{cars[0]},\{cars[1]},\{cars[2]}";
System.out.println(result);

拼接多行数据:

String name    = "jordan";
String phone   = "13388888888";
String address = "北京";
String json = STR."""
{
"name":    "\{name}",
"phone":   "\{phone}",
"address": "\{address}"
}
""";

System.out.println(json);

自己定义字符串模板,通过StringTemplate来自定义模板

var INTER = StringTemplate.Processor.of((StringTemplate st) -> {
    StringBuilder sb = new StringBuilder();
    Iterator<String> fragIter = st.fragments().iterator();
    for (Object value : st.values()) {
        sb.append(fragIter.next());//字符串中的字面量
        sb.append(value);
    }
    sb.append(fragIter.next());
    return sb.toString();
});


int x = 10, y = 20;
String s = INTER."\{x} plus \{y} equals \{x + y}";

System.out.println(s);

scoped values

ThreadLocal的问题

在 Web 应用中,一个请求通常会被多个线程处理,每个线程需要访问自己的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。但由于ThreadLocal在设计上的瑕疵,导致下面问题:

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

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

ScopeValue初体验

基本用法

ScopedValue对象用jdk.incubator.concurrent包中的ScopedValue类来表示。使用ScopedValue的第一步是创建ScopedValue对象,通过静态方法newInstance来完成,ScopedValue对象一般声明为static final,每个线程都能访问自己的scope value,与ThreadLocal不同的是,它只会被write 1次且仅在线程绑定的期间内有效。

下一步是指定ScopedValue对象的值和作用域,通过静态方法where来完成。where方法有 3 个参数:

  • ScopedValue 对象
  • ScopedValue 对象所绑定的值
  • RunnableCallable对象,表示ScopedValue对象的作用域

RunnableCallable对象执行过程中,其中的代码可以用ScopedValue对象的get方法获取到where方法调用时绑定的值。这个作用域是动态的,取决于RunnableCallable对象所调用的方法,以及这些方法所调用的其他方法。当RunnableCallable对象执行完成之后,ScopedValue对象会失去绑定,不能再通过get方法获取值。在当前作用域中,ScopedValue对象的值是不可变的,除非再次调用where方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。使用作用域值有以下几个优势:

  • 提高数据安全性:由于作用域值只能在当前范围内访问,因此可以避免数据泄露或被恶意修改。
  • 提高数据效率:由于作用域值是不可变的,并且可以在线程之间共享,因此可以减少数据复制或同步的开销。
  • 提高代码清晰度:由于作用域值只能在当前范围内访问,因此可以减少参数传递或全局变量的使用。

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

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属性进行的线程绑定。

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()); 
    }

}

ScopeValue的修改

通过上面的示例可以看到,ScopeValue的值是在第一次使用where的时候就设置好了,该值在当前线程使用的期间是不会被修改的,这样就提高了性能。当然,我们也可以修改ScopeValue中的值,但需要注意,这里的修改会不影响本次方法中读取的值,而是会导致where后run中调用的方法里面的值发生变化。

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() {
        ScopedValue.where(GIFT, "500元购物卡").run(() -> receiveMiddleMan());
    }


    //中间人
    public void receiveMiddleMan(){
        System.out.println(GIFT.get());//500
        //修改GIFT中的值,仅对run中调用的receiveGift方法生效
        ScopedValue.where(GIFT, "200元购物卡").run(() -> receiveGift());
        System.out.println(GIFT.get());//500
    }

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

}

所以,从以上分析可以看到,ScopedValue有一定的权限控制:就算在同一个线程中也不能任意修改ScopedValue的值,就算修改了对当前作用域(方法)也是无效的。另外ScopedValue也不需要手动remove,关于这块就需要分析它的实现原理了。这块内容待更新~

集合序列

image-20240425202520731
image-20240425202520731

在Java.util包下新增了3个接口

  1. SequencedCollection
  2. SequencedSet
  3. SequencedMap

通过这些接口可以为之前的部分List,Set,Map的实现类增加新的方法,以List为例:

List<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
list.add(3);

List<Integer> reversedList = list.reversed();//反转List
System.out.println(reversedList);

list.addFirst(4);//从List前面添加元素
list.addLast(5);//从List后面添加元素
System.out.println(list);

record pattern

Java14引入的新特性。通过该特性可以解构record类型中的值,例如

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格式匹配

之前的写法

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;
    }

}

新的写法,代码更加简洁

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

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方法的修饰符和形参也省略掉了

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

Structured Concurrency

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

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) {
}
seven97官方微信公众号
seven97官方微信公众号