Note/青空笔记/JavaSE 笔记(含新特性介绍)/Java新特性介绍.md
2023-08-31 11:30:31 +08:00

56 KiB
Raw Blame History

image-20220528201059618

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-20220528202952628

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

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

image-20220528220146662

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

Java 8 关键特性回顾

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

Lambda表达式

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

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表达式即可

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

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

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

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

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

image-20220529214948350

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

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

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

比如我们之前使用的Runable类

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

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

Runnable runnable = () -> {    };

我们也可以写一个玩玩:

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

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

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

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

Test test = (i) -> { return i+""; };

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

Test test = i -> { return i+""; };

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

Test test = i -> i+"";

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

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

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

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

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

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

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的静态方法:

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

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

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要求我们实现的方法为

public int compare(Integer o1, Integer o2) {
     return o1 - o2;
}

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

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

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

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方法,所以,直接缩写:

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

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

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

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

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

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

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

于是乎:

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

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

Optional类

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

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

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

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

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

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

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

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

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

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

System.out.println(Optional.ofNullable(str).get());

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

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

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

Java 9 新特性

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

模块机制

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

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

image-20220528210803658

而在引入模块机制之后:

image-20220528210958964

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

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

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

image-20220528213210752

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

image-20220528213428296

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

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

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

image-20220528214247006

image-20220528214308194

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

//Java17版本的String类
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    @Stable
    private final byte[] value;  //自JDK9后为了提高性能String底层数据存放的是byte[]而不是char[]
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-20220528221817482

反射 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需要用到

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-20220528230842617

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

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

image-20220528232033318

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

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进行包的暴露之外,我们也可以直接指定将包暴露给指定的模块:

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

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

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

image-20220529103614788

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

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

现在就可以使用了:

image-20220529103828560

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

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-20220529104451040

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

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

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

package com.test;

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

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

package com.main;

import com.test.Test;

public class TestImpl implements Test {

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

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

image-20220529112208486

然后我们导入了lombok框架的依赖如果我们不创建module-info.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文件:

module com.test {
}

可以看到,直接报错了:

image-20220529112707958

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

module com.test {
    requires lombok;   //lombok模块
    requires java.logging;    //JUL日志模块也需要使用到
}

image-20220529112909452

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

JShell交互式编程

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

image-20220529141547082

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

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

image-20220529141719363

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

image-20220529141954494

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

image-20220529142340030

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

image-20220529142440584

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

image-20220529142757286

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

image-20220529142852920

接口中的private方法

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

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

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

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

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

集合类新增工厂方法

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

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方法来快速创建了:

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-20220529144905646

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

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

public static void main(String[] args) {
    Set<String> set = Set.of("BBB", "CCC", "AAA");  //注意Set中元素顺序并不一定你的添加顺序
    List<String> list = List.of("AAA", "CCC", "BBB");   //好耶再也不用Arrays了
}

改进的 Stream API

还记得我们之前在JavaSE中学习的Stream流吗当然这里不是指进行IO操作的流而是JDK1.8新增的Stream API通过它大大方便了我们的编程。

public static void main(String[] args) {
    Stream
            .of("A", "B", "B", "C")   //这里我们可以直接将一些元素封装到Stream中
            .filter(s -> s.equals("B"))   //通过过滤器过滤
            .distinct()   //去重
            .forEach(System.out::println);   //最后打印
}

自从有了Stream我们对于集合的一些操作就大大地简化了对集合中元素的批量处理只需要在Stream中一气呵成具体的详细操作请回顾JavaSE篇

如此方便的框架在Java 9得到了进一步的增强

public static void main(String[] args) {
    Stream
            .of(null)   //如果传入null会报错
            .forEach(System.out::println);

    Stream
            .ofNullable(null) //使用新增的ofNullable方法这样就不会了不过这样的话流里面就没东西了
            .forEach(System.out::println);
}

还有我们可以通过迭代快速生成一组数据实际上Java 8就有了这里新增的是允许结束迭代的

public static void main(String[] args) {
    Stream
            .iterate(0, i -> i + 1)   //Java8只能像这样生成无限的流第一个参数是种子就是后面的UnaryOperator的参数i一开始的值最后会返回一个值作为i的新值每一轮都会执行UnaryOperator并生成一个新值到流中这个是源源不断的如果不加limit()进行限制的话,将无限生成下去。
      			.limit(20)   //这里限制生成20个
            .forEach(System.out::println); 
}
public static void main(String[] args) {
    Stream
            //不知道怎么写参考一下for (int i = 0;i < 20;i++)
            .iterate(0, i -> i < 20, i -> i + 1)  //快速生成一组0~19的int数据中间可以添加一个断言表示什么时候结束生成
            .forEach(System.out::println);
}

Stream还新增了对数据的截断操作比如我们希望在读取到某个元素时截断不再继续操作后面的元素

public static void main(String[] args) {
    Stream
            .iterate(0, i -> i + 1)
            .limit(20)
            .takeWhile(i -> i < 10)   //当i小于10时正常通过一旦大于等于10直接截断
            .forEach(System.out::println);
}
public static void main(String[] args) {
    Stream
            .iterate(0, i -> i + 1)
            .limit(20)
            .dropWhile(i -> i < 10)   //和上面相反,上来就是截断状态,只有当满足条件时再开始通过
            .forEach(System.out::println);
}

其他小型变动

Try-with-resource语法现在不需要再完整的声明一个变量了我们可以直接将现有的变量丢进去

public static void main(String[] args) throws IOException {
    InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));
    try (inputStream) {   //单独丢进try中效果是一样的
        for (int i = 0; i < 100; i++)
            System.out.print((char) inputStream.read());
    }
}

