Java新特性
介绍Java 9 - Java 17这些版本的所有新增特性
随着SpringBoot 3.0的到来,现在强制要求使用Java 17版本(同样也是LTS长期维护版本)
Java 8 关键特性
Lambda表达式
在Java 8之后,lambda表达式重写Runnable接口的run()
方法:
public static void main(String[] args) {
//现在我们想新建一个线程来做事情
Thread thread = new Thread(() -> {
System.out.println("Hello World!"); //只需留下我们需要具体实现的方法体
});
thread.start();
}
匿名内部类会在编译时创建一个单独的class文件,但是lambda却不会
Lambda为所需要的接口提供了一个方法作为它的实现,而之后创建实现类就只需要交给JVM去处理就好了。
Lambda表达式的具体规范:
- 标准格式为:
([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
- 和匿名内部类不同,Lambda仅支持接口,不支持抽象类
- 接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)
Runable类:
代码语言:javascript复制@FunctionalInterface //添加了此注解的接口,都支持lambda表达式,符合函数式接口定义
public interface Runnable {
public abstract void run(); //有且仅有一个抽象方法,此方法返回值为void,且没有参数
}
只有一个参数,可以不用添加小括号(多个参数时需要)
返回语句这一行,所以可以直接写最终返回的结果,并且无需花括号
可以将某个方法作为lambda表达式的方法体实现(其实这就是一种方法引用,引用了一个方法过来)
代码语言:javascript复制public static void main(String[] args) {
Test test = Main::impl; //使用 类名::方法名称 的形式来直接引用一个已有的方法作为实现
}
public static String impl(Integer i){
return "我是已经存在的实现" i;
}
成员方法也可以让对象本身不成为参与的那一方,仅仅引用方法
Optional类
Optional特性,来让我们更优雅的处理空指针异常。
可以将任何的变量包装进Optional类中使用:
代码语言:javascript复制public static void hello(String str){
Optional
.ofNullable(str) //将str包装进Optional
.ifPresent(s -> { //ifPresent表示只有对象不为null才会执行里面的逻辑,实现一个Consumer(接受一个参数,返回值为void)
System.out.println(s);
});
}
直接从Optional中获取被包装的对象:
代码语言:javascript复制System.out.println(Optional.ofNullable(str).get());
当被包装的对象为null时会直接抛出异常
指定如果get的对象为null的替代方案:
代码语言:javascript复制System.out.println(Optional.ofNullable(str).orElse("VVV")); //orElse表示如果为空就返回里面的内容
java9新增api:
代码语言:javascript复制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);
}
Java 9 新特性
模块机制
当我们导入一个jar
包作为依赖时(包括JDK官方库),实际上很多功能我们并不会用到,但是由于它们是属于同一个依赖捆绑在一起,这样就会导致我们可能只用到一部分内容,但是需要引用一个完整的类库,实际上我们可以把用不到的类库排除掉,大大降低依赖库的规模。
模块可以由一个或者多个在一起的 Java 包组成,通过将这些包分出不同的模块,我们就可以按照模块的方式进行管理了。
在src
目录下,新建module-info.java
文件表示此项目采用模块管理机制:
module NewHelloWorld { //模块名称随便起一个就可以,但是注意必须是唯一的,以及模块内的包名也得是唯一的,即使模块不同
}
进行模块导入:
代码语言:javascript复制module NewHelloWorld { //模块名称随便起一个就可以
requires java.logging; //除了JDK的一些常用包之外,只有我们明确需要的模块才会导入依赖库
//当然如果要导入JavaSE的所有依赖,想之前一样的话,直接 requires java.se; 即可
}
尝试通过反射获取JDK提供的类中的字段:
反射 API 的 Java 9 封装和安全性得到了改进,如果模块没有明确授权给其他模块使用反射的权限,那么其他模块是不允许使用反射进行修改的
模块机制四种类型:
- **系统模块:**来自JDK和JRE的模块(官方提供的模块),我使用
java --list-modules
命令来列出所有的模块,不同的模块会导出不同的包供我们使用。 - **应用程序模块:**我们自己写的Java模块项目。
- **自动模块:**可能有些库并不是Java 9以上的模块项目,这种时候就需要做兼容了,默认情况下是直接导出所有的包,可以访问所有其他模块提供的类,不然之前版本的库就用不了了。
- **未命名模块:**我们自己创建的一个Java项目,如果没有创建
module-info.java
,那么会按照未命名模块进行处理,未命名模块同样可以访问所有其他模块提供的类,这样我们之前写的Java 8代码才能正常地在Java 9以及之后的版本下运行。
直接指定将包暴露给指定的模块:
代码语言:javascript复制module module.a {
exports com.test to module.b; //这里我们将com.test包暴露给指定的模块module.b,非指定的模块即使导入也无法使用
}
如果模块module.a
依赖于其他模块,不会传递依赖模块
依赖传递关键字:
代码语言:javascript复制module module.a {
exports com.test to module.b;
requires transitive java.logging; //使用transitive来向其他模块传递此依赖
}
为其他类开通反射权限:
代码语言:javascript复制module module.a {
exports com.test to module.b;
opens com.test; //通过使用opens关键字来为其他模块开放反射权限
//也可以指定目标开放反射 opens com.test to module.b;
}
指定模块需要使用的抽象类或是接口实现:
代码语言:javascript复制open module module.a {
exports com.test to module.b;
uses com.test.Test; //使用uses指定,Test是一个接口(比如需要的服务等),模块需要使用到
}
声明我们提供了实现类:
代码语言:javascript复制package com.main;
import com.test.Test;
public class TestImpl implements Test {
}
代码语言:javascript复制module module.b {
requires module.a; //导入项目A的模块,此模块暴露了com.test包
provides com.test.Test with com.main.TestImpl; //声明此模块提供了Test的实现类
}
接口中的private方法
在Java 8中,接口中 的方法支持添加default
关键字来添加默认实现:
public interface Test {
default void test(){
System.out.println("我是test方法默认实现");
}
}
Java 9中,接口中可以存在私有方法了:
代码语言:javascript复制public interface Test {
default void test(){
System.out.println("我是test方法默认实现");
this.inner(); //接口中方法的默认实现可以直接调用接口中的私有方法
}
private void inner(){ //声明一个私有方法
System.out.println("我是接口中的私有方法!");
}
}
私有方法必须要提供方法体,并且此方法只能被接口中的其他私有方法或是默认实现调用。
集合类工厂方法
Java 9之后,通过of
方法来快速创建Map:
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
方法
改进 Stream API
JDK1.8新增的Stream API,通过它大大方便了我们的编程:
代码语言:javascript复制public static void main(String[] args) {
Stream
.of("A", "B", "B", "C") //这里我们可以直接将一些元素封装到Stream中
.filter(s -> s.equals("B")) //通过过滤器过滤
.distinct() //去重
.forEach(System.out::println); //最后打印
}
Java 9进一步的增强:
代码语言:javascript复制public static void main(String[] args) {
Stream
.of(null) //如果传入null会报错
.forEach(System.out::println);
Stream
.ofNullable(null) //使用新增的ofNullable方法,这样就不会了,不过这样的话流里面就没东西了
.forEach(System.out::println);
}
通过迭代快速生成一组数据:
代码语言:javascript复制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);
}
代码语言:javascript复制public static void main(String[] args) {
Stream
.iterate(0, i -> i < 20, i -> i 1) //快速生成一组0~19的int数据,中间可以添加一个断言,表示什么时候结束生成
.forEach(System.out::println);
}
新增了对数据的截断操作,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
代码语言:javascript复制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);
}
代码语言:javascript复制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语法可以直接将现有的变量丢进去:
代码语言:javascript复制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及之前,匿名内部类是没办法使用钻石运算符进行自动类型推断的:
代码语言:javascript复制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 10 新特性
局部变量类型推断
代码语言:javascript复制public static void main(String[] args) {
// String a = "Hello World!"; 之前我们定义变量必须指定类型
var a = "Hello World!"; //现在我们使用var关键字来自动进行类型推断,因为完全可以从后面的值来判断是什么类型
System.out.println(a.getClass());
}
var
关键字必须位于有初始值设定的变量上
Java终究不像JS那样进行动态推断,这种类型推断仅仅发生在编译期间,到最后编译完成后还是会变成具体类型的
var
关键字仅适用于局部变量,没办法在其他地方使用的
Java 11 新特性
Lambda的形参推断
在Java 10var
关键字,不支持在lambda中使用,在Java 11支持了:
String类方法增强
在Java 11为String新增一些更加方便的操作:
代码语言:javascript复制public static void main(String[] args) {
var str = "ABnCnD";
System.out.println(str.isBlank()); //isBlank方法用于判断是否字符串为空或者是仅包含空格
str
.lines() //根据字符串中的n换行符进行切割,分为多个字符串,并转换为Stream进行操作
.forEach(System.out::println);
System.out.println(str.repeat(2)); //让字符串重复拼接
str = " A B C D ";
System.out.println(str.strip()); //去除首尾空格
System.out.println(str.stripLeading()); //去除首部空格
System.out.println(str.stripTrailing()); //去除尾部空格
}
全新的HttpClient使用
新的API支持最新的HTTP2和WebSocket协议。
代码语言:javascript复制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());
}
Java 12-16 新特性
新的switch语法
在Java 12引入全新的switch语法,让我们使用switch语句更加的灵活
比如编写一个根据成绩得到等级的方法:
代码语言:javascript复制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 -> "不及格";
};
}
全新的switch语法称为switch表达式
:
var res = switch (obj) { //这里和之前的switch语句是一样的,但是注意这样的switch是有返回值的,所以可以被变量接收
case [匹配值, ...] -> "优秀"; //case后直接添加匹配值,匹配值可以存在多个,需要使用逗号隔开,使用 -> 来返回如果匹配此case语句的结果
case ... //根据不同的分支,可以存在多个case
default -> "不及格"; //注意,表达式要求必须涵盖所有的可能,所以是需要添加default的
};
代码语言:javascript复制var res = switch (obj) { //增强版switch语法
case [匹配值, ...] -> "优秀";
default -> { //我们可以使用花括号来将整套逻辑括起来
//... 我是其他要做的事情
yield "不及格"; //注意处理完成后需要返回最终结果,但是这样并不是使用return,而是yield关键字
}
};
文本块
Java15中可以使用这样的三引号来表示字符串了,并且我们可以随意在里面使用特殊字符,包括双引号等:
新instanceof语法
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本instanceof加强之后,我们就不需要了,我们可以直接将student替换为模式变量:
代码语言:javascript复制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
判断类型成立后,会自动强制转换类型为指定类型,简化了我们手动转换的步骤。
记录类型
在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据
记录类型在Java 16才正式开放使用,记录类型本质上也是一个普通的类,不过是final类型且继承自java.lang.Record抽象类的,它会在编译时,会自动编译出 public get
hashcode
、equals
、toString
等方法
public record Account(String username, String password) { //直接把字段写在括号中
}
Java 17 新特性
密封类型
在Java中,我们可以通过继承(extends关键字)来实现类的能力复用、扩展与增强。但有的时候,可能并不是所有的类我们都希望能够被继承。
而密封类的作用就是限制类的继承。
密封类型有以下要求:
- 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
- 必须有子类继承,且不能是匿名内部类或是lambda的形式。
sealed
写在原来final
的位置,但是不能和final
、non-sealed
关键字同时出现,只能选择其一。- 继承的子类必须显式标记为
final
、sealed
或是non-sealed
类型。
声明格式:
代码语言:javascript复制public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{
//里面的该咋写咋写
}
子类格式为:
代码语言:javascript复制public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类
//final类型:任何类不能再继承当前类,到此为止,已经封死了。
//sealed类型:同父类,需要指定由哪些类继承。
//non-sealed类型:重新开放为普通类,任何类都可以继承。
}
通过反射来获取类是否为密封类型:
代码语言:javascript复制public static void main(String[] args) {
Class<A> a = A.class;
System.out.println(a.isSealed()); //是否为密封
}
承的子类必须显式标记为`final`、`sealed`或是`non-sealed`类型。
声明格式:
~~~java
public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{
//里面的该咋写咋写
}
子类格式为:
代码语言:javascript复制public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类
//final类型:任何类不能再继承当前类,到此为止,已经封死了。
//sealed类型:同父类,需要指定由哪些类继承。
//non-sealed类型:重新开放为普通类,任何类都可以继承。
}
通过反射来获取类是否为密封类型:
代码语言:javascript复制public static void main(String[] args) {
Class<A> a = A.class;
System.out.println(a.isSealed()); //是否为密封
}