Java 8是Oracle 公司于 2014 年 3 月 18 日发布的,距离今天已经过了近十年的时间了,但是由于Java 8的稳定和生态完善(目前仍是LTS长期维护版本),依然有很多公司在坚持使用Java 8,不过随着SpringBoot 3.0的到来,现在强制要求使用Java 17版本(同样也是LTS长期维护版本),我们来聊聊 java 9-17的新特性.
Java 9的主要特性有,全新的模块机制、接口的private方法等。
在我们之前的开发中,不知道各位有没有发现一个问题,就是当我们导入一个jar包作为依赖时(包括JDK官方库),实际上很多功能我们并不会用到,但是由于它们是属于同一个依赖捆绑在一起,这样就会导致我们可能只用到一部分内容,但是需要引用一个完整的类库,实际上我们可以把用不到的类库排除掉,大大降低依赖库的规模。
于是,Java 9引入了模块机制来对这种情况进行优化,在之前的我们的项目是这样的:
而在引入模块机制之后:
可以看到,模块可以由一个或者多个在一起的 Java 包组成,通过将这些包分出不同的模块,我们就可以按照模块的方式进行管理了。这里我们创建一个新的项目,并在src
目录下,新建module-info.java
文件表示此项目采用模块管理机制:
我们发现,JDK为我们提供的某些框架不见了:
这里我们导入java.logging相关模块后,就可以正常使用Logger了:
全新的模块化机制提供了另一个级别的Java代码可见性、可访问性的控制,不过,你以为仅仅是做了包的分离吗?我们可以来尝试通过反射获取JDK提供的类中的字段:
但是我们发现,在程序运行之后,修改操作被阻止了:
反射 API 的 Java 9 封装和安全性得到了改进,如果模块没有明确授权给其他模块使用反射的权限,那么其他模块是不允许使用反射进行修改的.
模块具有四种类型:
**系统模块:**来自JDK和JRE的模块(官方提供的模块,比如我们上面用的),我们也可以直接使用java --list-modules命令来列出所有的模块,不同的模块会导出不同的包供我们使用。
**应用程序模块:**我们自己写的Java模块项目。
**自动模块:**可能有些库并不是Java 9以上的模块项目,这种时候就需要做兼容了,默认情况下是直接导出所有的包,可以访问所有其他模块提供的类,不然之前版本的库就用不了了。
**未命名模块:**我们自己创建的一个Java项目,如果没有创建module-info.java,那么会按照未命名模块进行处理,未命名模块同样可以访问所有其他模块提供的类,这样我们之前写的Java 8代码才能正常地在Java 9以及之后的版本下运行。不过,由于没有使用Java 9的模块新特性,未命名模块只能默认暴露给其他未命名的模块和自动模块,应用程序模块无法访问这些类(实际上就是传统Java 8以下的编程模式,因为没有模块只需要导包就行)
这里我们就来创建两个项目,看看如何使用模块机制,首先我们在项目A中,添加一个User类,一会项目B需要用到:
这里我们将com.test
包下所有内容都暴露出去,默认情况下所有的包都是私有的,就算其他项目将此项目作为依赖也无法使用。
接着我们现在想要在项目B中使用项目A的User类,我们需要进行导入:
现在我们就可以在Main类中使用模块module.a
中暴露出来的包内容了.
当然除了普通的exports
进行包的暴露之外,我们也可以直接指定将包暴露给指定的模块:
我们希望依赖可以传递,就是哪个模块用了什么依赖,依赖此模块的模块也会自动进行依赖,我们可以通过一个关键字解决:
我们依赖了一个模块,是没办法直接进行反射操作的:
那么怎么样才可以使用反射呢?我们可以为其他模块开放某些运行使用反射的类:
还可以指定模块需要使用的抽象类或是接口实现:
我们可以在模块B中去实现一下,然后声明我们提供了实现类:
Java 9为我们通过了一种交互式编程工具JShell,有Python味.
在Java 8中,接口中 的方法支持添加default
关键字来添加默认实现.
而在Java 9中,接口再次得到强化,现在接口中可以存在私有方法了:
注意私有方法必须要提供方法体,因为权限为私有的,也只有这里能进行方法的具体实现了,并且此方法只能被接口中的其他私有方法或是默认实现调用。
在之前,如果我们想要快速创建一个Map只能.
而在Java 9之后,我们可以直接通过of
方法来快速创建了:
of方法还被重载了很多次,分别适用于快速创建包含0~10对键值对的Map:
但是注意,通过这种方式创建的Map和通过Arrays创建的List比较类似,也是无法进行修改的。
当然,除了Map之外,其他的集合类都有相应的of
方法:
Set set = Set.of("BBB", "CCC", "AAA"); //注意Set中元素顺序并不一定你的添加顺序
List list = List.of("AAA", "CCC", "BBB"); //也不用 Arrays.asList()
System.out.println("===> set: " + set);
System.out.println("===> list: " + list);
Stream 在Java 9得到了进一步的增强:
public class StreamApi {
public static void main(String[] args) {
Stream
.of(null) //如果传入null会报错
.forEach(System.out :: println);
Stream
.ofNullable(null) //使用新增的ofNullable方法,这样就不会了,不过这样的话流里面就没东西了
.forEach(System.out :: println);
}
}
我们可以通过迭代快速生成一组数据(实际上Java 8就有了,这里新增的是允许结束迭代的):
// java 8
Stream
.iterate(0, i -> i + 1)
//Java8只能像这样生成无限的流,第一个参数是种子,就是后面的UnaryOperator的参数i一开始的值,最后会返回一个值作为i的新值,每一轮都会执行UnaryOperator并生成一个新值到流中,这个是源源不断的,如果不加limit()进行限制的话,将无限生成下去。
.limit(20) //这里限制生成20个
.forEach(System.out::println);
// java 9
Stream
.iterate(0, i -> i < 20, i -> i + 1) // 可以直接指定 i < 20
.forEach(System.out::println);
Stream还新增了对数据的截断操作,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
// java 9
Stream
.iterate(0, i -> i < 20, i -> i + 1) // 可以直接指定 i < 20
// .takeWhile(i -> i < 10) //当i小于10时正常通过,一旦大于等于10直接截断
.dropWhile(i -> i < 10) // 和上面的相反, 直接丢弃满足条件的.
.forEach(System.out::println);
Try-with-resource语法现在不需要再完整的声明一个变量了,我们可以直接将现有的变量丢进去:
// java8
// try (InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));) {
// for (int i = 0; i < 100; i++)
// System.out.print((char) inputStream.read());
// }
// java 9
InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));
try (inputStream) { //单独丢进try中,效果是一样的
for (int i = 0; i < 100; i++)
System.out.print((char) inputStream.read());
}
但是编译还是最终编译成了 try catch:
Optional类, 在Java 9新增了一些更加方便的操作:
String str = null;
// java 8
Optional
.ofNullable(str)
.ifPresent(s -> System.out.println(s.toLowerCase()));
// java 9
//通过使用ifPresentOrElse,我们同时处理两种情况
Optional.ofNullable(str).ifPresentOrElse(s -> {
System.out.println("被包装的元素为:"+s); //第一种情况和ifPresent是一样的
}, () -> {
System.out.println("被包装的元素为null"); //第二种情况是如果为null的情况
});
当然除了以上的特性之外还有Java 9的多版本JAR包支持、CompletableFuture API的改进等,因为不太常用,就不做介绍了.
Java 10主要带来的是一些内部更新,相比Java 9带来的直观改变不是很多,其中比较突出的就是局部变量类型推断了。
public static void main(String[] args) {
// String a = "Hello World!"; 之前我们定义变量必须指定类型
var a = "Hello World!"; //现在我们使用var关键字来自动进行类型推断,因为完全可以从后面的值来判断是什么类型
}
但是注意,var
关键字必须位于有初始值设定的变量上,否则鬼知道你要用什么类型。
虽然是有了var关键字进行自动类型推断,但是最终还是会变成String类型,得到的Class也是String类型。但是Java终究不像JS那样进行动态推断,这种类型推断仅仅发生在编译期间,到最后编译完成后还是会变成具体类型的:
var
关键字仅适用于局部变量,我们是没办法在其他地方使用的,比如类的成员变量:
Java 11 是继Java 8之后的又一个TLS长期维护版本,在Java 17出现之前,一直都是此版本作为广泛使用的版本,其中比较关键的是用于Lambda的形参局部变量语法。
我们刚认识了var
关键字,它能够直接让局部变量自动进行类型推断,不过它不支持在lambda中使用:
但是实际上这里是完全可以进行类型推断的,所以在Java 11,支持了,这样编写就不会报错了:
public static void main(String[] args) {
Consumer consumer = (var str) -> {
};
Consumer consumer1 = (String str) -> {
};
}
在Java 11为String新增一些更加方便的操作:
public static void main(String[] args) {
String s = "", s1 = " ";
System.out.println(s.isEmpty());
System.out.println(s1.isEmpty());
System.out.println(s1.isBlank()); //isBlank方法用于判断是否字符串为空或者是仅包含空格
var str = "AB\nC\nD";
str.lines() //根据字符串中的\n换行符进行切割,分为多个字符串,并转换为Stream进行操作
.forEach(System.out::println);
}
我们还可以通过repeat()
方法来让字符串重复拼接:
var str = "ABCD";
//比如现在我们有一个ABCD,但是现在我们想要一个ABCDABCD这样的基于原本字符串的重复字符串
System.out.println(str.repeat(2)); //一个repeat就搞定了
String str = " A B C D ";
System.out.println(str.trim()); //去除首尾空格
System.out.println(str.strip()); //去除首尾空格
/**
* trim()可以去除字符串前后的半角空白字符
* strip()可以去除字符串前后的全角和半角空白字符
*/
System.out.println(str.stripLeading()); //去除首部空格
System.out.println(str.stripTrailing()); //去除尾部空格
在Java 9的时候其实就已经引入了全新的Http Client API,用于取代之前比较老旧的HttpURLConnection类,新的API
主要的特性有:
完整支持HTTP 2.0 或者HTTP 1.1
支持 HTTPS/TLS
有简单的阻塞使用方法
支持异步发送,异步时间通知
支持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 response = client.send(request, HttpResponse.BodyHandlers.ofString());
//来看看响应实体是什么吧
System.out.println(response.body());
}
由于Java版本的更新迭代速度自Java 9开始为半年更新一次(Java 8到Java 9隔了整整三年),所以各个版本之间的更新内容比较少,所以咱们就多个版本放在一起说了。
在Java 12引入全新的switch语法,让我们使用switch语句更加的灵活,比如我们想要编写一个根据成绩得到等级的方法:
public static void main(String[] args) {
System.out.println(gradeNew(79));
}
public static String gradeNew(int score) {
score /= 10;
return switch (score) {
case 10, 9 -> "优秀";
case 8, 7 -> "良好";
case 6 -> "及格";
default -> "不及格";
};
}
// yield
public static String gradeNew2(int score) {
score /= 10;
return switch (score) {
case 10, 9 -> "优秀";
case 8, 7 -> "良好";
case 6 -> "及格";
default -> {
System.out.println("做了很多事情");
yield "不及格";
}
};
}
/**
* 传入分数(范围 0 - 100)返回对应的等级:
* 100-90:优秀
* 70-80:良好
* 60-70:及格
* 0-60:寄
*
* @param score 分数
* @return 等级
*/
public static String gradeBefore(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;
}
这种全新的语法,可以说极大地方便了我们的编码,不仅代码简短,而且语义明确。唯一遗憾的是依然不支持区间匹配。
**注意:**switch表达式在Java 14才正式开放使用,所以我们项目的代码级别需要调整到14以上。
js, python 的相同语法,三引号:
public static void main(String[] args) {
var str = """
dsfds
sdfsg " > < !
dsfdsg dfsdf
""";
System.out.println(str);
}
可以看到,Java中也可以使用这样的三引号来表示字符串了,并且我们可以随意在里面使用特殊字符,包括双引号等,但是最后编译出来的结果实际上还是会变成一个之前这样使用了转义字符的字符串:
**注意:**文本块表达式在Java 15才正式开放使用,所以我们项目的代码级别需要调整到15以上。
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本instanceof加强之后,我们就不需要了,我们可以直接将student替换为模式变量:
// @Override
// public boolean equals(Object obj) {
// if(obj instanceof Student) { //首先判断是否为Student类型
// Student student = (Student) obj; //如果是,那么就类型转换
// return student.name.equals(this.name); //最后比对属性是否一样
// }
// return false;
// }
@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 main(String[] args) {
test(null, "sdf");
}
public static void test(String a, String b){
int length = a.length() + b.length(); //可能给进来的a或是b为null
System.out.println(length);
}
但是当我们在Java 14或更高版本运行时:
继类、接口、枚举、注解之后的又一新类型: "记录", 在Java 14中首次出场. 有点 Lombok 的感觉.
在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据,比如我们从数据库中查询的账户信息,最后会被映射为一个实体类:
使用起来也很方便:
并且toString也是被重写了的:
UserRecord userRecord = new UserRecord("zhangsan", 20);
System.out.println(userRecord);
UserRecord[name=zhangsan, age=20]
equals()
方法仅做成员字段之间的值比较,也是帮助我们实现好了的:
UserRecord userRecord = new UserRecord("zhangsan", 20);
System.out.println(userRecord);
UserRecord userRecord2 = new UserRecord("zhangsan", 20);
// 两个属性都是一模一样的
System.out.println(userRecord.equals(userRecord2)); //得到true
//支持实现接口,但是不支持继承,因为继承的坑位已经默认被占了
Java 17目前最新的LTS长期维护版本.
密封类型可以说是Java 17正式推出的又一重磅类型,它在Java 15首次提出并测试了两个版本。
在Java中,我们可以通过继承(extends关键字)来实现类的能力复用、扩展与增强。但有的时候,可能并不是所有的类我们都希望能够被继承。所以,我们需要对继承关系有一些限制的控制手段,而密封类的作用就是限制类的继承。
实际上在之前我们如果不希望别人继承我们的类,可以直接添加final关键字. 这样有一个缺点,如果添加了final
关键字,那么无论是谁,包括我们自己也是没办法实现继承的,但是现在我们有一个需求,只允许我们自己写的类继承A,但是不允许别人写的类继承A,这时该咋写?在Java 17之前想要实现就很麻烦。
non-sealed
主动放弃了密封特性:
密封类型有以下要求:
1. 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
2. 必须有子类继承,且不能是匿名内部类或是lambda的形式。
3. sealed写在原来final的位置,但是不能和final、non-sealed关键字同时出现,只能选择其一。
4. 继承的子类必须显式标记为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 bClass = B.class;
Class cClass = C.class;
Class dClass = D.class;
System.out.println(bClass.isSealed()); // true
System.out.println(cClass.isSealed()); // false
System.out.println(dClass.isSealed()); // false
}
以上就是 Java 9 - 17的主要新特性.