在Java 8中引入了Optional类它很好的解决了判空问题

public static void main(String[] args) throws IOException {
    test(null);
}

public static void test(String s){
    //比如现在我们想执行 System.out.println(str.toLowerCase())
    //但是由于我们不清楚给进来的str到底是不是null如果是null的话会引起空指针异常
    //但是去单独进行一次null判断写起来又不太简洁这时我们可以考虑使用Optional进行包装
    Optional
            .ofNullable(s)
            .ifPresent(str -> System.out.println(str.toLowerCase()));
}

这种写法就有点像Kotlin或是JS中的语法

fun main() {
    test(null)
}

fun test(str : String?){   //传入的String对象可能为null这里类型写为String?
    println(str?.lowercase())   // ?.表示只有不为空才进行调用
}

在Java 9新增了一些更加方便的操作

public static void main(String[] args) {
    String str = null;
    Optional.ofNullable(str).ifPresentOrElse(s -> {  //通过使用ifPresentOrElse我们同时处理两种情况
        System.out.println("被包装的元素为:"+s);     //第一种情况和ifPresent是一样的
    }, () -> {
        System.out.println("被包装的元素为null");   //第二种情况是如果为null的情况
    });
}

我们也可以使用or()方法快速替换为另一个Optional类

public static void main(String[] args) {
    String str = null;
    Optional.ofNullable(str)
      .or(() -> Optional.of("AAA"))   //如果当前被包装的类不是null依然返回自己但是如果是null那就返回Supplier提供的另一个Optional包装
      .ifPresent(System.out::println);
}

当然还支持直接转换为Stream这里就不多说了。

在Java 8及之前匿名内部类是没办法使用钻石运算符进行自动类型推断的

public abstract class Test<T>{   //这里我们写一个泛型类
    public T t;

    public Test(T t) {
        this.t = t;
    }

    public abstract T test();
}
public static void main(String[] args) throws IOException {
    Test<String> test = new Test<>("AAA") {   //在低版本这样写是会直接报错的因为匿名内部类不支持自动类型推断但是很明显我们这里给的参数是String类型的所以明明有机会进行类型推断却还是要我们自己填类型就很蠢
      //在Java 9之后这样的写法终于可以编译通过了
        @Override
        public String test() {
            return t;
        }
    };
}

当然除了以上的特性之外还有Java 9的多版本JAR包支持、CompletableFuture API的改进等因为不太常用这里就不做介绍了。

Java 10 新特性

Java 10主要带来的是一些内部更新相比Java 9带来的直观改变不是很多其中比较突出的就是局部变量类型推断了。

局部变量类型推断

