当前正在阅读
其他内容
预计阅读时间: 2 小时
  知识库提供的所有文档均为本站版权所有,禁止任何未经授权的个人或企业发布、传播、售卖本站提供的文档,如经发现,本站有权起诉侵权方并追究法律责任。
image-20230306174814057

Java新特性介绍

注意: 推荐完成此路线所有前置内容后,再来学习本篇。

经过前面的学习,我们基本已经了解了Java 8及之前的所有语法,不过,Java 8是Oracle 公司于 2014 年 3 月 18 日发布的,距离今天已经过了近十年的时间了,Java并没有就此止步,而是继续不断发展壮大,几乎每隔6个月,就会冒出一个新版本,最新的版本已经快要迭代到Java 20了,与Java 8相差了足足十来个版本,但是由于Java 8的稳定和生态完善(目前仍是LTS长期维护版本),依然有很多公司在坚持使用Java 8,不过随着SpringBoot 3.0的到来,现在强制要求使用Java 17版本(同样也是LTS长期维护版本),下一个Java版本的时代,或许已经临近了。

image-20230306174835142

随着这些主流框架全面拥抱Java 17,为了不被时代所淘汰,我们的学习之路,也要继续前行了。就像很多年前Java 6还是主流的时代,终究还是被Java 8所取代一样。

在本篇视频中,我们将介绍Java 9 - Java 17这些版本的所有新增特性,这里推荐各位小伙伴提前准备好JDK 17环境(Oracle JDK 17已全面支持arm芯片的Mac电脑,请放心食用)

image-20230306174844769

全篇视频挑重点说,不墨迹,开始吧。

Java 8 关键特性回顾

在开始之前,我们先来回顾一下Java 8中学习的Lambda表达式和Optional类,有关Stream API请各位小伙伴回顾一下Java SE篇视频教程,这里不再进行介绍。

Lambda表达式

在Java 8之前,我们在某些情况下可能需要用到匿名内部类,比如:

java 复制代码
public static void main(String[] args) {
    //现在我们想新建一个线程来搞事情
    Thread thread = new Thread(new Runnable() {   //创建一个实现Runnable的匿名内部类
        @Override
        public void run() {   //具体的实现逻辑
            System.out.println("Hello World!");
        }
    });
    thread.start();
}

在创建Thread时,我们需要传入一个Runnable接口的实现类,来指定具体的在新的线程中要执行的任务,相关的逻辑需要我们在run()方法中实现,这时为了方便,我们就直接使用匿名内部类的方式传入一个实现,但是这样的写法实在是太过臃肿了。

在Java 8之后,我们可以对类似于这种匿名内部类的写法,进行缩减,实际上我们进行观察会发现,真正有用的那一部分代码,实际上就是我们对run()方法的具体实现,而其他的部分实际上在任何地方编写都是一模一样的,那么我们能否针对于这种情况进行优化呢?我们现在只需要一个简短的lambda表达式即可:

java 复制代码
public static void main(String[] args) {
    //现在我们想新建一个线程来做事情
    Thread thread = new Thread(() -> {
        System.out.println("Hello World!");  //只需留下我们需要具体实现的方法体
    });
    thread.start();
}

我们可以发现,原本需要完整编写包括类、方法在内的所有内容,全部不再需要,而是直接使用类似于() ‐> { 代码语句 }的形式进行替换即可。是不是感觉瞬间代码清爽了N倍?

当然这只是一种写法而已,如果各位不好理解,可以将其视为之前匿名内部类写法的一种缩短。

但是注意,它的底层其实并不只是简简单单的语法糖替换,而是通过invokedynamic指令实现的,不难发现,匿名内部类会在编译时创建一个单独的class文件,但是lambda却不会,间接说明编译之后lambda并不是以匿名内部类的形式存在的:

java 复制代码
//现在我们想新建一个线程来做事情
Thread thread = new Thread(() -> {
    throw new UnsupportedOperationException();   //这里我们拋个异常看看
});
thread.start();
image-20230306174907441

