java8之后版本的变化-涵盖java9~21新特性-哔哩哔哩
javase

各版本支持时间

接口私有方法

在jdk9中新增了接口私有方法,即我们可以在接口中声明private修饰的方法了,这样的话,接口越来越像抽象类

public interface MyInterface {
    //定义私有方法
    private void m1() {
        System.out.println("123");
    }
    
    //default中调用
    default void m2() {
        m1();
    }
}

Java 9 私有接口方法

改进的try with resource

java7中新增了try with resource语法用来自动关闭资源文件,在IO流和jdbc部分使用的比较多。使用方式是将需要自动关闭的资源对象的创建放到try后面的小括号中,在jdk9中我们可以将这些资源对象的创建代码放到小括号外面,然后将需要关闭的对象名放到try后面的小括号中即可,示例:

/*
    改进了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();
        }
    }
}

@Deprecated注解的变化

该注解用于标识废弃的内容,在jdk9中新增了2个内容:
● String since() default “”:标识是从哪个版本开始废弃
● boolean forRemoval() default false:标识该废弃的内容会在未来的某个版本中移除

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

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

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

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

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

String新增方法

strip方法,可以去除首尾空格,与之前的trim的区别是还可以去除unicode编码的空白字符,例如

char c = '\u2000';//Unicdoe空白字符
String str = c + "abc" + c;
System.out.println(str.strip());
System.out.println(str.trim());

System.out.println(str.stripLeading());//去除前面的空格
System.out.println(str.stripTrailing());//去除后面的空格

isBlank方法,判断字符串长度是否为0,或者是否是空格,制表符等其他空白字符

String str = " ";
System.out.println(str.isBlank());

repeat方法,字符串重复的次数

String str = "monkey";
System.out.println(str.repeat(4));

升级的switch语句

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

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,这样使用

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以上的版本,正常的编译和运行即可。否则需要使用预览功能来编译和运行
IDEA启用预览功能

升级的switch语句

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

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

文本块的变化

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

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

jdk13之后可以直接这样写:

String s = """
            Hello
            World
            Learn
            Java
           """;
  System.out.println(s);

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

instanceof模式匹配

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

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

友好的空指针(NullPointerException)提示

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了。此外,还可以使用下面参数来查看:

java -XX:+ShowCodeDetailsInExceptionMessages TestNull

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

record类型

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

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

编写测试类:

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

这个功能在当前版本是预览版的功能,如果是使用的14的话需要启用预览模式
记录类型有自动生成的成员,包括:
● 状态描述中的每个组件都有对应的private final字段。
● 状态描述中的每个组件都有对应的public访问方法。方法的名称与组件名称相同。
● 一个包含全部组件的公开构造器,用来初始化对应组件。
● 实现了equals()和hashCode()方法。equals()要求全部组件都必须相等。
● 实现了toString(),输出全部组件的信息。

java15新特性

密封类和接口,作用是限制一个类可以由哪些子类继承或者实现。
1. 如果指定模块的话,sealed class和其子类必须在同一个模块下。如果没有指定模块,则需要在同一个包下。
2. sealed class指定的子类必须直接继承该sealed class。
3. sealed class的子类要用final修饰。
4. sealed class的子类如果不想用final修饰的话,可以将子类声明为sealed class。
Animal类,在指定允许继承的子类时可以使用全限定名

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

        public void eat(){}
}

Cat类

public final class Cat extends Animal{
    public void eat(){
        System.out.println("123");
    }
}

Dog类

public sealed class Dog extends Animal
    permits Husky {}

Husky类

public final class Husky extends Dog{
}

Test类

public class Test{
    public static void main(String[] args){
        Cat c = new Cat();
        c.eat();
        Dog d = new Dog();
    }
}

文本块

文本块由预览版变为正式版

无需配置环境变量

win系统中安装完成之后会自动将java.exe, javaw.exe, javac.exe, jshell.exe这几个命令添加到环境变量中。这部分可以打开环境变量看下。

switch语法的变化(预览)

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强转操作代码的编写:

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

}

Sealed Classes

在jdk15中已经添加了Sealed Classes,只不过当时是作为预览版,经历了2个版本之后,在jdk17中Sealed Classes已经成为正式版了。Sealed Classes的作用是可以限制一个类或者接口可以由哪些子类继承或者实现。

默认使用UTF-8字符编码

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

-Dfile.encoding=UTF-8

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

与虚拟地址可以映射到物理内存类似,java是将大量的虚拟线程映射到少量的操作系统线程,多个虚拟线程可以使用同一个操作系统线程,其创建所耗费的资源也是极其低廉的,无需系统调用和系统级别的上下文切换,且虚拟线程的生命周期短暂,不会有很深的栈的调用,一个虚拟线程的生命周期中只运行一个任务,因此我们可以创建大量的虚拟线程,且虚拟线程无需池化。
虚拟线程的应用场景
在服务器端的应用程序中,可能会有大量的并发任务需要执行,而虚拟线程能够明显的提高应用的吞吐量。下面的场景能够显著的提高程序的吞吐量:
● 至少几千的并发任务量
● 任务为io密集型 计算密集型和io密集型的区别
下面代码中为每个任务创建一个线程,当任务量较多的时候,你的电脑可以感受到明显的卡顿(如果没有,可以增加任务数量试下):

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

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

//newVirtualThreadPerTaskExecutor为每个任务创建一个虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1000000).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。
以前任务和平台线程的关系:

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

携带器

调度器将虚拟线程挂载到平台线程之后,该平台线程叫做虚拟线程的携带器,调度器并不维护虚拟线程和携带器之间的关联关系,因此在一个虚拟线程的生命周期中可以被分配到不同的携带器,即虚拟线程运行了一小段代码后,可能会脱离携带器,此时其他的虚拟线程会被分配到这个携带器上。
携带器和虚拟线程是相互独立的,比如:
● 虚拟线程不能使用携带器的标识,Thread.current()方法获取的是虚拟线程本身。
● 两者有各自的栈空间。
● 两者不能访问对方的Thread Local变量。
在程序的执行过程中,虚拟线程遇到阻塞的操作时大部分情况下会被解除挂载,阻塞结束后,虚拟线程会被调度器重新挂载到携带器上,因此虚拟线程会频繁的挂载和解除挂载,这并不会导致操作系统线程的阻塞。下面的代码在执行两个get方法和send方法(会有io操作)时会使虚拟线程发生挂载和解除挂载:

response.send(future1.get() + future2.get());

有些阻塞操作并不会导致虚拟线程解除挂载,这样会同时阻塞携带器和操作系统线程,例如:操作系统基本的文件操作,java中的Object.wait()方法。下面两种情况不会导致虚拟线程的解除挂载:
1. 执行synchronized同步代码(会导致携带器阻塞,所以建议使用ReentrantLock替换掉synchronized)
2. 执行本地方法或外部函数

虚拟线程和平台线程api的区别

从内存空间上来说,虚拟线程的栈空间可以看作是一个大块的栈对象,它被存储在了java堆中,相比于单独存储对象,堆中存储虚拟线程的栈会造成一些空间的浪费,这点在后续的java版本中应该会得到改善,当然这样也是有一些好处的,就是可以重复利用这部分栈空间,不用多次申请开辟新的内存地址。虚拟线程的栈空间最大可以达到平台线程的栈空间容量。
虚拟线程并不是GC root,其中的引用不会出现stop-world,当虚拟线程被阻塞之后比如BlockingQueue.take(),平台线程既不能获取到虚拟线程,也不能获取到queue队列,这样该平台线程可能会被回收掉,虚拟线程在运行或阻塞时不会被GC
● 通过Thread构造方法创建的线程都是平台线程
● 虚拟线程是守护线程,不能通过setDaemon方法改成非守护线程
● 虚拟线程的优先级是默认的5,不能被修改,将来的版本可能允许修改
● 虚拟线程不支持stop(),suspend(),resume()方法

创建虚拟线程的方式

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

//创建任务
Runnable task = () -> {
    System.out.println("执行任务");
};

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

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

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

//创建任务
Runnable task = () -> {
    System.out.println(Thread.currentThread().getName());
};

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

//主线程睡眠,否则可能看不到控制台的打印
TimeUnit.SECONDS.sleep(1);
也可以在创建虚拟线程的时候直接启动
//创建任务
Runnable task = () -> {
    System.out.println(Thread.currentThread().getName());
};

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

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

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

/*
    通过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表示虚拟线程:

//创建任务
Runnable task = () -> {
    System.out.println("执行任务");
};

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

字符串模板

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

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

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

这个特性目前是预览版,编译和运行需要添加额外的参数
其他使用示例,在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);

集合序列

在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);

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

        };

    }

}