在Java中我们可以使用自动类型推断

public static void main(String[] args) {
    // String a = "Hello World!";   之前我们定义变量必须指定类型
    var a = "Hello World!";   //现在我们使用var关键字来自动进行类型推断因为完全可以从后面的值来判断是什么类型
}

但是注意,var关键字必须位于有初始值设定的变量上,否则鬼知道你要用什么类型。

image-20220529171216795

我们来看看是不是类型也能正常获取:

public static void main(String[] args) {
    var a = "Hello World!";
    System.out.println(a.getClass());
}

这里虽然是有了var关键字进行自动类型推断但是最终还是会变成String类型得到的Class也是String类型。但是Java终究不像JS那样进行动态推断这种类型推断仅仅发生在编译期间到最后编译完成后还是会变成具体类型的

image-20220529170538383

并且var关键字仅适用于局部变量,我们是没办法在其他地方使用的,比如类的成员变量:

image-20220529171444062

有关Java 10新增的一些其他改进这里就不提了。

Java 11 新特性

Java 11 是继Java 8之后的又一个TLS长期维护版本在Java 17出现之前一直都是此版本作为广泛使用的版本其中比较关键的是用于Lambda的形参局部变量语法。

用于Lambda的形参局部变量语法

在Java 10我们认识了var关键字它能够直接让局部变量自动进行类型推断不过它不支持在lambda中使用

image-20220529235822891

但是实际上这里是完全可以进行类型推断的所以在Java 11终于是支持了这样编写就不会报错了

image-20220529235935071

针对于String类的方法增强

在Java 11为String新增一些更加方便的操作

public static void main(String[] args) {
    var str = "AB\nC\nD";
    System.out.println(str.isBlank());    //isBlank方法用于判断是否字符串为空或者是仅包含空格
    str
            .lines()   //根据字符串中的\n换行符进行切割分为多个字符串并转换为Stream进行操作
            .forEach(System.out::println);
}

我们还可以通过repeat()方法来让字符串重复拼接:

public static void main(String[] args) {
    String str = "ABCD";   //比如现在我们有一个ABCD但是现在我们想要一个ABCDABCD这样的基于原本字符串的重复字符串
    System.out.println(str.repeat(2));  //一个repeat就搞定了
}

我们也可以快速地进行空格去除操作:

public static void main(String[] args) {
    String str = " A B C D ";
    System.out.println(str.strip());   //去除首尾空格
    System.out.println(str.stripLeading());  //去除首部空格
    System.out.println(str.stripTrailing());   //去除尾部空格
}

全新的HttpClient使用

在Java 9的时候其实就已经引入了全新的Http Client API用于取代之前比较老旧的HttpURLConnection类新的API支持最新的HTTP2和WebSocket协议。

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();   //直接创建一个新的HttpClient
  	//现在我们只需要构造一个Http请求实体就可以让客户端帮助我们发送出去了实际上就跟浏览器访问类似
    HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://www.baidu.com")).build();
  	//现在我们就可以把请求发送出去了注意send方法后面还需要一个响应体处理器内置了很多这里我们选择ofString直接吧响应实体转换为String字符串
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  	//来看看响应实体是什么吧
    System.out.println(response.body());
}

利用全新的客户端,我们甚至可以轻松地做一个爬虫(仅供学习使用,别去做违法的事情,爬虫玩得好,牢饭吃到饱),比如现在我们想去批量下载某个网站的壁纸:

image-20220530112549225

网站地址:https://pic.netbian.com/4kmeinv/

我们随便点击一张壁纸发现网站的URL格式为

image-20220530112701156

并且不同的壁纸似乎都是这样:https://pic.netbian.com/tupian/数字.html好了差不多可以开始整活了

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();
    for (int i = 0; i < 10; i++) {  //先不要一次性获取太多先来10个
        HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://pic.netbian.com/tupian/"+(29327 + i)+".html")).build();  //这里我们按照规律,批量获取
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());  //这里打印一下看看网页
    }
}

可以看到,最后控制台成功获取到这些图片的网站页面了:

image-20220530113039571

接着我们需要来观察一下网站的HTML具体怎么写的把图片的地址提取出来