可以看到,实际上是Main类中的lambda$main$0()方法抛出的异常,但是我们的Main类中压根没有这个方法,很明显是自动生成的。所以,与其说Lambda是匿名内部类的语法糖,不如说是我们为所需要的接口提供了一个方法作为它的实现。比如Runnable接口需要一个方法体对它的run()方法进行实现,而这里我们就通过lambda的形式给了它一个方法体,这样就万事具备了,而之后创建实现类就只需要交给JVM去处理就好了。

我们来看一下Lambda表达式的具体规范:

  • 标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
  • 和匿名内部类不同,Lambda仅支持接口,不支持抽象类
  • 接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)

比如我们之前使用的Runable类:

java 复制代码
@FunctionalInterface   //添加了此注解的接口,都支持lambda表达式,符合函数式接口定义
public interface Runnable {
    public abstract void run();   //有且仅有一个抽象方法,此方法返回值为void,且没有参数
}

因此,Runable的的匿名内部类实现,就可以简写为:

java 复制代码
Runnable runnable = () -> {    };

我们也可以写一个玩玩:

java 复制代码
@FunctionalInterface
public interface Test {   //接口类型
    String test(Integer i);    //只有这一个抽象方法,且接受一个int类型参数,返回一个String类型结果
}

它的Lambda表达式的实现就可以写为:

java 复制代码
Test test = (Integer i) -> { return i+""; };  //这里我们就简单将i转换为字符串形式

不过还可以进行优化,首先方法参数类型是可以省略的:

java 复制代码
Test test = (i) -> { return i+""; };

由于只有一个参数,可以不用添加小括号(多个参数时需要):

java 复制代码
Test test = i -> { return i+""; };

由于仅有返回语句这一行,所以可以直接写最终返回的结果,并且无需花括号:

java 复制代码
Test test = i -> i+"";

这样,相比我们之前直接去编写一个匿名内部类,是不是简介了很多很多。当然,除了我们手动编写接口中抽象方法的方法体之外,如果已经有实现好的方法,是可以直接拿过来用的,比如:

java 复制代码
String test(Integer i);   //接口中的定义
java 复制代码
public static String impl(Integer i){   //现在有一个静态方法,刚好匹配接口中抽象方法的返回值和参数列表
    return "我是已经存在的实现"+i;
}

所以,我们可以直接将此方法,作为lambda表达式的方法体实现(其实这就是一种方法引用,引用了一个方法过来,这也是为什么前面说是我们为所需要的接口提供了一个方法作为它的实现,是不是越来越体会到这句话的精髓了):

java 复制代码
public static void main(String[] args) {
    Test test = Main::impl;    //使用 类名::方法名称 的形式来直接引用一个已有的方法作为实现
}

public static String impl(Integer i){
    return "我是已经存在的实现"+i;
}

比如我们现在需要对一个数组进行排序:

java 复制代码
public static void main(String[] args) {
    Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};   //来个数组
    Arrays.sort(array, new Comparator<Integer>() {   //Arrays.sort()可以由我们自己指定排序规则,只需要实现Comparator方法即可
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    });
    System.out.println(Arrays.toString(array));   //按从小到大的顺序排列
}

但是我们发现,Integer类中有一个叫做compare的静态方法:

java 复制代码
public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

这个方法是一个静态方法,但是它却和Comparator需要实现的方法返回值和参数定义一模一样,所以,懂的都懂:

java 复制代码
public static void main(String[] args) {
    Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
    Arrays.sort(array, Integer::compare);   //直接指定一手,效果和上面是一模一样
    System.out.println(Arrays.toString(array));
}

那么要是不是静态方法而是普通的成员方法呢?我们注意到Comparator要求我们实现的方法为:

java 复制代码
public int compare(Integer o1, Integer o2) {
     return o1 - o2;
}

其中o1和o2都是Integer类型的,我们发现Integer类中有一个compareTo方法:

java 复制代码
public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

