本文最后更新于:1 年前
引言 Java 8 是 Oracle 公司于 2014 年 3 月 18 日发布的,距离今天已经过了近十年的时间了,Java 并没有就此止步,而是继续不断发展壮大,几乎每隔 6 个月,就会冒出一个新版本,最新的版本已经快要迭代到 Java 22 了,与 Java 8 相差了足足十来个版本,但是由于 Java 8 的稳定和生态完善,依然有很多公司在坚持使用 Java 8,不过随着 SpringBoot 3.0 的到来,现在强制要求使用 Java 17 版本(同样也是 LTS 长期维护版本),下一个 Java 版本的时代,或许已经临近了。
随着这些主流框架全面拥抱 Java 17,为了不被时代所淘汰,我们的学习之路,也要继续前行了。就像很多年前 Java 6 还是主流的时代,终究还是被 Java 8 所取代一样。
Java 8 关键特性回顾 在开始之前,我们先来回顾一下 Java 8 中学习的 Lambda 表达式和 Optional 类,有关 Stream API 请各位小伙伴回顾一下 Java SE 篇视频教程,这里不再进行介绍。
Lambda 表达式 在 Java 8 之前,我们在某些情况下可能需要用到匿名内部类,比如:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { Thread thread = new Thread (new Runnable () { @Override public void run () { System.out.println("Hello World!" ); } }); thread.start(); }
在创建 Thread 时,我们需要传入一个 Runnable 接口的实现类,来指定具体的在新的线程中要执行的任务,相关的逻辑需要我们在 run()
方法中实现,这时为了方便,我们就直接使用匿名内部类的方式传入一个实现,但是这样的写法实在是太过臃肿了。
在 Java 8之后,我们可以对类似于这种匿名内部类的写法,进行缩减,实际上我们进行观察会发现,真正有用的那一部分代码,实际上就是我们对 run()
方法的具体实现,而其他的部分实际上在任何地方编写都是一模一样的,那么我们能否针对于这种情况进行优化呢?我们现在只需要一个简短的 lambda 表达式即可:
1 2 3 4 5 6 7 public static void main (String[] args) { Thread thread = new Thread (() -> { System.out.println("Hello World!" ); }); thread.start(); }
我们可以发现,原本需要完整编写包括类、方法在内的所有内容,全部不再需要,而是直接使用类似于 () ‐> { 代码语句 }
的形式进行替换即可。是不是感觉瞬间代码清爽了 N 倍?
当然这只是一种写法而已,如果各位不好理解,可以将其视为之前匿名内部类写法的一种缩短。
但是注意,它的底层其实并不只是简简单单的语法糖替换,而是通过 invokedynamic
指令实现的,不难发现,匿名内部类会在编译时创建一个单独的 class 文件,但是 lambda 却不会,间接说明编译之后 lambda 并不是以匿名内部类的形式存在的:
1 2 3 4 5 Thread thread = new Thread (() -> { throw new UnsupportedOperationException (); }); thread.start();
可以看到,实际上是 Main 类中的 lambda$main$0()
方法抛出的异常,但是我们的 Main 类中压根没有这个方法,很明显是自动生成的。所以,与其说 Lambda 是匿名内部类的语法糖,不如说是我们为所需要的接口提供了一个方法作为它的实现。比如 Runnable 接口需要一个方法体对它的 run()
方法进行实现,而这里我们就通过 lambda 的形式给了它一个方法体,这样就万事具备了,而之后创建实现类就只需要交给 JVM 去处理就好了。
我们来看一下 Lambda 表达式的具体规范:
标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
和匿名内部类不同,Lambda 仅支持接口,不支持抽象类
接口内部必须有且仅有一个抽象方法 (可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)
比如我们之前使用的 Runable 类:
1 2 3 4 @FunctionalInterface public interface Runnable { public abstract void run () ; }
因此,Runable 的的匿名内部类实现,就可以简写为:
1 Runnable runnable = () -> { };
我们也可以写一个玩玩:
1 2 3 4 @FunctionalInterface public interface Test { String test (Integer i) ; }
它的 Lambda 表达式的实现就可以写为:
1 Test test = (Integer i) -> { return i+"" ; };
不过还可以进行优化,首先方法参数类型是可以省略的:
1 Test test = (i) -> { return i+"" ; };
由于只有一个参数,可以不用添加小括号(多个参数时需要):
1 Test test = i -> { return i+"" ; };
由于仅有返回语句这一行,所以可以直接写最终返回的结果,并且无需花括号:
这样,相比我们之前直接去编写一个匿名内部类,是不是简介了很多很多。当然,除了我们手动编写接口中抽象方法的方法体之外,如果已经有实现好的方法,是可以直接拿过来用的,比如:
1 2 3 public static String impl (Integer i) { return "我是已经存在的实现" +i; }
所以,我们可以直接将此方法,作为 lambda 表达式的方法体实现(其实这就是一种方法引用,引用了一个方法过来,这也是为什么前面说 是我们为所需要的接口提供了一个方法作为它的实现
,是不是越来越体会到这句话的精髓了):
1 2 3 4 5 6 7 public static void main (String[] args) { Test test = Main::impl; }public static String impl (Integer i) { return "我是已经存在的实现" +i; }
比如我们现在需要对一个数组进行排序:
1 2 3 4 5 6 7 8 9 10 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>() { @Override public int compare (Integer o1, Integer o2) { return o1 - o2; } }); System.out.println(Arrays.toString(array)); }
但是我们发现,Integer 类中有一个叫做 compare
的静态方法:
1 2 3 public static int compare (int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1 ); }
这个方法是一个静态方法,但是它却和 Comparator
需要实现的方法返回值和参数定义一模一样,所以,懂的都懂:
1 2 3 4 5 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 要求我们实现的方法为:
1 2 3 public int compare (Integer o1, Integer o2) { return o1 - o2; }
其中 o1和 o2都是 Integer 类型的,我们发现 Integer 类中有一个 compareTo
方法:
1 2 3 public int compareTo (Integer anotherInteger) { return compare(this .value, anotherInteger.value); }
只不过这个方法并不是静态的,而是对象所有:
1 2 3 4 5 6 7 8 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
方法,所以,直接缩写:
1 2 3 4 5 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)); }
成员方法也可以让对象本身不成为参与的那一方,仅仅引用方法:
1 2 3 4 5 6 7 8 9 10 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); System.out.println(Arrays.toString(array)); }public int reserve (Integer a, Integer b) { return b.compareTo(a); }
当然,类的构造方法同样可以作为方法引用传递:
1 2 3 public interface Test { String test (String str) ; }
我们发现,String 类中刚好有一个:
1 2 3 4 5 public String (String original) { this .value = original.value; this .coder = original.coder; this .hash = original.hash; }
于是乎:
1 2 3 public static void main (String[] args) { Test test = String::new ; }
当然除了上面提到的这些情况可以使用方法引用之外,还有很多地方都可以。Java 8 也为我们提供了一些内置的函数式接口供我们使用:Consumer、Function、Supplier 等。
Optional 类 Java 8 中新引入了 Optional 特性,来让我们更优雅的处理空指针异常 。我们先来看看下面这个例子:
1 2 3 public static void hello (String str) { System.out.println(str.toLowerCase()); }
但是这样实现的话,我们少考虑了一个问题,万一给进来的 str
是 null
呢?如果是 null
的话,在调用 toLowerCase
方法时岂不是直接空指针异常了?所以我们还得判空一下:
1 2 3 4 5 public static void hello (String str) { if (str != null ) { System.out.println(str.toLowerCase()); } }
但是这样写着就不能一气呵成了,我现在又有强迫症,我就想一行解决,这时,Optional 来了,我们可以将任何的变量包装进 Optional 类中使用:
1 2 3 4 5 6 7 public static void hello (String str) { Optional .ofNullable(str) .ifPresent(s -> { System.out.println(s); }); }
由于这里只有一句打印,所以我们来优化一下:
1 2 3 4 5 6 public static void hello (String str) { Optional .ofNullable(str) .ifPresent(System.out::println); }
这样,我们就又可以一气呵成了,是不是感觉比之前的写法更优雅。
除了在不为空时执行的操作外,还可以直接从 Optional 中获取被包装的对象:
1 System.out.println(Optional.ofNullable(str).get());
不过此时当被包装的对象为 null 时会直接抛出异常,当然,我们还可以指定如果 get 的对象为 null 的替代方案:
1 System.out.println(Optional.ofNullable(str).orElse("VVV" ));
Java 9 新特性 这一部分,我们将介绍 Java 9 为我们带来的新特性,Java 9 的主要特性有,全新的模块机制、接口的 private 方法等。
模块机制 在我们之前的开发中,不知道各位有没有发现一个问题,就是当我们导入一个 jar
包作为依赖时(包括 JDK 官方库),实际上很多功能我们并不会用到,但是由于它们是属于同一个依赖捆绑在一起,这样就会导致我们可能只用到一部分内容,但是需要引用一个完整的类库,实际上我们可以把用不到的类库排除掉,大大降低依赖库的规模。
于是,Java 9 引入了模块机制来对这种情况进行优化,在之前的我们的项目是这样的:
而在引入模块机制之后:
可以看到,模块可以由一个或者多个在一起的 Java 包组成,通过将这些包分出不同的模块,我们就可以按照模块的方式进行管理了。这里我们创建一个新的项目,并在 src
目录下,新建 module-info.java
文件表示此项目采用模块管理机制:
1 2 3 module NewHelloWorld { }
接着我们来创建一个主类:
程序可以正常运行,貌似和之前没啥区别,不过我们发现,JDK 为我们提供的某些框架不见了:
Java 为我们提供的 logging
相关日志库呢?我们发现现在居然不见了?实际上它就是被作为一个模块单独存在,这里我们需进行模块导入:
1 2 3 4 module NewHelloWorld { requires java.logging; }
这里我们导入 java.logging 相关模块后,就可以正常使用 Logger 了:
是不是瞬间感觉编写代码时清爽了许多,全新的模块化机制提供了另一个级别的 Java 代码可见性、可访问性的控制,不过,你以为仅仅是做了包的分离吗?我们可以来尝试通过反射获取 JDK 提供的类中的字段:
1 2 3 4 5 6 public final class String implements java .io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc { @Stable private final byte [] value;
1 2 3 4 5 6 public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException { Class<String> stringClass = String.class; Field field = stringClass.getDeclaredField("value" ); field.setAccessible(true ); System.out.println(field.get("ABCD" )); }
但是我们发现,在程序运行之后,修改操作被阻止了:
反射 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 需要用到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 的模块设置:
这里我们将 com.test
包下所有内容都暴露出去,默认情况下所有的包都是私有的,就算其他项目将此项目作为依赖也无法使用 。
接着我们现在想要在项目 B 中使用项目 A 的 User 类,我们需要进行导入:
[!WARNING] requires 导入模块时添加 static 关键字表示只在编译时需要,运行时可以不需要。
现在我们就可以在 Main 类中使用模块 module.a
中暴露出来的包内容了:
1 2 3 4 5 6 7 8 import com.test.User; public class Main { public static void main (String[] args) { User user = new User ("lbw" , 18 ); System.out.println(user); } }
当然除了普通的 exports
进行包的全局暴露之外,我们也可以直接指定将包暴露给指定的模块 :
1 2 3 module module .a { exports com.test to module .b; }
不过现在还有一个问题,如果模块 module.a
依赖于其他模块,那么会不会传递给依赖于模块 module.a
的模块呢?
1 2 3 4 module module .a { exports com.test to module .b; requires java.logging; }
可以看到,在模块 module.b
中,并没有进行依赖传递,说明哪个模块导入的依赖只能哪个模块用,但是现在我们希望依赖可以传递,就是哪个模块用了什么依赖,依赖此模块的模块也会自动进行依赖,我们可以通过一个关键字 transitive 进行模块级依赖传递 解决:
1 2 3 4 module module .a { exports com.test to module .b; requires transitive java.logging; }
现在就可以使用了:
还有我们前面演示的反射,我们发现如果我们依赖了一个模块,是没办法直接进行反射操作的:
1 2 3 4 5 6 7 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)); }
我们可以通过添加 open 或者 opens 关键字为其他模块开放某些运行使用反射的类:
1 2 3 open module module .a { exports com.test to module .b; }
1 2 3 4 5 module module .a { exports com.test to module .b; opens com.test to module .b; }
我们还可以指定模块需要使用的抽象类或是接口实现 :
1 2 3 4 package com.test;public interface Test { }
1 2 3 4 open module module .a { exports com.test to module .b; uses com.test.Test; }
我们可以在模块 B 中去实现一下,然后声明我们提供了实现类:
1 2 3 4 5 6 7 package com.main;import com.test.Test;public class TestImpl implements Test { }
1 2 3 4 module module .b { requires module .a; provides com.test.Test with com.main.TestImpl; }
了解了以上的相关知识后,我们就可以简单地进行模块的使用了。比如现在我们创建了一个新的 Maven 项目:
然后我们导入了 lombok 框架的依赖,如果我们不创建 module-info.java
文件,那么就是一个未命名模块,未命名模块默认可以使用其他所有模块提供的类,实际上就是我们之前的开发模式:
1 2 3 4 5 6 7 8 9 10 package com.test;import lombok.extern.java.Log;@Log public class Main { public static void main (String[] args) { log.info("Hello World!" ); } }
现在我们希望按照全新的模块化开发模式来进行开发,将我们的项目从未命名模块改进为应用程序模块,所以我们先创建好 module-info.java
文件:
可以看到,直接报错了:
明明导入了 lombok 依赖,却无法使用,这是因为我们还需要去依赖对应的模块才行:
1 2 3 4 module com.test { requires lombok; requires java.logging; }
这样我们就可以正常使用了,之后为了教程演示方便,咱们还是不用模块。
JShell 交互式编程 Java 9 为我们通过了一种交互式编程工具 JShell,你还别说,真有 Python 那味。
环境配置完成后,我们只需要输入 jshell
命令即可开启交互式编程了,它支持我们一条一条命令进行操作。
比如我们来做一个简单的计算:
我们一次输入一行(可以不加分号),先定义一个 a=10 和 b=10,然后定义 c 并得到 a+b 的结果,可以看到还是非常方便的,但是注意语法还是和 Java 是一样的。
我们也可以快速创建一个方法供后续的调用。当我们按下 Tab 键还可以进行自动补全:
除了直接运行我们写进去的代码之外,它还支持使用命令,输入 help
来查看命令列表:
比如我们可以使用 /vars
命令来展示当前定义的变量列表:
当我们不想使用 jshell 时,直接输入 /exit
退出即可:
接口中的 private 方法 在 Java 8 中,接口中 的方法支持添加 default
关键字来添加默认实现:
1 2 3 4 5 public interface Test { default void test () { System.out.println("我是 test 方法默认实现" ); } }
而在 Java 9 中,接口再次得到强化,现在接口中可以存在私有方法 了:
1 2 3 4 5 6 7 8 9 10 public interface Test { default void test () { System.out.println("我是 test 方法默认实现" ); this .inner(); } private void inner () { System.out.println("我是接口中的私有方法!" ); } }
私有方法必须要提供方法体,因为权限为私有的,也只有这里能进行方法的具体实现了,并且此方法只能被接口中的其他私有方法或是默认实现调用。
集合类新增工厂方法 在之前,如果我们想要快速创建一个 Map 只能:
1 2 3 4 5 6 7 public static void main (String[] args) { Map<String, Integer> map = new HashMap <>(); map.put("AAA" , 19 ); map.put("BBB" , 23 ); System.out.println(map); }
而在 Java 9 之后,我们可以直接通过 of
方法来快速创建了:
1 2 3 4 5 public static void main (String[] args) { Map<String, Integer> map = Map.of("AAA" , 18 , "BBB" , 20 ); System.out.println(map); }
非常方便 of 方法还被重载了很多次,分别适用于快速创建包含 0~10 对键值对的 Map:
但是注意,通过这种方式创建的 Map 和通过 Arrays 创建的 List 比较类似,也是无法进行修改的。
当然,除了 Map 之外,其他的集合类都有相应的 of
方法:
1 2 3 4 public static void main (String[] args) { Set<String> set = Set.of("BBB" , "CCC" , "AAA" ); List<String> list = List.of("AAA" , "CCC" , "BBB" ); }
改进的 Stream API 还记得我们之前在 JavaSE 中学习的 Stream 流吗?当然这里不是指进行 IO 操作的流,而是 JDK1.8 新增的 Stream API,通过它大大方便了我们的编程。
1 2 3 4 5 6 7 public static void main (String[] args) { Stream .of("A" , "B" , "B" , "C" ) .filter(s -> s.equals("B" )) .distinct() .forEach(System.out::println); }
自从有了 Stream,我们对于集合的一些操作就大大地简化了,对集合中元素的批量处理,只需要在 Stream 中一气呵成(具体的详细操作请回顾 JavaSE 篇)
如此方便的框架,在 Java 9 得到了进一步的增强:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { Stream .of(null ) .forEach(System.out::println); Stream .ofNullable(null ) .forEach(System.out::println); }
还有,我们可以通过迭代快速生成一组数据 (实际上 Java 8 就有了,这里新增的是允许结束迭代的 ):
1 2 3 4 5 6 public static void main (String[] args) { Stream .iterate(0 , i -> i + 1 ) .limit(20 ) .forEach(System.out::println); }
1 2 3 4 5 6 public static void main (String[] args) { Stream .iterate(0 , i -> i < 20 , i -> i + 1 ) .forEach(System.out::println); }
Stream 还新增了对数据的截断操作 ,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
1 2 3 4 5 6 7 public static void main (String[] args) { Stream .iterate(0 , i -> i + 1 ) .limit(20 ) .takeWhile(i -> i < 10 ) .forEach(System.out::println); }
1 2 3 4 5 6 7 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 语法现在不需要再完整的声明一个变量了,我们可以直接将现有的变量丢进去:
1 2 3 4 5 6 7 public static void main (String[] args) throws IOException { InputStream inputStream = Files.newInputStream(Paths.get("pom.xml" )); try (inputStream) { for (int i = 0 ; i < 100 ; i++) System.out.print((char ) inputStream.read()); } }
在 Java 8 中引入了 Optional 类,它很好的解决了判空问题:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws IOException { test(null ); }public static void test (String s) { Optional .ofNullable(s) .ifPresent(str -> System.out.println(str.toLowerCase())); }
这种写法就有点像 Kotlin 或是 JS 中的语法:
1 2 3 4 5 6 7 fun main () { test(null ) }fun test (str : String ?) { println(str?.lowercase()) }
在 Java 9 新增了一些更加方便的操作:
1 2 3 4 5 6 7 8 public static void main (String[] args) { String str = null ; Optional.ofNullable(str).ifPresentOrElse(s -> { System.out.println("被包装的元素为:" +s); }, () -> { System.out.println("被包装的元素为 null" ); }); }
我们也可以使用 or()
方法快速替换为另一个 Optional 类:
1 2 3 4 5 6 public static void main (String[] args) { String str = null ; Optional.ofNullable(str) .or(() -> Optional.of("AAA" )) .ifPresent(System.out::println); }
当然还支持直接转换为 Stream,这里就不多说了。
在 Java 8 及之前,匿名内部类是没办法使用钻石运算符进行自动类型推断 的:
1 2 3 4 5 6 7 8 9 public abstract class Test <T>{ public T t; public Test (T t) { this .t = t; } public abstract T test () ; }
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws IOException { Test<String> test = new Test <>("AAA" ) { @Override public String test () { return t; } }; }
当然除了以上的特性之外还有 Java 9 的多版本 JAR 包支持、CompletableFuture API 的改进等,因为不太常用,这里就不做介绍了。
Java 10 新特性 Java 10 主要带来的是一些内部更新,相比 Java 9 带来的直观改变不是很多,其中比较突出的就是局部变量类型推断了。
局部变量类型推断 在 Java 中,我们可以使用自动类型推断:
1 2 3 4 public static void main (String[] args) { var a = "Hello World!" ; }
但是注意,var
关键字必须位于有初始值设定的变量上,否则鬼知道你要用什么类型。
我们来看看是不是类型也能正常获取:
1 2 3 4 public static void main (String[] args) { var a = "Hello World!" ; System.out.println(a.getClass()); }
这里虽然是有了 var 关键字进行自动类型推断,但是最终还是会变成 String 类型,得到的 Class 也是 String 类型。但是 Java 终究不像 JS 那样进行动态推断,这种类型推断仅仅发生在编译期间,到最后编译完成后还是会变成具体类型的:
并且 var
关键字仅适用于局部变量,我们是没办法在其他地方使用的,比如类的成员变量:
Java 11 新特性 Java 11 是继 Java 8 之后的又一个 TLS 长期维护版本,在 Java 17 出现之前,一直都是此版本作为广泛使用的版本,其中比较关键的是用于 Lambda 的形参局部变量语法。
用于 Lambda 的形参局部变量语法 在 Java 10 我们认识了 var
关键字,它能够直接让局部变量自动进行类型推断,不过它不支持在 lambda 中使用:
但是实际上这里是完全可以进行类型推断的,所以在 Java 11,终于是支持了,这样编写就不会报错了:
针对于 String 类的方法增强 在 Java 11 为 String 新增一些更加方便的操作:
1 2 3 4 5 6 7 public static void main (String[] args) { var str = "AB\nC\nD" ; System.out.println(str.isBlank()); str .lines() .forEach(System.out::println); }
我们还可以通过 repeat()
方法来让字符串重复拼接:
1 2 3 4 public static void main (String[] args) { String str = "ABCD" ; System.out.println(str.repeat(2 )); }
我们也可以快速地进行空格去除 操作:
1 2 3 4 5 6 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()); }
根据换行符 \n
进行切割字符串:
1 2 3 String str = "A\nB\nCD" ; str.lines() .forEach(System.out::println);
全新的 HttpClient 使用 在 Java 9 的时候其实就已经引入了全新的 Http Client API,用于取代之前比较老旧的 HttpURLConnection 类,新的 API 支持最新的 HTTP2 和 WebSocket 协议。
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws URISyntaxException, IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(new URI (" https://www.baidu.com" )).build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }
利用全新的客户端,我们甚至可以轻松地做一个爬虫,比如现在我们想去批量下载某个网站的壁纸:
网站地址: https://pic.netbian.top/
我们随便点击一张壁纸,发现网站的 URL 格式为:
并且不同的壁纸似乎都是这样: https://pic.netbian.top/tupian/数字.html ,好了差不多可以开始整活了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) throws URISyntaxException, IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); int begin = 0 ; int end = 10 ; Random random = new Random (); for (int i = begin; i < end; i++) { int id = random.nextInt(10000 , 99999 ); var url = "https://pic.netbian.top/tupian/" + id + ".html" ; var fileName = "img-" + id + ".jpg" ; List<String> imgs = crawler(client, url); Optional.ofNullable(imgs).ifPresent(s -> downloadImage(s, client, fileName)); System.out.print("已下载序号为:" + id + " 的图片\r" ); } }
可以看到,最后控制台成功获取到这些图片的网站页面了:
接着我们需要来观察一下网站的 HTML 具体怎么写的,把图片的地址提取出来:
好了,知道图片在哪里就好办了,直接通过正则表达式截取:
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 31 32 33 34 35 36 37 private static List<String> crawler (HttpClient client, String url) throws URISyntaxException, IOException, InterruptedException { if (url.contains("?" )) { url = url.substring(0 , url.indexOf("?" )); } HttpRequest request = HttpRequest.newBuilder().uri(new URI (url)).header("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" ).build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 403 ) { String text = response.body(); String pattern = "btwaf=\\d+" ; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(text); if (m.find()) { var newUrl = url + "?" .concat(m.group()); System.out.println(newUrl); crawler(client, newUrl); } } else { String text = response.body(); String pattern = "<img src=\"https([^\"]+)\"" ; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(text); if (m.find()) { var imgurl = m.group(); if (imgurl.contains("https" )) { var imgUrl = imgurl.substring(imgurl.indexOf("https" ), imgurl.length() - 1 ) + ".jpg" ; return List.of(imgUrl); } } } return null ; }
好了,现在图片地址也可以批量拿到了,直接获取这些图片然后保存到本地吧:
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 private static void downloadImage (List<String> imgs, HttpClient client, String fileName) { imgs.forEach(img -> { Optional.ofNullable(img).ifPresent(s -> { try { HttpRequest imageRequest = HttpRequest.newBuilder().uri(new URI (s)).build(); HttpResponse<InputStream> imageResponse = client.send(imageRequest, HttpResponse.BodyHandlers.ofInputStream()); InputStream imageInput = imageResponse.body(); FileOutputStream stream = new FileOutputStream ("E:\\IDEAProjects\\javaNewProps\\module-b\\src\\main\\resources\\images\\" + fileName); try (stream; imageInput) { int size; byte [] data = new byte [1024 ]; while ((size = imageInput.read(data)) > 0 ) { stream.write(data, 0 , size); } } } catch (Exception e) { throw new RuntimeException (e); } }); }); }
我们现在来看看效果吧,美女的图片已经成功保存到本地了:
当然,比较简单的爬虫到此为止了。
Java 12-16 新特性 由于 Java 版本的更新迭代速度自 Java 9 开始为半年更新一次(Java 8 到 Java 9 隔了整整三年),所以各个版本之间的更新内容比较少,剩余的 6 个版本,我们就多个版本放在一起进行讲解了。
Java12-16 这五个版本并非长期支持版本,所以很多特性都是一种处于实验性功能,12/13 版本引入了一些实验性功能,并根据反馈进行调整,最后在后续版本中正式开放使用,其实就是体验服的那种感觉。
增强型 switch 语法 在 Java 12 引入全新的 switch 语法,让我们使用 switch 语句更加的灵活,比如我们想要编写一个根据成绩得到等级的方法:
1 2 3 4 5 6 7 8 9 10 11 12 public static String grade (int score) { }
现在我们使用 switch 来实现这个功能,之前的写法是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static String grade (int score) { score /= 10 ; String res = null ; switch (score) { case 10 : case 9 : res = "优秀" ; break ; case 8 : case 7 : res = "良好" ; break ; case 6 : res = "及格" ; break ; default : res = "不及格" ; break ; } return res; }
但是现在我们可以使用增强型 Switch 语法 了:
1 2 3 4 5 6 7 8 9 public static String grade (int score) { score /= 10 ; return switch (score) { case 10 , 9 -> "优秀" ; case 8 , 7 -> "良好" ; case 6 -> "及格" ; default -> "不及格" ; }; }
不过最后编译出来的样子,貌似还是和之前是一样的:
这种全新的 switch 语法称为 switch 表达式
,它的意义不仅仅体现在语法的精简上,我们来看看它的详细规则:
1 2 3 4 5 var res = switch (obj) { case [匹配值, ...] -> "优秀" ; case ... default -> "不及格" ; };
那么如果我们并不是能够马上返回,而是需要做点什么其他的工作才能返回结果呢?
1 2 3 4 5 6 7 var res = switch (obj) { case [匹配值, ...] -> "优秀" ; default -> { yield "不及格" ; } };
当然,也可以像这样:
1 2 3 4 5 6 7 var res = switch (args.length) { case [匹配值, ...]: yield "AAA" ; default : System.out.println("默认情况" ); yield "BBB" ; };
这种全新的语法,可以说极大地方便了我们的编码,不仅代码简短,而且语义明确。唯一遗憾的是依然不支持区间匹配。
switch 表达式在 Java 14
才正式开放使用,所以我们项目的代码级别需要调整到 14 以上。
文本块 如果你学习过 Python,一定知道三引号:
1 2 3 4 5 6 7 multi_line = """ nice to meet you! nice to meet you! nice to meet you! """ print multi_line
没错,Java13 也带了这样的特性,旨在方便我们编写复杂字符串,这样就不用再去用那么多的转义字符了:
可以看到,Java 中也可以使用这样的三引号来表示字符串 了,并且我们可以随意在里面使用特殊字符,包括双引号等,但是最后编译出来的结果实际上还是会变成一个之前这样使用了转义字符的字符串:
仔细想想,这样我们写 SQL 或是 HTML 岂不是就舒服多了?
文本块表达式在 Java 15 才正式开放使用,所以我们项目的代码级别需要调整到 15 以上。
新的 instanceof 语法 在 Java 14,instanceof 迎来了一波小更新, 比如我们之前要重写一个类的 equals 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) obj; return student.name.equals(this .name); } return false ; } }
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本 instanceof 加强之后,我们就不需要了,我们可以直接将 student 替换为模式变量 :
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @AllArgsConstructor @NoArgsConstructor public class Student { private final String 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 以上。
空指针异常的改进 相信各位小伙伴在调试代码时,经常遇到空指针异常,比如下面的这个例子:
1 2 3 4 public static void test (String a, String b) { int length = a.length() + b.length(); System.out.println(length); }
那么为空时,就会直接:
但是由于我们这里 a 和 b 都调用了 length()
方法,虽然空指针异常告诉我们问题出现在这一行,但是到底是 a 为 null 还是 b 为 null 呢?我们是没办法直接得到的(遇到过这种问题的扣个 1 吧,只能调试,就很头疼)
但是当我们在 Java 14 或更高版本运行时:
这里会明确指出是哪一个变量调用出现了空指针,更人性化。
记录类型 继类、接口、枚举、注解之后的又一新类型来了,它的名字叫”记录”,在 Java 14 中首次出场,这一出场,Lombok 的噩梦来了。
在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据,比如我们从数据库中查询的账户信息,最后会被映射为一个实体类:
1 2 3 4 5 @Data public class Account { String username; String password; }
Lombok 可以说是简化代码的神器了,他能在编译时自动生成 getter 和 setter、构造方法、toString()方法等实现,在编写这些实体类时,简直不要太好用,而这一波,官方也是看不下去了,于是自己也搞了一个记录类型。
记录类型本质上也是一个普通的类,不过是 final 类型且继承自 java.lang.Record 抽象类的,它会在编译时,会自动编译出 public get
hashcode
、equals
、toString
等方法,好家伙,这是要逼死 Lombok 啊。
1 2 3 public record Account (String username, String password) { }
使用起来也是非常方便,自动生成了构造方法和成员字段的公共 get 方法:
并且 toString 也是被重写了的:
equals()
方法仅做成员字段之间的值比较,也是帮助我们实现好了的:
1 2 3 Account account0 = new Account ("Admin" , "123456" );Account account1 = new Account ("Admin" , "123456" ); System.out.println(account0.equals(account1));
是不是感觉这种类型就是专门为这种实体类而生的。
1 2 3 4 5 6 7 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
关键字:
1 2 3 public final class A { }
这样有一个缺点,如果添加了 final
关键字,那么无论是谁,包括我们自己也是没办法实现继承的,但是现在我们有一个需求,只允许我们自己写的类继承 A,但是不允许别人写的类继承 A,这时该咋写?在 Java 17 之前想要实现就很麻烦。
但是现在我们可以使用密封类型来实现这个功能:
1 2 3 public sealed class A permits B{ }
密封类型有以下要求:
可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
必须有子类继承,且不能是匿名内部类或是 lambda 的形式。
sealed
写在原来 final
的位置,但是不能和 final
、non-sealed
关键字同时出现,只能选择其一。
继承的子类必须显式标记为 final
、sealed
或是 non-sealed
类型。
标准的声明格式如下:
1 2 3 public sealed [abstract ] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{ }
注意子类格式为:
1 2 3 4 5 public [final /sealed /non-sealed ] class 子类 extends 父类 { }
比如现在我们写了这些类:
1 2 3 public sealed class A permits B{ }
1 2 3 public final class B extends A { }
我们可以看到其他的类无论是继承 A 还是继承 B 都无法通过编译:
但是如果此时我们主动将 B 设定为 non-sealed
类型:
1 2 3 public non-sealed class B extends A { }
这样就可以正常继承了,因为 B 指定了 non-sealed
主动放弃了密封特性,这样就显得非常灵活了。
当然我们也可以通过反射来获取类是否为密封类型:
1 2 3 4 public static void main (String[] args) { Class<A> a = A.class; System.out.println(a.isSealed()); }
专题
java 出入牛犊- [[Java Notes]]
java 小试牛刀- [[Java_Exercise]]
java 窥探炼器-[[Java常用新特性-Lambda-Optional-Stream]]
Springboot 速成秘籍-[[SpringBoot启示录]]
Spring出入牛犊-[[Spring核心学习]]
SpringSecurity 行走利器-[[SpringSecurity]]
SpringBoot 结丹-[[SpringBoot]]
SpringBoot 整活-[[SpringBoot+Vue项目实战]]
SpringCloud 元婴-[[SpringCloud-微服务基础]]
SpringCloud 淬炼-[[SpringCloud Alibaba-微服务进阶]]
SpringCloud 盘它-[[SpringCloud-微服务应用]]
SpringCLoud 空明-[[Spring Cloud Alibaba 微服务原理与实战-阅读心得]]
参考 1 2 3 4 5 url: https://www.bilibili.com/video/BV1tU4y1y7Fg/?vd_source=9c896fa9c3f9023797e8efe7be0c113e title: "JavaSE 9-17 新特性 已完结(IDEA 2022.1 最新版)4K 蓝光画质 Java9/10/11/12/13/14/15/16/17 讲解_哔哩哔哩_bilibili" description: "JavaSE 9-17 新特性 已完结(IDEA 2022.1 最新版)4K 蓝光画质 Java9/10/11/12/13/14/15/16/17 讲解共计 19 条视频,包括:Java9-17 新特性介绍、Java8 回顾:Lambda 表达式、Java8 回顾:Optional 类等,UP 主更多精彩视频,请关注 UP 账号。" host: www.bilibili.com image: https://i2.hdslb.com/bfs/archive/dc0cc9464c6fc274c1f23f682a01dab5a358217b.jpg@100w_100h_1c.png
JavaSE 9-17 新特性 已完结(IDEA 2022.1最新版)4K蓝光画质 Java9/10/11/12/13/14/15/16/17讲解_哔哩哔哩_bilibili