image-20220530113136156

好了,知道图片在哪里就好办了,直接字符串截取:

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();
    for (int i = 0; i < 10; i++) {
        ...
        String html = response.body();
        
        String prefix = "<a href=\"\" id=\"img\"><img src=\"";  //先找好我们要截取的前面一段,作为前缀去匹配位置
        String suffix = "\" data-pic=";   //再找好我们要截取的屁股后面紧接着的位置,作为后缀去匹配位置
      	//直接定位,然后前后截取,得到最终的图片地址
        html = html.substring(html.indexOf(prefix) + prefix.length());
        html = html.substring(0, html.indexOf(suffix));
        System.out.println(html);  //最终的图片地址就有了
    }
}

好了,现在图片地址也可以批量拿到了,直接获取这些图片然后保存到本地吧:

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();
    for (int i = 0; i < 10; i++) {
        ...
				//创建请求,把图片取到
        HttpRequest imageRequest = HttpRequest.newBuilder().uri(new URI("https://pic.netbian.com"+html)).build();
      	//这里以输入流的方式获取,不过貌似可以直接下载文件,各位小伙伴可以单独试试看
        HttpResponse<InputStream> imageResponse = client.send(imageRequest, HttpResponse.BodyHandlers.ofInputStream());
      	//拿到输入流和文件输出流
        InputStream imageInput = imageResponse.body();
        FileOutputStream stream = new FileOutputStream("images/"+i+".jpg"); //一会要保存的格式
        try (stream;imageInput){  //直接把要close的变量放进来就行简洁一些了
            int size;   //下面具体保存过程的不用我多说了吧
            byte[] data = new byte[1024];  
            while ((size = imageInput.read(data)) > 0) {  
                stream.write(data, 0, size);
            }
        }
    }
}

我们现在来看看效果吧,美女的图片已经成功保存到本地了:

image-20220530114824605

当然这仅仅是比较简单的爬虫不过我们的最终目的还是希望各位能够学会使用新的HttpClient API。

Java 12-16 新特性

由于Java版本的更新迭代速度自Java 9开始为半年更新一次Java 8到Java 9隔了整整三年所以各个版本之间的更新内容比较少剩余的6个版本我们就多个版本放在一起进行讲解了。

image-20220530120729757

Java12-16这五个版本并非长期支持版本所以很多特性都是一种处于实验性功能12/13版本引入了一些实验性功能并根据反馈进行调整最后在后续版本中正式开放使用其实就是体验服的那种感觉。

新的switch语法

在Java 12引入全新的switch语法让我们使用switch语句更加的灵活比如我们想要编写一个根据成绩得到等级的方法

/**
 * 传入分数(范围 0 - 100返回对应的等级
 *      100-90优秀
 *      70-80良好
 *      60-70及格
 *      0-60 * @param score 分数
 * @return 等级
 */
public static String grade(int score){
    
}

现在我们想要使用switch来实现这个功能不会吧不会吧不会有人要想半天怎么用switch实现吧之前的写法是

public static String grade(int score){
    score /= 10;  //既然分数段都是整数那就直接整除10
  	String res = null;
    switch (score) {
        case 10:
        case 9:
            res =  "优秀";   //不同的分数段就可以返回不同的等级了
        		break;   //别忘了break不然会贯穿到后面
        case 8:
        case 7:
            res = "良好";
        		break;
        case 6:
            res = "及格";
        		break;
        default:
            res = "不及格";
        		break;
    }
  	return res;
}

但是现在我们可以使用新的特性了:

public static String grade(int score){
    score /= 10;  //既然分数段都是整数那就直接整除10
    return switch (score) {   //增强版switch语法
        case 10, 9 -> "优秀";   //语法那是相当的简洁而且也不需要我们自己考虑break或是return来结束switch了有时候就容易忘记这样的话就算忘记也没事了
        case 8, 7 -> "良好"; 
        case 6 -> "及格";
        default -> "不及格";
    };
}

不过最后编译出来的样子,貌似还是和之前是一样的:

image-20220530222918174

这种全新的switch语法称为switch表达式,它的意义不仅仅体现在语法的精简上,我们来看看它的详细规则:

var res = switch (obj) {   //这里和之前的switch语句是一样的但是注意这样的switch是有返回值的所以可以被变量接收
    case [匹配值, ...] -> "优秀";   //case后直接添加匹配值匹配值可以存在多个需要使用逗号隔开使用 -> 来返回如果匹配此case语句的结果
    case ...   //根据不同的分支可以存在多个case
    default -> "不及格";   //注意表达式要求必须涵盖所有的可能所以是需要添加default的
};

那么如果我们并不是能够马上返回,而是需要做点什么其他的工作才能返回结果呢?

var res = switch (obj) {   //增强版switch语法
    case [匹配值, ...] -> "优秀";
    default -> {   //我们可以使用花括号来将整套逻辑括起来
        //... 我是其他要做的事情
        yield  "不及格";  //注意处理完成后需要返回最终结果但是这样并不是使用return而是yield关键字
    }
};

当然,也可以像这样:

var res = switch (args.length) {   //增强版switch语法
    case [匹配值, ...]:
        yield "AAA";   //传统的:写法通过yield指定返回结果同样不需要break
    default:
    		System.out.println("默认情况");
        yield "BBB";
};

这种全新的语法,可以说极大地方便了我们的编码,不仅代码简短,而且语义明确。唯一遗憾的是依然不支持区间匹配。

**注意:**switch表达式在Java 14才正式开放使用所以我们项目的代码级别需要调整到14以上。

文本块

如果你学习过Python一定知道三引号

#当我们需要使用复杂字符串时,可能字符串中包含了很多需要转义的字符,比如双引号等,这时我们就可以使用三引号来囊括字符串
multi_line =  """
                nice to meet you!
                  nice to meet you!
                      nice to meet you!
                """
print multi_line

没错Java13也带了这样的特性旨在方便我们编写复杂字符串这样就不用再去用那么多的转义字符了

image-20220530230225037

可以看到Java中也可以使用这样的三引号来表示字符串了并且我们可以随意在里面使用特殊字符包括双引号等但是最后编译出来的结果实际上还是会变成一个之前这样使用了转义字符的字符串

image-20220530230343933

仔细想想这样我们写SQL或是HTML岂不是就舒服多了

**注意:**文本块表达式在Java 15才正式开放使用所以我们项目的代码级别需要调整到15以上。

新的instanceof语法

在Java 14instanceof迎来了一波小更新哈哈这版本instanceof又加强了版本强势语法

比如我们之前要重写一个类的equals方法

public class Student {
    private final String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Student) {   //首先判断是否为Student类型
            Student student = (Student) obj;  //如果是,那么就类型转换
            return student.name.equals(this.name);  //最后比对属性是否一样
        }
        return false;
    }
}

在之前我们一直都是采用这种先判断类型然后类型转换最后才能使用的方式但是这个版本instanceof加强之后我们就不需要了我们可以直接将student替换为模式变量

image-20220530232252253

public class Student {
    private final String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Student student) {   //在比较完成的屁股后面,直接写变量名字,而这个变量就是类型转换之后的
            return student.name.equals(this.name);  //下面直接用,是不是贼方便
        }
        return false;
    }
}

在使用instanceof判断类型成立后,会自动强制转换类型为指定类型,简化了我们手动转换的步骤。

**注意:**新的instanceof语法在Java 16才正式开放使用所以我们项目的代码级别需要调整到16以上。

空指针异常的改进

相信各位小伙伴在调试代码时,经常遇到空指针异常,比如下面的这个例子:

public static void test(String a, String b){
    int length = a.length() + b.length();   //可能给进来的a或是b为null
    System.out.println(length);
}

那么为空时,就会直接:

image-20220530232755797

但是由于我们这里a和b都调用了length()方法虽然空指针异常告诉我们问题出现在这一行但是到底是a为null还是b为null呢我们是没办法直接得到的遇到过这种问题的扣个1吧只能调试就很头疼

但是当我们在Java 14或更高版本运行时

image-20220530233031005

这里会明确指出是哪一个变量调用出现了空指针,是不是感觉特别人性化。

记录类型