只不过这个方法并不是静态的,而是对象所有:

java 复制代码
Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
Arrays.sort(array, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);   //这样进行比较也行,和上面效果依然是一样的
    }
});
System.out.println(Arrays.toString(array));

但是此时我们会发现,IDEA提示我们可以缩写,这是为什么呢?实际上,当我们使用非静态方法时,会使用抽象方参数列表的第一个作为目标对象,后续参数作为目标对象成员方法的参数,也就是说,此时,o1作为目标对象,o2作为参数,正好匹配了compareTo方法,所以,直接缩写:

java 复制代码
public static void main(String[] args) {
    Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
    Arrays.sort(array, Integer::compareTo);  //注意这里调用的不是静态方法
    System.out.println(Arrays.toString(array));
}

成员方法也可以让对象本身不成为参与的那一方,仅仅引用方法:

java 复制代码
public static void main(String[] args) {
    Main mainObject = new Main();
    Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
    Arrays.sort(array, mainObject::reserve);  //使用Main类的成员方法,但是mainObject对象并未参与进来,只是借用了一下刚好匹配的方法
    System.out.println(Arrays.toString(array));
}

public int reserve(Integer a, Integer b){  //现在Main类中有一个刚好匹配的方法
    return b.compareTo(a);
}

当然,类的构造方法同样可以作为方法引用传递:

java 复制代码
public interface Test {
    String test(String str);   //现在我们需要一个参数为String返回值为String的实现
}

我们发现,String类中刚好有一个:

java 复制代码
public String(String original) {   //由于String类的构造方法返回的肯定是一个String类型的对象,且此构造方法需要一个String类型的对象,所以,正好匹配了接口中的
    this.value = original.value;
    this.coder = original.coder;
    this.hash = original.hash;
}

于是乎:

java 复制代码
public static void main(String[] args) {
    Test test = String::new;   //没错,构造方法直接使用new关键字就行
}

当然除了上面提到的这些情况可以使用方法引用之外,还有很多地方都可以,还请各位小伙伴自行探索了。Java 8也为我们提供了一些内置的函数式接口供我们使用:Consumer、Function、Supplier等,具体请回顾一下JavaSE篇视频教程。

Optional类

Java 8中新引入了Optional特性,来让我们更优雅的处理空指针异常。我们先来看看下面这个例子:

java 复制代码
public static void hello(String str){   //现在我们要实现一个方法,将传入的字符串转换为小写并打印
    System.out.println(str.toLowerCase());  //那太简单了吧,直接转换打印一气呵成
}

但是这样实现的话,我们少考虑了一个问题,万一给进来的strnull呢?如果是null的话,在调用toLowerCase方法时岂不是直接空指针异常了?所以我们还得判空一下:

java 复制代码
public static void hello(String str){
    if(str != null) {
        System.out.println(str.toLowerCase());
    }
}

但是这样写着就不能一气呵成了,我现在又有强迫症,我就想一行解决,这时,Optional来了,我们可以将任何的变量包装进Optional类中使用:

java 复制代码
public static void hello(String str){
    Optional
            .ofNullable(str)   //将str包装进Optional
            .ifPresent(s -> {   //ifPresent表示只有对象不为null才会执行里面的逻辑,实现一个Consumer(接受一个参数,返回值为void)
                System.out.println(s);   
            });
}

由于这里只有一句打印,所以我们来优化一下:

java 复制代码
public static void hello(String str){
    Optional
            .ofNullable(str)   //将str包装进Optional
            .ifPresent(System.out::println);  
  	//println也是接受一个String参数,返回void,所以这里使用我们前面提到的方法引用的写法
}

这样,我们就又可以一气呵成了,是不是感觉比之前的写法更优雅。

除了在不为空时执行的操作外,还可以直接从Optional中获取被包装的对象:

java 复制代码
System.out.println(Optional.ofNullable(str).get());

不过此时当被包装的对象为null时会直接抛出异常,当然,我们还可以指定如果get的对象为null的替代方案:

java 复制代码
System.out.println(Optional.ofNullable(str).orElse("VVV"));   //orElse表示如果为空就返回里面的内容

其他操作还请回顾JavaSE篇视频教程。

Java 9 新特性

这一部分,我们将介绍Java 9为我们带来的新特性,Java 9的主要特性有,全新的模块机制、接口的private方法等。

模块机制

在我们之前的开发中,不知道各位有没有发现一个问题,就是当我们导入一个jar包作为依赖时(包括JDK官方库),实际上很多功能我们并不会用到,但是由于它们是属于同一个依赖捆绑在一起,这样就会导致我们可能只用到一部分内容,但是需要引用一个完整的类库,实际上我们可以把用不到的类库排除掉,大大降低依赖库的规模。

于是,Java 9引入了模块机制来对这种情况进行优化,在之前的我们的项目是这样的:

image-20230306174940813

而在引入模块机制之后:

image-20230306174956804

可以看到,模块可以由一个或者多个在一起的 Java 包组成,通过将这些包分出不同的模块,我们就可以按照模块的方式进行管理了。这里我们创建一个新的项目,并在src目录下,新建module-info.java文件表示此项目采用模块管理机制:

java 复制代码
module NewHelloWorld {  //模块名称随便起一个就可以,但是注意必须是唯一的,以及模块内的包名也得是唯一的,即使模块不同
    
}

接着我们来创建一个主类:

image-20230306175006986

程序可以正常运行,貌似和之前没啥区别,不过我们发现,JDK为我们提供的某些框架不见了:

image-20230306175016858

Java为我们提供的logging相关日志库呢?我们发现现在居然不见了?实际上它就是被作为一个模块单独存在,这里我们需进行模块导入:

java 复制代码
module NewHelloWorld {  //模块名称随便起一个就可以
    requires java.logging;   //除了JDK的一些常用包之外,只有我们明确需要的模块才会导入依赖库
  	//当然如果要导入JavaSE的所有依赖,想之前一样的话,直接 requires java.se;  即可
}

这里我们导入java.logging相关模块后,就可以正常使用Logger了:

image-20230306175043681
image-20230306175035293

是不是瞬间感觉编写代码时清爽了许多,全新的模块化机制提供了另一个级别的Java代码可见性、可访问性的控制,不过,你以为仅仅是做了包的分离吗?我们可以来尝试通过反射获取JDK提供的类中的字段:

java 复制代码
//Java17版本的String类
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    @Stable
    private final byte[] value;  //自JDK9后,为了提高性能,String底层数据存放的是byte[]而不是char[]
java 复制代码
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    Class<String> stringClass = String.class;
    Field field = stringClass.getDeclaredField("value");   //这里我们通过反射来获取String类中的value字段
    field.setAccessible(true);   //由于是private访问权限,所以我们修改一下
    System.out.println(field.get("ABCD"));
}

但是我们发现,在程序运行之后,修改操作被阻止了:

image-20230306175056384

反射 API 的 Java 9 封装和安全性得到了改进,如果模块没有明确授权给其他模块使用反射的权限,那么其他模块是不允许使用反射进行修改的,看来Unsafe类是玩不成了。

我们现在就来细嗦一下这个模块机制,首先模块具有四种类型:

  • 系统模块: 来自JDK和JRE的模块(官方提供的模块,比如我们上面用的),我们也可以直接使用java --list-modules命令来列出所有的模块,不同的模块会导出不同的包供我们使用。
  • 应用程序模块: 我们自己写的Java模块项目。
  • 自动模块: 可能有些库并不是Java 9以上的模块项目,这种时候就需要做兼容了,默认情况下是直接导出所有的包,可以访问所有其他模块提供的类,不然之前版本的库就用不了了。
  • 未命名模块: 我们自己创建的一个Java项目,如果没有创建module-info.java,那么会按照未命名模块进行处理,未命名模块同样可以访问所有其他模块提供的类,这样我们之前写的Java 8代码才能正常地在Java 9以及之后的版本下运行。不过,由于没有使用Java 9的模块新特性,未命名模块只能默认暴露给其他未命名的模块和自动模块,应用程序模块无法访问这些类(实际上就是传统Java 8以下的编程模式,因为没有模块只需要导包就行)

