Java新特性介绍
Java 8是Oracle 公司于 2014 年 3 月 18 日发布的,距离今天已经过了近十年的时间了,Java并没有就此止步,而是继续不断发展壮大,几乎每隔6个月,就会冒出一个新版本,最新的版本已经快要迭代到Java 20了,与Java 8相差了足足十来个版本,但是由于Java 8的稳定和生态完善(目前仍是LTS长期维护版本),依然有很多公司在坚持使用Java 8,不过随着SpringBoot 3.0的到来,现在强制要求使用Java 17版本(同样也是LTS长期维护版本),下一个Java版本的时代,或许已经临近了。
Java 8 关键特性回顾
在开始之前,我们先来回顾一下Java 8中学习的Lambda表达式和Optional类,有关Stream API请各位小伙伴回顾一下Java SE篇视频教程,这里不再进行介绍。
Lambda表达式
在Java 8之前,我们在某些情况下可能需要用到匿名内部类,比如:
1 | public static void main(String[] args) { |
在创建Thread时,我们需要传入一个Runnable接口的实现类,来指定具体的在新的线程中要执行的任务,相关的逻辑需要我们在run()
方法中实现,这时为了方便,我们就直接使用匿名内部类的方式传入一个实现,但是这样的写法实在是太过臃肿了。
在Java 8之后,我们可以对类似于这种匿名内部类的写法,进行缩减,实际上我们进行观察会发现,真正有用的那一部分代码,实际上就是我们对run()
方法的具体实现,而其他的部分实际上在任何地方编写都是一模一样的,那么我们能否针对于这种情况进行优化呢?我们现在只需要一个简短的lambda表达式即可:
1 | public static void main(String[] args) { |
我们可以发现,原本需要完整编写包括类、方法在内的所有内容,全部不再需要,而是直接使用类似于() ‐> { 代码语句 }
的形式进行替换即可。是不是感觉瞬间代码清爽了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 | //添加了此注解的接口,都支持lambda表达式,符合函数式接口定义 |
因此,Runable的的匿名内部类实现,就可以简写为:
1 | Runnable runnable = () -> { }; |
我们也可以写一个玩玩:
1 |
|
它的Lambda表达式的实现就可以写为:
1 | Test test = (Integer i) -> { return i+""; }; //这里我们就简单将i转换为字符串形式 |
不过还可以进行优化,首先方法参数类型是可以省略的:
1 | Test test = (i) -> { return i+""; }; |
由于只有一个参数,可以不用添加小括号(多个参数时需要):
1 | Test test = i -> { return i+""; }; |
由于仅有返回语句这一行,所以可以直接写最终返回的结果,并且无需花括号:
1 | Test test = i -> i+""; |
这样,相比我们之前直接去编写一个匿名内部类,是不是简介了很多很多。当然,除了我们手动编写接口中抽象方法的方法体之外,如果已经有实现好的方法,是可以直接拿过来用的,比如:
1 | String test(Integer i); //接口中的定义 |
1 | public static String impl(Integer i){ //现在有一个静态方法,刚好匹配接口中抽象方法的返回值和参数列表 |
所以,我们可以直接将此方法,作为lambda表达式的方法体实现(其实这就是一种方法引用,引用了一个方法过来,这也是为什么前面说是我们为所需要的接口提供了一个方法作为它的实现
,是不是越来越体会到这句话的精髓了):
1 | public static void main(String[] args) { |
比如我们现在需要对一个数组进行排序:
1 | public static void main(String[] args) { |
但是我们发现,Integer类中有一个叫做compare
的静态方法:
1 | public static int compare(int x, int y) { |
这个方法是一个静态方法,但是它却和Comparator
需要实现的方法返回值和参数定义一模一样,所以,懂的都懂:
1 | public static void main(String[] args) { |
那么要是不是静态方法而是普通的成员方法呢?我们注意到Comparator要求我们实现的方法为:
1 | public int compare(Integer o1, Integer o2) { |
其中o1和o2都是Integer类型的,我们发现Integer类中有一个compareTo
方法:
1 | public int compareTo(Integer anotherInteger) { |
只不过这个方法并不是静态的,而是对象所有:
1 | Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5}; |
但是此时我们会发现,IDEA提示我们可以缩写,这是为什么呢?实际上,当我们使用非静态方法时,会使用抽象方参数列表的第一个作为目标对象,后续参数作为目标对象成员方法的参数,也就是说,此时,o1
作为目标对象,o2
作为参数,正好匹配了compareTo
方法,所以,直接缩写:
1 | public static void main(String[] args) { |
成员方法也可以让对象本身不成为参与的那一方,仅仅引用方法:
1 | public static void main(String[] args) { |
当然,类的构造方法同样可以作为方法引用传递:
1 | public interface Test { |
我们发现,String类中刚好有一个:
1 | public String(String original) { //由于String类的构造方法返回的肯定是一个String类型的对象,且此构造方法需要一个String类型的对象,所以,正好匹配了接口中的 |
于是乎:
1 | public static void main(String[] args) { |
当然除了上面提到的这些情况可以使用方法引用之外,还有很多地方都可以,还请各位小伙伴自行探索了。Java 8也为我们提供了一些内置的函数式接口供我们使用:Consumer、Function、Supplier等,具体请回顾一下JavaSE篇视频教程。
Optional类
Java 8中新引入了Optional特性,来让我们更优雅的处理空指针异常。我们先来看看下面这个例子:
1 | public static void hello(String str){ //现在我们要实现一个方法,将传入的字符串转换为小写并打印 |
但是这样实现的话,我们少考虑了一个问题,万一给进来的str
是null
呢?如果是null
的话,在调用toLowerCase
方法时岂不是直接空指针异常了?所以我们还得判空一下:
1 | public static void hello(String str){ |
但是这样写着就不能一气呵成了,我现在又有强迫症,我就想一行解决,这时,Optional来了,我们可以将任何的变量包装进Optional类中使用:
1 | public static void hello(String str){ |
由于这里只有一句打印,所以我们来优化一下:
1 | public static void hello(String str){ |
这样,我们就又可以一气呵成了,是不是感觉比之前的写法更优雅。
除了在不为空时执行的操作外,还可以直接从Optional中获取被包装的对象:
1 | System.out.println(Optional.ofNullable(str).get()); |
不过此时当被包装的对象为null时会直接抛出异常,当然,我们还可以指定如果get的对象为null的替代方案:
1 | System.out.println(Optional.ofNullable(str).orElse("VVV")); //orElse表示如果为空就返回里面的内容 |
其他操作还请回顾JavaSE篇视频教程。
Java 9 新特性
这一部分,我们将介绍Java 9为我们带来的新特性,Java 9的主要特性有,全新的模块机制、接口的private方法等。
模块机制
在我们之前的开发中,不知道各位有没有发现一个问题,就是当我们导入一个jar
包作为依赖时(包括JDK官方库),实际上很多功能我们并不会用到,但是由于它们是属于同一个依赖捆绑在一起,这样就会导致我们可能只用到一部分内容,但是需要引用一个完整的类库,实际上我们可以把用不到的类库排除掉,大大降低依赖库的规模。
于是,Java 9引入了模块机制来对这种情况进行优化,在之前的我们的项目是这样的:
而在引入模块机制之后:
可以看到,模块可以由一个或者多个在一起的 Java 包组成,通过将这些包分出不同的模块,我们就可以按照模块的方式进行管理了。这里我们创建一个新的项目,并在src
目录下,新建module-info.java
文件表示此项目采用模块管理机制:
1 | module NewHelloWorld { //模块名称随便起一个就可以,但是注意必须是唯一的,以及模块内的包名也得是唯一的,即使模块不同 |
接着我们来创建一个主类:
程序可以正常运行,貌似和之前没啥区别,不过我们发现,JDK为我们提供的某些框架不见了:
Java为我们提供的logging
相关日志库呢?我们发现现在居然不见了?实际上它就是被作为一个模块单独存在,这里我们需进行模块导入:
1 | module NewHelloWorld { //模块名称随便起一个就可以 |
这里我们导入java.logging相关模块后,就可以正常使用Logger了:
是不是瞬间感觉编写代码时清爽了许多,全新的模块化机制提供了另一个级别的Java代码可见性、可访问性的控制,不过,你以为仅仅是做了包的分离吗?我们可以来尝试通过反射获取JDK提供的类中的字段:
1 | //Java17版本的String类 |
1 | public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { |
但是我们发现,在程序运行之后,修改操作被阻止了:
反射 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 | package com.test; |
接着我们编写一下项目A的模块设置:
这里我们将com.test
包下所有内容都暴露出去,默认情况下所有的包都是私有的,就算其他项目将此项目作为依赖也无法使用。
接着我们现在想要在项目B中使用项目A的User类,我们需要进行导入:
现在我们就可以在Main类中使用模块module.a
中暴露出来的包内容了:
1 | import com.test.User; //如果模块module.a不暴露,那么将无法导入 |
当然除了普通的exports
进行包的暴露之外,我们也可以直接指定将包暴露给指定的模块:
1 | module module.a { |
不过现在还有一个问题,如果模块module.a
依赖于其他模块,那么会不会传递给依赖于模块module.a
的模块呢?
1 | module module.a { |
可以看到,在模块module.b
中,并没有进行依赖传递,说明哪个模块导入的依赖只能哪个模块用,但是现在我们希望依赖可以传递,就是哪个模块用了什么依赖,依赖此模块的模块也会自动进行依赖,我们可以通过一个关键字解决:
1 | module module.a { |
现在就可以使用了:
还有我们前面演示的反射,我们发现如果我们依赖了一个模块,是没办法直接进行反射操作的:
1 | public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { |
那么怎么样才可以使用反射呢?我们可以为其他模块开放某些运行使用反射的类:
1 | open module module.a { //直接添加open关键字开放整个模块的反射权限 |
1 | module module.a { |
我们还可以指定模块需要使用的抽象类或是接口实现:
1 | package com.test; |
1 | open module module.a { |
我们可以在模块B中去实现一下,然后声明我们提供了实现类:
1 | package com.main; |
1 | module module.b { |
了解了以上的相关知识后,我们就可以简单地进行模块的使用了。比如现在我们创建了一个新的Maven项目:
然后我们导入了lombok框架的依赖,如果我们不创建module-info.java
文件,那么就是一个未命名模块,未命名模块默认可以使用其他所有模块提供的类,实际上就是我们之前的开发模式:
1 | package com.test; |
现在我们希望按照全新的模块化开发模式来进行开发,将我们的项目从未命名模块改进为应用程序模块,所以我们先创建好module-info.java
文件:
1 | module com.test { |
可以看到,直接报错了:
明明导入了lombok依赖,却无法使用,这是因为我们还需要去依赖对应的模块才行:
1 | module com.test { |
这样我们就可以正常使用了,之后为了教程演示方便,咱们还是不用模块。
JShell交互式编程
Java 9为我们通过了一种交互式编程工具JShell,你还别说,真有Python那味。
环境配置完成后,我们只需要输入jshell
命令即可开启交互式编程了,它支持我们一条一条命令进行操作。
比如我们来做一个简单的计算:
我们一次输入一行(可以不加分号),先定义一个a=10和b=10,然后定义c并得到a+b的结果,可以看到还是非常方便的,但是注意语法还是和Java是一样的。
我们也可以快速创建一个方法供后续的调用。当我们按下Tab键还可以进行自动补全:
除了直接运行我们写进去的代码之外,它还支持使用命令,输入help
来查看命令列表:
比如我们可以使用/vars
命令来展示当前定义的变量列表:
当我们不想使用jshell时,直接输入/exit
退出即可:
接口中的private方法
在Java 8中,接口中 的方法支持添加default
关键字来添加默认实现:
1 | public interface Test { |
而在Java 9中,接口再次得到强化,现在接口中可以存在私有方法了:
1 | public interface Test { |
注意私有方法必须要提供方法体,因为权限为私有的,也只有这里能进行方法的具体实现了,并且此方法只能被接口中的其他私有方法或是默认实现调用。
集合类新增工厂方法
在之前,如果我们想要快速创建一个Map只能:
1 | public static void main(String[] args) { |
而在Java 9之后,我们可以直接通过of
方法来快速创建了:
1 | public static void main(String[] args) { |
是不是感觉非常方便,of方法还被重载了很多次,分别适用于快速创建包含0~10对键值对的Map:
但是注意,通过这种方式创建的Map和通过Arrays创建的List比较类似,也是无法进行修改的。
当然,除了Map之外,其他的集合类都有相应的of
方法:
1 | public static void main(String[] args) { |
改进的 Stream API
还记得我们之前在JavaSE中学习的Stream流吗?当然这里不是指进行IO操作的流,而是JDK1.8新增的Stream API,通过它大大方便了我们的编程。
1 | public static void main(String[] args) { |
自从有了Stream,我们对于集合的一些操作就大大地简化了,对集合中元素的批量处理,只需要在Stream中一气呵成(具体的详细操作请回顾JavaSE篇)
如此方便的框架,在Java 9得到了进一步的增强:
1 | public static void main(String[] args) { |
还有,我们可以通过迭代快速生成一组数据(实际上Java 8就有了,这里新增的是允许结束迭代的):
1 | public static void main(String[] args) { |
1 | public static void main(String[] args) { |
Stream还新增了对数据的截断操作,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
1 | public static void main(String[] args) { |
1 | public static void main(String[] args) { |
其他小型变动
Try-with-resource语法现在不需要再完整的声明一个变量了,我们可以直接将现有的变量丢进去:
1 | public static void main(String[] args) throws IOException { |
在Java 8中引入了Optional类,它很好的解决了判空问题:
1 | public static void main(String[] args) throws IOException { |
这种写法就有点像Kotlin或是JS中的语法:
1 | fun main() { |
在Java 9新增了一些更加方便的操作:
1 | public static void main(String[] args) { |
我们也可以使用or()
方法快速替换为另一个Optional类:
1 | public static void main(String[] args) { |
当然还支持直接转换为Stream,这里就不多说了。
在Java 8及之前,匿名内部类是没办法使用钻石运算符进行自动类型推断的:
1 | public abstract class Test<T>{ //这里我们写一个泛型类 |
1 | public static void main(String[] args) throws IOException { |
当然除了以上的特性之外还有Java 9的多版本JAR包支持、CompletableFuture API的改进等,因为不太常用,这里就不做介绍了。
Java 10 新特性
Java 10主要带来的是一些内部更新,相比Java 9带来的直观改变不是很多,其中比较突出的就是局部变量类型推断了。
局部变量类型推断
在Java中,我们可以使用自动类型推断:
1 | public static void main(String[] args) { |
但是注意,var
关键字必须位于有初始值设定的变量上,否则鬼知道你要用什么类型。
我们来看看是不是类型也能正常获取:
1 | public static void main(String[] args) { |
这里虽然是有了var关键字进行自动类型推断,但是最终还是会变成String类型,得到的Class也是String类型。但是Java终究不像JS那样进行动态推断,这种类型推断仅仅发生在编译期间,到最后编译完成后还是会变成具体类型的:
并且var
关键字仅适用于局部变量,我们是没办法在其他地方使用的,比如类的成员变量:
有关Java 10新增的一些其他改进,这里就不提了。
Java 11 新特性
Java 11 是继Java 8之后的又一个TLS长期维护版本,在Java 17出现之前,一直都是此版本作为广泛使用的版本,其中比较关键的是用于Lambda的形参局部变量语法。
用于Lambda的形参局部变量语法
在Java 10我们认识了var
关键字,它能够直接让局部变量自动进行类型推断,不过它不支持在lambda中使用:
但是实际上这里是完全可以进行类型推断的,所以在Java 11,终于是支持了,这样编写就不会报错了:
针对于String类的方法增强
在Java 11为String新增一些更加方便的操作:
1 | public static void main(String[] args) { |
我们还可以通过repeat()
方法来让字符串重复拼接:
1 | public static void main(String[] args) { |
我们也可以快速地进行空格去除操作:
1 | public static void main(String[] args) { |
全新的HttpClient使用
在Java 9的时候其实就已经引入了全新的Http Client API,用于取代之前比较老旧的HttpURLConnection类,新的API支持最新的HTTP2和WebSocket协议。
1 | public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException { |
利用全新的客户端,我们甚至可以轻松地做一个爬虫(仅供学习使用,别去做违法的事情,爬虫玩得好,牢饭吃到饱),比如现在我们想去批量下载某个网站的壁纸:
网站地址:https://pic.netbian.com/4kmeinv/
我们随便点击一张壁纸,发现网站的URL格式为:
并且不同的壁纸似乎都是这样:https://pic.netbian.com/tupian/数字.html,好了差不多可以开始整活了:
1 | public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException { |
可以看到,最后控制台成功获取到这些图片的网站页面了:
接着我们需要来观察一下网站的HTML具体怎么写的,把图片的地址提取出来:
好了,知道图片在哪里就好办了,直接字符串截取:
1 | public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException { |
好了,现在图片地址也可以批量拿到了,直接获取这些图片然后保存到本地吧:
1 | public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException { |
我们现在来看看效果吧,美女的图片已经成功保存到本地了:
当然,这仅仅是比较简单的爬虫,不过我们的最终目的还是希望各位能够学会使用新的HttpClient API。
Java 12-16 新特性
由于Java版本的更新迭代速度自Java 9开始为半年更新一次(Java 8到Java 9隔了整整三年),所以各个版本之间的更新内容比较少,剩余的6个版本,我们就多个版本放在一起进行讲解了。
Java12-16这五个版本并非长期支持版本,所以很多特性都是一种处于实验性功能,12/13版本引入了一些实验性功能,并根据反馈进行调整,最后在后续版本中正式开放使用,其实就是体验服的那种感觉。
新的switch语法
在Java 12引入全新的switch语法,让我们使用switch语句更加的灵活,比如我们想要编写一个根据成绩得到等级的方法:
1 | /** |
现在我们想要使用switch来实现这个功能(不会吧不会吧,不会有人要想半天怎么用switch实现吧),之前的写法是:
1 | public static String grade(int score){ |
但是现在我们可以使用新的特性了:
1 | public static String grade(int score){ |
不过最后编译出来的样子,貌似还是和之前是一样的:
这种全新的switch语法称为switch表达式
,它的意义不仅仅体现在语法的精简上,我们来看看它的详细规则:
1 | var res = switch (obj) { //这里和之前的switch语句是一样的,但是注意这样的switch是有返回值的,所以可以被变量接收 |
那么如果我们并不是能够马上返回,而是需要做点什么其他的工作才能返回结果呢?
1 | var res = switch (obj) { //增强版switch语法 |
当然,也可以像这样:
1 | var res = switch (args.length) { //增强版switch语法 |
这种全新的语法,可以说极大地方便了我们的编码,不仅代码简短,而且语义明确。唯一遗憾的是依然不支持区间匹配。
注意:switch表达式在Java 14才正式开放使用,所以我们项目的代码级别需要调整到14以上。
文本块
如果你学习过Python,一定知道三引号:
1 | #当我们需要使用复杂字符串时,可能字符串中包含了很多需要转义的字符,比如双引号等,这时我们就可以使用三引号来囊括字符串 |
没错,Java13也带了这样的特性,旨在方便我们编写复杂字符串,这样就不用再去用那么多的转义字符了:
可以看到,Java中也可以使用这样的三引号来表示字符串了,并且我们可以随意在里面使用特殊字符,包括双引号等,但是最后编译出来的结果实际上还是会变成一个之前这样使用了转义字符的字符串:
仔细想想,这样我们写SQL或是HTML岂不是就舒服多了?
注意:文本块表达式在Java 15才正式开放使用,所以我们项目的代码级别需要调整到15以上。
新的instanceof语法
在Java 14,instanceof迎来了一波小更新(哈哈,这版本instanceof又加强了,版本强势语法)
比如我们之前要重写一个类的equals方法:
1 | public class Student { |
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本instanceof加强之后,我们就不需要了,我们可以直接将student替换为模式变量:
1 | public class Student { |
在使用instanceof
判断类型成立后,会自动强制转换类型为指定类型,简化了我们手动转换的步骤。
注意:新的instanceof语法在Java 16才正式开放使用,所以我们项目的代码级别需要调整到16以上。
空指针异常的改进
相信各位小伙伴在调试代码时,经常遇到空指针异常,比如下面的这个例子:
1 | public static void test(String a, String b){ |
那么为空时,就会直接:
但是由于我们这里a和b都调用了length()
方法,虽然空指针异常告诉我们问题出现在这一行,但是到底是a为null还是b为null呢?我们是没办法直接得到的(遇到过这种问题的扣个1吧,只能调试,就很头疼)
但是当我们在Java 14或更高版本运行时:
这里会明确指出是哪一个变量调用出现了空指针,是不是感觉特别人性化。
记录类型
继类、接口、枚举、注解之后的又一新类型来了,它的名字叫”记录”,在Java 14中首次出场,这一出场,Lombok的噩梦来了。
在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据,比如我们从数据库中查询的账户信息,最后会被映射为一个实体类:
1 |
|
Lombok可以说是简化代码的神器了,他能在编译时自动生成getter和setter、构造方法、toString()方法等实现,在编写这些实体类时,简直不要太好用,而这一波,官方也是看不下去了,于是自己也搞了一个记录类型。
记录类型本质上也是一个普通的类,不过是final类型且继承自java.lang.Record抽象类的,它会在编译时,会自动编译出 public get
hashcode
、equals
、toString
等方法,好家伙,这是要逼死Lombok啊。
1 | public record Account(String username, String password) { //直接把字段写在括号中 |
使用起来也是非常方便,自动生成了构造方法和成员字段的公共get方法:
并且toString也是被重写了的:
equals()
方法仅做成员字段之间的值比较,也是帮助我们实现好了的:
1 | Account account0 = new Account("Admin", "123456"); |
是不是感觉这种类型就是专门为这种实体类而生的。
1 | public record Account(String username, String password) implements Runnable { //支持实现接口,但是不支持继承,因为继承的坑位已经默认被占了 |
注意:记录类型在Java 16才正式开放使用,所以我们项目的代码级别需要调整到16以上。
Java 17 新特性
Java 17作为新的LTS长期维护版本,我们来看看都更新了什么(不包含预览特性,包括switch第二次增强,哈哈,果然还是强度不够,都连续加强两个版本了)
密封类型
密封类型可以说是Java 17正式推出的又一重磅类型,它在Java 15首次提出并测试了两个版本。
在Java中,我们可以通过继承(extends关键字)来实现类的能力复用、扩展与增强。但有的时候,可能并不是所有的类我们都希望能够被继承。所以,我们需要对继承关系有一些限制的控制手段,而密封类的作用就是限制类的继承。
实际上在之前我们如果不希望别人继承我们的类,可以直接添加final
关键字:
1 | public final class A{ //添加final关键字后,不允许对此类继承 |
这样有一个缺点,如果添加了final
关键字,那么无论是谁,包括我们自己也是没办法实现继承的,但是现在我们有一个需求,只允许我们自己写的类继承A,但是不允许别人写的类继承A,这时该咋写?在Java 17之前想要实现就很麻烦。
但是现在我们可以使用密封类型来实现这个功能:
1 | public sealed class A permits B{ //在class关键字前添加sealed关键字,表示此类为密封类型,permits后面跟上允许继承的类型,多个子类使用逗号隔开 |
密封类型有以下要求:
- 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
- 必须有子类继承,且不能是匿名内部类或是lambda的形式。
sealed
写在原来final
的位置,但是不能和final
、non-sealed
关键字同时出现,只能选择其一。- 继承的子类必须显式标记为
final
、sealed
或是non-sealed
类型。
标准的声明格式如下:
1 | public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{ |
注意子类格式为:
1 | public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类 |
比如现在我们写了这些类:
1 | public sealed class A permits B{ //指定B继承A |
1 | public final class B extends A { //在子类final,彻底封死 |
我们可以看到其他的类无论是继承A还是继承B都无法通过编译:
但是如果此时我们主动将B设定为non-sealed
类型:
1 | public non-sealed class B extends A { |
这样就可以正常继承了,因为B指定了non-sealed
主动放弃了密封特性,这样就显得非常灵活了。
当然我们也可以通过反射来获取类是否为密封类型:
1 | public static void main(String[] args) { |
至此,Java 9 - 17的主要新特性就讲解完毕了。