继类、接口、枚举、注解之后的又一新类型来了,它的名字叫"记录"在Java 14中首次出场这一出场Lombok的噩梦来了。

在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据,比如我们从数据库中查询的账户信息,最后会被映射为一个实体类:

@Data
public class Account {   //使用Lombok一个注解就搞定了
    String username;
    String password;
}

Lombok可以说是简化代码的神器了他能在编译时自动生成getter和setter、构造方法、toString()方法等实现,在编写这些实体类时,简直不要太好用,而这一波,官方也是看不下去了,于是自己也搞了一个记录类型。

记录类型本质上也是一个普通的类不过是final类型且继承自java.lang.Record抽象类的它会在编译时会自动编译出 public get hashcodeequalstoString 等方法好家伙这是要逼死Lombok啊。

public record Account(String username, String password) {  //直接把字段写在括号中

}

使用起来也是非常方便自动生成了构造方法和成员字段的公共get方法

image-20220530235609885

并且toString也是被重写了的

image-20220530235719341

equals()方法仅做成员字段之间的值比较,也是帮助我们实现好了的:

Account account0 = new Account("Admin", "123456");
Account account1 = new Account("Admin", "123456");   //两个属性都是一模一样的
System.out.println(account0.equals(account1));  //得到true

是不是感觉这种类型就是专门为这种实体类而生的。

public record Account(String username, String password) implements Runnable {  //支持实现接口,但是不支持继承,因为继承的坑位已经默认被占了

    @Override
    public void run() {
        
    }
}

**注意:**记录类型在Java 16才正式开放使用所以我们项目的代码级别需要调整到16以上。

Java 17 新特性

Java 17作为新的LTS长期维护版本我们来看看都更新了什么不包含预览特性包括switch第二次增强哈哈果然还是强度不够都连续加强两个版本了

密封类型

密封类型可以说是Java 17正式推出的又一重磅类型它在Java 15首次提出并测试了两个版本。

在Java中我们可以通过继承extends关键字来实现类的能力复用、扩展与增强。但有的时候可能并不是所有的类我们都希望能够被继承。所以我们需要对继承关系有一些限制的控制手段而密封类的作用就是限制类的继承

实际上在之前我们如果不希望别人继承我们的类,可以直接添加final关键字:

public final class A{   //添加final关键字后不允许对此类继承
    
}

这样有一个缺点,如果添加了final关键字那么无论是谁包括我们自己也是没办法实现继承的但是现在我们有一个需求只允许我们自己写的类继承A但是不允许别人写的类继承A这时该咋写在Java 17之前想要实现就很麻烦。

但是现在我们可以使用密封类型来实现这个功能:

public sealed class A permits B{   //在class关键字前添加sealed关键字表示此类为密封类型permits后面跟上允许继承的类型多个子类使用逗号隔开

}

密封类型有以下要求:

  • 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
  • 必须有子类继承且不能是匿名内部类或是lambda的形式。
  • sealed写在原来final的位置,但是不能和finalnon-sealed关键字同时出现,只能选择其一。
  • 继承的子类必须显式标记为finalsealed或是non-sealed类型。

标准的声明格式如下:

public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{
		//里面的该咋写咋写
}

注意子类格式为:

public [final/sealed/non-sealed] class 子类 extends 父类 {   //必须继承自父类
			//final类型任何类不能再继承当前类到此为止已经封死了。
  		//sealed类型同父类需要指定由哪些类继承。
  		//non-sealed类型重新开放为普通类任何类都可以继承。
}

比如现在我们写了这些类:

public sealed class A  permits B{   //指定B继承A

}
public final class B extends A {   //在子类final彻底封死

}

我们可以看到其他的类无论是继承A还是继承B都无法通过编译

image-20220531090136485

image-20220531090152743

但是如果此时我们主动将B设定为non-sealed类型:

public non-sealed class B extends A {

}

这样就可以正常继承了因为B指定了non-sealed主动放弃了密封特性,这样就显得非常灵活了。

当然我们也可以通过反射来获取类是否为密封类型:

public static void main(String[] args) {
    Class<A> a = A.class;
    System.out.println(a.isSealed());   //是否为密封
}

至此Java 9 - 17的主要新特性就讲解完毕了。