这里我们就来创建两个项目,看看如何使用模块机制,首先我们在项目A中,添加一个User类,一会项目B需要用到:

java 复制代码
package com.test;

public class User {
    String name;
    int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name+" ("+age+"岁)";
    }
}

接着我们编写一下项目A的模块设置:

image-20230306175110049

这里我们将com.test包下所有内容都暴露出去,默认情况下所有的包都是私有的,就算其他项目将此项目作为依赖也无法使用。

接着我们现在想要在项目B中使用项目A的User类,我们需要进行导入:

image-20230306175950291

现在我们就可以在Main类中使用模块module.a中暴露出来的包内容了:

java 复制代码
import com.test.User;   //如果模块module.a不暴露,那么将无法导入

public class Main {
    public static void main(String[] args) {
        User user = new User("lbw", 18);
        System.out.println(user);
    }
}

当然除了普通的exports进行包的暴露之外,我们也可以直接指定将包暴露给指定的模块:

java 复制代码
module module.a {
    exports com.test to module.b;   //这里我们将com.test包暴露给指定的模块module.b,非指定的模块即使导入也无法使用
}

不过现在还有一个问题,如果模块module.a依赖于其他模块,那么会不会传递给依赖于模块module.a的模块呢?

java 复制代码
module module.a {
    exports com.test to module.b;   //使用exports将com.test包下所有内容暴露出去,这样其他模块才能导入
    requires java.logging;   //这里添加一个模块的依赖
}
image-20230306180001653

可以看到,在模块module.b中,并没有进行依赖传递,说明哪个模块导入的依赖只能哪个模块用,但是现在我们希望依赖可以传递,就是哪个模块用了什么依赖,依赖此模块的模块也会自动进行依赖,我们可以通过一个关键字解决:

java 复制代码
module module.a {
    exports com.test to module.b;   //使用exports将com.test包下所有内容暴露出去,这样其他模块才能导入
    requires transitive java.logging;   //使用transitive来向其他模块传递此依赖
}

现在就可以使用了:

image-20230306180011462

还有我们前面演示的反射,我们发现如果我们依赖了一个模块,是没办法直接进行反射操作的:

java 复制代码
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    User user = new User("AAA", 18);
    Class<User> userClass = User.class;
    Field field = userClass.getDeclaredField("name");
    field.setAccessible(true);   //尝试修改访问权限
    System.out.println(field.get(user));
}
image-20230306180056716

那么怎么样才可以使用反射呢?我们可以为其他模块开放某些运行使用反射的类:

java 复制代码
open module module.a {   //直接添加open关键字开放整个模块的反射权限
    exports com.test to module.b;
}
java 复制代码
module module.a {
    exports com.test to module.b;
    opens com.test;   //通过使用opens关键字来为其他模块开放反射权限
  	//也可以指定目标开放反射 opens com.test to module.b;
}

我们还可以指定模块需要使用的抽象类或是接口实现:

java 复制代码
package com.test;

public interface Test {
}
java 复制代码
open module module.a {
    exports com.test to module.b;
    uses com.test.Test;  //使用uses指定,Test是一个接口(比如需要的服务等),模块需要使用到
}

我们可以在模块B中去实现一下,然后声明我们提供了实现类:

java 复制代码
package com.main;

import com.test.Test;

public class TestImpl implements Test {

}
java 复制代码
module module.b {
    requires module.a;   //导入项目A的模块,此模块暴露了com.test包
    provides com.test.Test with com.main.TestImpl;  //声明此模块提供了Test的实现类
}

了解了以上的相关知识后,我们就可以简单地进行模块的使用了。比如现在我们创建了一个新的Maven项目:

image-20230306180028905

然后我们导入了lombok框架的依赖,如果我们不创建module-info.java文件,那么就是一个未命名模块,未命名模块默认可以使用其他所有模块提供的类,实际上就是我们之前的开发模式:

java 复制代码
package com.test;

import lombok.extern.java.Log;

@Log
public class Main {
    public static void main(String[] args) {
        log.info("Hello World!");   //使用lombok提供的注解,可以正常运行
    }
}

现在我们希望按照全新的模块化开发模式来进行开发,将我们的项目从未命名模块改进为应用程序模块,所以我们先创建好module-info.java文件:

java 复制代码
module com.test {
}

可以看到,直接报错了:

image-20230306180116166

明明导入了lombok依赖,却无法使用,这是因为我们还需要去依赖对应的模块才行:

java 复制代码
module com.test {
    requires lombok;   //lombok模块
    requires java.logging;    //JUL日志模块,也需要使用到
}
image-20230306180127459

这样我们就可以正常使用了,之后为了教程演示方便,咱们还是不用模块。

JShell交互式编程

Java 9为我们通过了一种交互式编程工具JShell,你还别说,真有Python那味。

image-20230306180136996

环境配置完成后,我们只需要输入jshell命令即可开启交互式编程了,它支持我们一条一条命令进行操作。

比如我们来做一个简单的计算:

image-20230306180146794

我们一次输入一行(可以不加分号),先定义一个a=10和b=10,然后定义c并得到a+b的结果,可以看到还是非常方便的,但是注意语法还是和Java是一样的。

image-20230306180158288

我们也可以快速创建一个方法供后续的调用。当我们按下Tab键还可以进行自动补全:

image-20230306180220301

除了直接运行我们写进去的代码之外,它还支持使用命令,输入help来查看命令列表:

image-20230306180228542

比如我们可以使用/vars命令来展示当前定义的变量列表:

image-20230306180242109

当我们不想使用jshell时,直接输入/exit退出即可:

image-20230306180252071

接口中的private方法

在Java 8中,接口中 的方法支持添加default关键字来添加默认实现:

java 复制代码
public interface Test {
    default void test(){
        System.out.println("我是test方法默认实现");
    }
}

而在Java 9中,接口再次得到强化,现在接口中可以存在私有方法了:

java 复制代码
public interface Test {
    default void test(){
        System.out.println("我是test方法默认实现");
        this.inner();   //接口中方法的默认实现可以直接调用接口中的私有方法
    }
    
    private void inner(){   //声明一个私有方法
        System.out.println("我是接口中的私有方法!");
    }
}

注意私有方法必须要提供方法体,因为权限为私有的,也只有这里能进行方法的具体实现了,并且此方法只能被接口中的其他私有方法或是默认实现调用。

集合类新增工厂方法

在之前,如果我们想要快速创建一个Map只能:

java 复制代码
public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();   //要快速使用Map,需要先创建一个Map对象,然后再添加数据
    map.put("AAA", 19);
    map.put("BBB", 23);

    System.out.println(map);
}

而在Java 9之后,我们可以直接通过of方法来快速创建了:

java 复制代码
public static void main(String[] args) {
    Map<String, Integer> map = Map.of("AAA", 18, "BBB", 20);  //直接一句搞定

    System.out.println(map);
}

是不是感觉非常方便,of方法还被重载了很多次,分别适用于快速创建包含0~10对键值对的Map:

image-20230306180306844

但是注意,通过这种方式创建的Map和通过Arrays创建的List比较类似,也是无法进行修改的。

当然,除了Map之外,其他的集合类都有相应的of方法:

java 复制代码
public static void main(String[] args) {
    Set<String> set = Set.of("BBB", "CCC", "AAA");  //注意Set中元素顺序并不一定你的添加顺序
    List<String> list = List.of("AAA", "CCC", "BBB");   //好耶,再也不用Arrays了
}
大纲 (于 2025年1月1日 更新)
正在加载页面,请稍后...