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类:
@FunctionalInterface //添加了此注解的接口,都支持lambda表达式,符合函数式接口定义 public interface Runnable { public abstract void run(); //有且仅有一个抽象方法,此方法返回值为void,且没有参数 }
只有一个参数,可以不用添加小括号(多个参数时需要)
返回语句这一行,所以可以直接写最终返回的结果,并且无需花括号
可以将某个方法作为lambda表达式的方法体实现(其实这就是一种方法引用,引用了一个方法过来)
public static void main(String[] args) { Test test = Main::impl; //使用 类名::方法名称 的形式来直接引用一个已有的方法作为实现 } public static String impl(Integer i){ return "我是已经存在的实现"+i; }
成员方法也可以让对象本身不成为参与的那一方,仅仅引用方法
Optional类
Optional特性,来让我们更优雅的处理空指针异常。
可以将任何的变量包装进Optional类中使用:
public static void hello(String str){ Optional .ofNullable(str) //将str包装进Optional .ifPresent(s -> { //ifPresent表示只有对象不为null才会执行里面的逻辑,实现一个Consumer(接受一个参数,返回值为void) System.out.println(s); }); }
直接从Optional中获取被包装的对象:
System.out.println(Optional.ofNullable(str).get());
当被包装的对象为null时会直接抛出异常
指定如果get的对象为null的替代方案:
System.out.println(Optional.ofNullable(str).orElse("VVV")); //orElse表示如果为空就返回里面的内容
java9新增api:
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 { //模块名称随便起一个就可以,但是注意必须是唯一的,以及模块内的包名也得是唯一的,即使模块不同 }
进行模块导入:
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以及之后的版本下运行。
直接指定将包暴露给指定的模块:
module module.a { exports com.test to module.b; //这里我们将com.test包暴露给指定的模块module.b,非指定的模块即使导入也无法使用 }
如果模块module.a
依赖于其他模块,不会传递依赖模块
依赖传递关键字:
module module.a { exports com.test to module.b; requires transitive java.logging; //使用transitive来向其他模块传递此依赖 }
为其他类开通反射权限:
module module.a { exports com.test to module.b; opens com.test; //通过使用opens关键字来为其他模块开放反射权限 //也可以指定目标开放反射 opens com.test to module.b; }
指定模块需要使用的抽象类或是接口实现:
open module module.a { exports com.test to module.b; uses com.test.Test; //使用uses指定,Test是一个接口(比如需要的服务等),模块需要使用到 }
声明我们提供了实现类:
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的实现类 }
接口中的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("我是接口中的私有方法!"); } }
私有方法必须要提供方法体,并且此方法只能被接口中的其他私有方法或是默认实现调用。
集合类工厂方法
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,通过它大大方便了我们的编程:
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进一步的增强:
public static void main(String[] args) { Stream .of(null) //如果传入null会报错 .forEach(System.out::println); Stream .ofNullable(null) //使用新增的ofNullable方法,这样就不会了,不过这样的话流里面就没东西了 .forEach(System.out::println); }
通过迭代快速生成一组数据:
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 .iterate(0, i -> i < 20, i -> i + 1) //快速生成一组0~19的int数据,中间可以添加一个断言,表示什么时候结束生成 .forEach(System.out::println); }
新增了对数据的截断操作,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
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及之前,匿名内部类是没办法使用钻石运算符进行自动类型推断的:
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 新特性
局部变量类型推断
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新增一些更加方便的操作:
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); 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协议。
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语句更加的灵活
比如编写一个根据成绩得到等级的方法:
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的 };
var res = switch (obj) { //增强版switch语法 case [匹配值, ...] -> "优秀"; default -> { //我们可以使用花括号来将整套逻辑括起来 //... 我是其他要做的事情 yield "不及格"; //注意处理完成后需要返回最终结果,但是这样并不是使用return,而是yield关键字 } };
文本块
Java15中可以使用这样的三引号来表示字符串了,并且我们可以随意在里面使用特殊字符,包括双引号等:
新instanceof语法
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本instanceof加强之后,我们就不需要了,我们可以直接将student替换为模式变量:
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
类型。
声明格式:
public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{ //里面的该咋写咋写 }
子类格式为:
public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类 //final类型:任何类不能再继承当前类,到此为止,已经封死了。 //sealed类型:同父类,需要指定由哪些类继承。 //non-sealed类型:重新开放为普通类,任何类都可以继承。 }
通过反射来获取类是否为密封类型:
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 [子类, ...]{ //里面的该咋写咋写 }
子类格式为:
public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类 //final类型:任何类不能再继承当前类,到此为止,已经封死了。 //sealed类型:同父类,需要指定由哪些类继承。 //non-sealed类型:重新开放为普通类,任何类都可以继承。 }
通过反射来获取类是否为密封类型:
public static void main(String[] args) { Class<A> a = A.class; System.out.println(a.isSealed()); //是否为密封 }