Java 11(LTS)
概述
Java 11 在 2018 年 9 月 25 日正式发布,与 Java 9 和 Java 10 这两个被称为”功能性的版本”不同,Java 11 提供长期支持服务(LTS, Long-Term-Support),将作为从发布日期开始的 Java 平台的默认支持版本,并且会提供技术支持直至 2023 年 9 月,对应的补丁和安全警告等支持将持续至 2026 年。
Java 11 是继 Java 8 之后的第二个 LTS 版本。自Java 11起,Oracle JDK 将不再免费用于商业用途。
您可以在开发阶段使用它,但若想将其用于商业环境,您需要购买许可证。 如果不这样做,可能会面临来自Oracle的法律诉讼。
Java 10 是最后一个可以免费使用的 Oracle JDK。
Oracle 从 2019 年 1 月起停止 Java 8 支持。您需要为更多支持付费。您可以继续使用Java 8,但是不会再收到任何补丁或安全更新。
从Java 11开始,Oracle 将不再为任何单一 Java 版本提供免费的长期支持 (LTS)。
尽管Oracle JDK不再免费,但您始终可以从 Oracle 或其他提供商(例如 AdoptOpenJDK、Azul、IBM、Red Hat 等)下载 Open JDK 版本。在我看来,除非您正在寻找有兴趣的企业级使用 支付支持费用,您可以使用 OpenJDK 并在必要时对其进行升级。
商用收费
Java的商用收费始于Java 8 ,Oracle JDK宣布从2019年1月份开始对Java SE 8之后的版本开始进行商用收费,确切的说是8u201/202之后的版本。如果你用 Java 开发的功能如果是用作商业用途的,如果还不想花钱购买的话,能免费使用的最新版本是8u201/202。当然如果是个人客户端或者个人开发者可以免费试用 Oracle JDK所有的版本。另外Oracle官方其实还提供了一个完全免费开源的JDK版本—— Open JDK,这是免费使用的。
决定是否收费首先得看JDK使用的是什么协议?
- BCL协议:即
Oracle Binary Code License Agreement,协议规定你可以使用JDK,但是不能进行修改。私用和商用都可以,但是JDK中的某些商业特性,是需要付费才可以使用的。 - OTN协议:即
Oracle Technology Network License Agreement,目前新发布的JDK用的都是这个协议,可以私用,商用需要付费。
版本说明
- Java 8以下版本,仍可免费使用。
- Java 8 (LTS) 部分免费,
8u201/202及之前的版本是免费的,之后的商用收费。 - Java 9 是免费的,过渡版本且不再更新不建议使用。
- Java 10 是免费的,过渡版本且不再更新不建议使用。
- Java 11 (LTS) 开始
Oracle JDK商用收费,同时提供免费版的Open JDK,下载地址。
补充说明
- 从2019年4月开始,
Oracle JDK 8更新将具有商业使用限制。 - Java 10.0.2(2018年7月),Java 8u201/202(2019年1月)是 Oracle 发布的最后的免费的Oracle JDK。
- 从 Java 11 开始,每6个月发布一个新的版本,即 11 → 12 → 13 → 14。
- 从 Java 11 开始,每3个月更新一个补丁版本,即 11.0.1 → 11.0.2。
- 从 Java 11(2018年9月,LTS)开始,Oracle将提供基于
GPLv2 + CPE协议的OpenJDK,Oracle JDK构建和OpenJDK构建将基本相同。 - 免费的JDK有 OpenJDK 、 AdoptOpenJDK 、 Amazon Corretto 、 Azul Zulu 、 BellSoft 、 IBM 、 jClarity 、 Red Hat 、 SAP 、 阿里巴巴 Dragonwell等。
变动说明
官网:
更多参考:
docs.oracle.com/en/java/jav… 更多版本:docs.oracle.com/en/java/jav…
1、JEP 323:局部变量类型推导的升级
Java 10 引入了 局部变量类型推导这一关键特性。类型推导允许使用关键字 var 作为局部变量的类型而不是实际类型,编译器根据分配给变量的值推导出类型。这一改进简化了代码编写、节省了开发者的工作时间,因为不再需要显式声明局部变量的类型,而是使用关键字 var替代。
在 Java 10 中,var声明局部变量还有下面几个限制:
- 定义的时候必须初始化
- 只能用于定义局部变量
- 不能用于定义成员变量、方法参数、返回类型
- 不能在 Lambda 表达式中使用
其中不能在 Lambda参数使用var,这条限制在Java 11中得到了改进。Java 11 允许开发者在Lambda表达式中使用var进行参数声明。
下面我们回顾一下var声明局部变量的使用,如果只想看更新部分,可以直接看下面总结的第5部分:
可以使用关键字 var 声明局部变量,示例如下:
try {
URL url = new URL("http://www.oracle.com/");
URLConnection conn = url.openConnection();
Reader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
您可以通过使用var标识符声明局部变量来重写此示例。变量的类型是从上下文中推导出来的:
try {
var url = new URL("http://www.oracle.com/");
var conn = url.openConnection();
var reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
显然下面的代码更简练。
var是一个保留的类型名称,而不是关键字,这意味着使用var作为变量、方法或包名称的现有代码不受影响。但是,使用var作为类或接口名称的代码会受到影响,需要重命名该类或接口。
var可用于以下类型的变量:
- 带有初始值设定项的局部变量声明:
var list = new ArrayList<String>(); // 推导为 ArrayList<String>
var stream = list.stream(); // 推导为 Stream<String>
var path = Paths.get(fileName); // 推导为 Path
var bytes = Files.readAllBytes(path); // 推导为 bytes[]
- 增加循环中声明索引变量:
List<String> myList = Arrays.asList("礼", "乐", "射", "御", "书", "数"); //六艺
for (var element : myList) {...} // 推导为 String
- 传统for循环中声明索引变量:
for (var counter = 0; counter < 10; counter++) {...} // 推导为 int
- try with resources变量:
try (var input = new FileInputStream("唐宋八大家.txt")) {...} // 推导为 FileInputStream
- 隐式类型lambda表达式的形参声明:
BiFunction<Integer, Integer, Integer> = (a, b) -> a + b;
在JDK11及更高版本中,可以使用var标识符声明隐式类型lambda表达式的每个形参:
(var a, var b) -> a + b;
因此,隐式类型lambda表达式中的形参声明的语法与局部变量声明的语法一致;将var标识符应用于隐式类型lambda表达式中的每个形参,其效果与不使用var完全相同。
既然如此,那为什么我们要对lambda参数使用var?
与局部变量声明的语法保持一致的一个好处是:可以为lambda参数添加注解:
Function<String, String> msgWarpper = m -> "'" + m + "'";
Function<String, String> msgWarpper1 = (@NotNull var m) -> "'" + m + "'";
System.out.println(msgWarpper.apply("李商隐")); //结果 '李商隐'
System.out.println(msgWarpper1.apply("杜牧")); // 结果 '杜牧'
System.out.println(msgWarpper1.apply(null)); // 结果 'null' 这里notnull单独使用没有效果,还要加上其他处理才能生效
需要注意:不能在隐式类型的lambda表达式中混合隐式声明的参数和var声明的形参,也不能在显式类型的lambda表达式中混合var声明的正式参数和显式类型。以下示例是不允许的:
(var x, y) -> x.process(y) // 不能混合var声明和隐式类型声明
(var x, int y) -> x.process(y) // 不能混合var声明和显示类型声明
局部变量类型推导原则
局部变量声明可以通过消除冗余信息来提高代码的可读性。然而,它也会通过省略有用的信息来降低代码的可读性。因此,要根据实际情况来判断是否应该使用此功能;关于何时应该使用和不应该使用,没有严格的规则。
局部变量声明不是孤立存在的;周围的代码可能会影响甚至压倒var声明的效果。局部变量类型推导:风格指南 检查了周围代码对var声明的影响,解释了显式和隐式类型声明之间如何权衡,可以为有效使用var声明提供指导。
2、新增String API
String 是 Java 中最常用的一个类,Java 11 为了更好地处理字符串,新增了几个新的 API:
| 方法名 | 描述 |
|---|---|
isBlank() | 检查字符串是否为空或仅包含空白字符 |
lines() | 分割获取字符串流(Stream) |
strip() | 去除字符串首尾的空白字符 |
repeat(n) | 复制字符串 |
isBlank()
用于判断字符串是否为空或者只包含空白字符。
"".isBlank(); // true
" ".isBlank(); // true
" \n\t ".isBlank() // true
"大音希声".isBlank(); // false
该方法是对isEmpty()的补充,isEmpty() 只能检查字符串长度是否为0,而isBlank()则回进一步检查了字符串的内容是否也只包含空白字符。
lines()
该方法返回一个流(Stream),该流由字符串中的行组成,使用行结束符作为分隔符。
"象形\n指事\n会意\n形声\n转注\n假借".lines().forEach(System.out::println);
执行结果:
象形
指事
会意
形声
转注
假借
该方法可以处理不同平台上的换行符,无论是\n,\r还是\r\n。
strip()
去除字符串首尾的空白字符。如果字符串中间有空白字符,它们不会被去掉。
System.out.println(" 汉字六书 ".strip());
执行结果:
汉字六书
在原来老的 Java 版本中有一个方法:trim(),该方法也可以去掉空格,但是它只能去掉除半角空格。看下面例子
// 全角
System.out.printf("strip[全角] :%s%n", "[" + " 汉字 六书 ".strip() + "]"); // [汉字 六书]
System.out.printf("trim[全角] :%s%n", "[" + " 汉字 六书 ".trim() + "]"); // [ 汉字 六书 ]
// Unicode空白字符
char ch = '\u2000'; // Unicode空白字符
System.out.printf("strip[Unicode空白字符]:%s%n", "[" + (ch + "汉字 六书" + ch).strip() + "]");
System.out.printf("trim[Unicode空白字符] :%s%n", "[" + (ch + "汉字 六书" + ch).trim() + "]");
// 半角
System.out.printf("strip[半角] :%s%n", "[" + " 汉字 六书 ".strip() + "]");
System.out.printf("trim[半角] :%s%n", "[" + " 汉字 六书 ".trim() + "]");
// tab键、换行符
System.out.printf("strip[tab键、换行符] :%s%n", "[" + "\n汉字 六书\t".strip() + "]");
System.out.printf("trim[tab键、换行符] :%s%n", "[" + "\n汉字 六书\t".trim() + "]");
运行结果:
strip[全角] :[汉字 六书]
trim[全角] :[ 汉字 六书 ]
strip[Unicode空白字符]:[汉字 六书]
trim[Unicode空白字符] :[ 汉字 六书 ]
strip[半角] :[汉字 六书]
trim[半角] :[汉字 六书]
strip[tab键、换行符] :[汉字 六书]
trim[tab键、换行符] :[汉字 六书]
trim()早在Java早期就存在,当时Unicode还没有完全发展到我们今天广泛使用的标准。
trim()方法移除字符串两侧的空白字符(空格、tab键、换行符),无法删除掉Unicode空白字符,但用Character.isWhitespace方法可以判断出来
所以:
trim()只移除ASCII字符集中定义的空白字符。strip()移除所有Unicode字符集中定义的空白字符。
strip() 还有两个方法:
stripLeading():仅移除开头的空白字符。stripTrailing():仅移除末尾的空白字符。
这里就不具体介绍了
repeat()
将字符串重复指定的次数,并将这些重复的字符串连接为一个新的字符串。
System.out.println("汉字六书;".repeat(6));
运行结果:
汉字六书;汉字六书;汉字六书;汉字六书;汉字六书;汉字六书;
注意
- 如果传入的次数为0,结果是一个空字符串。
- 如果次数小于0,会抛出
IllegalArgumentException。
3、新增Files API
Java 11 对java.nio.file.Files新增了三个 API:
readString():读取文件内容writeString():写入文件内容isSameFile():比较两个路径是否指向文件系统中的同一个文件
这三个 API 简化了我们对文件的操作,使得操作文件变得更加简便了。
writeString()
该方法可以将字符串写入文件中。
public static Path writeString(Path path, CharSequence csq, OpenOption... options) throws IOException
public static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption... options) throws IOException
第一个方法等效于writeString(path, csq, StandardCharsets.UTF_8, options)。
@Test
public void writeStringTest() throws IOException {
Path path = Paths.get("file.txt");
String content = "“谷”原来是指有壳的粮食;像稻、稷(jì计,即谷子)、黍(亦称黄米)等外面都有一层壳,所以叫做谷。谷字的音,就是从壳的音来的。五谷原是中国古代所称的五种谷物,后泛指粮食类作物。关于五谷主要有两种说法,主流的是稻(俗称水稻、大米)、黍(shǔ,俗称黄米)、稷(jì,又称粟,俗称小米)、麦(俗称小麦,制作面粉用)、菽(俗称大豆)。因为有的地方气候干旱,不利于水稻的种植,因此有将麻(俗称麻子)代替稻,作为五谷之一。";
Files.writeString(path, content);
}
readString()
该方法可以读取文件的全部内容,返回一个字符串。有2个重载方法。
第一种方法将文件中的所有内容读取为字符串,并使用 UTF-8 字符集将其从字节解码为字符。该方法可确保在所有内容均已关闭时关闭文件读取或抛出I/O错误或其他运行时异常。 第二种方法与仅使用指定字符集的方法相同。
请注意,这些方法不适用于读取非常大的文件。否则,如果文件过大,例如,文件可能会抛出 OutOfMemoryError。
public static String readString(Path path) throws IOException
public static String readString(Path path, Charset cs) throws IOException
第一个方法等效于readString(path,StandardCharsets.UTF_8)。
@Test
public void readStringTest() throws IOException {
Path path = Paths.get("file.txt");
System.out.println(path.toAbsolutePath()); // 打印路径
String content = Files.readString(path);
System.out.println(content);
}
isSameFile()
该方法用于比较两个路径是否指向文件系统中的同一个文件,可以用来检查符号链接或文件快捷方式是否指向同一个文件。
@Test
public void isSameFileTest() throws IOException {
Path path1 = Paths.get("file_01.txt");
Path path2 = Paths.get("file_02.txt");
boolean result = Files.isSameFile(path1,path2);
System.out.println(result);
}
文件路径相同返回 true,否则返回 false。
4、JEP 330:运行单文件源码程序
增强java启动器以运行作为java源代码的单个文件提供的程序,包括通过“shebang”文件和相关技术在脚本中使用。
这个功能尤其适合初学Java编程的新手,或想迅速体验编写简短代码的用户。此功能与 jshell 一起,将成为Java初学者的重要助手。对于有经验的开发者,它可以用来简化代码验证和测试工具。
这项特性允许开发者直接运行一个包含源代码的单个Java文件,而无需事先将其编译成字节码。
- 此功能允许使用 Java 解释器直接执行 Java 源代码。
- 源代码在内存中编译,然后由解释器执行。但是有个限制条件是所有相关的类必须定义在同一个 Java 文件中。
我们可以使用 Java 命令直接运行一个单文件的Java源代码,比如我们有一个名为 LaunchSourceTest.java 的文件,内容如下:
public class LaunchSourceTest {
public static void main(String[] args) {
System.out.println("天地玄黄,宇宙洪荒!");
}
}
我们可以使用下面命令来执行这个 Java 文件
cd F:\workspacenew2023-04\mylab\mytest\src\test\java\com\ld\mytest\test\java11[java文件所在目录]
# windows下修改编码,改为utf-8编码,因为上面使用了中文,不改编码会乱码。
F:\workspacenew2023-04\mylab\mytest\src\test\java\com\ld\mytest\test\java11>chcp 65001
java LaunchSourceTest.java
执行结果:
天地玄黄,宇宙洪荒!
传参
java java文件名 [参数……] #参数可以有多个,空格分隔,如果参数本身待空格,用引号包裹
public class LaunchSourceTest {
public static void main(String[] args) {
if (args == null || args.length < 1) {
System.err.println("谁说的?");
System.exit(1);
}
System.out.println(String.format("%s说:天地玄黄,宇宙洪荒!", args[0]));
}
}
运行
java LaunchSourceTest.java kk
执行结果:
kk说:天地玄黄,宇宙洪荒!
如果不传参
运行
java LaunchSourceTest.java
执行结果:
谁说的?
上面我们文件后缀是java,如果后缀不是java,那么jvm会用文件名+.class搜寻文件,找不到就会报错。当然也开业使用—source选项指定源文件:
java --source version[jdk版本] [java文件名]
# 如
java --source 11 LaunchSourceTest.java kk
一个文件中多个类
上面提到所有需要执行的代码是在同一个java文件中即可,这同一个文件中可以存在多个类。
下面这个java文件,在IDE中是编译不通过的,因为一个java文件中只能有一个public类。但是用java命令是可以运行的。
public class LaunchSourceTest {
public static void main(String[] args) {
BaijiaXing.write();
}
}
// 下面类的public去掉,在IDE中就不报错了
public class BaijiaXing {
public static void write() {
System.out.println("赵钱孙李,周吴郑王!");
}
public static void main(String[] args) {
System.out.println("第二个类的main方法");
}
}
运行
java LaunchSourceTest.java
执行结果:
赵钱孙李,周吴郑王!
执行的是第一个类的main方法,后面的不会执行。
Shebang文件
shebang,Linux中常见的符号。#!,其中she指代#,而bang指代!。
Shebang文件是Unix系统中常见的文件,以#!/path/executable(#!开头后面跟一个可执行文件路径)作为文件的开头第一行,shebang通常出现在类Unix系统的脚本中第一行,作为前两个字符,用于命令解释器声明,如:
#!/bin/sh
#!/bin/bash
如果脚本文件中没有#!这一行或者指定的不是一个可执行文件,那么执行时会默认采用当前Shell去解释这个脚本(即:$SHELL环境变量)。
我们来创建一个shebang文件:
在文件第一行增加
#!/path/java --source version
#!/home/ubuntu/jdk/jdk-11.0.12/bin/java --source 11
public class LaunchSourceTest {
public static void main(String[] args) {
BaijiaXing.write();
}
}
// 下面类的public去掉,在IDE中就不报错了
public class BaijiaXing {
public static void write() {
System.out.println("赵钱孙李,周吴郑王!");
}
public static void main(String[] args) {
System.out.println("第二个类的main方法");
}
}
将上面内容为LaunchSourceTest的文件中。按下面的方式来运行:
~$ cd /home/ubuntu/workdir[java文件所在路径]
~$ ./LaunchSourceTest
赵钱孙李,周吴郑王! #执行结果
5、JEP 321:标准 HTTP Client 升级
在Java 9的时候,引入了一个新的HTTP客户端API(HttpClient)作为作为实验特性,到了 Java 11,则被标准化并正式成为Java标准库的一部分。经过前面两个版本中的孵化,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。新版 Java 中,Http Client 的包名由 jdk.incubator.http 改为java.net.http,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义。
全新的 HTTP 客户端 API 具有如下几个优势:
- HTTP/2支持:全新的 HttpClient 支持 HTTP/2 协议的所有特性,包括同步和异步编程模型。
- WebSocket支持:支持WebSocket,允许建立持久的连接,并进行全双工通信。
- 同步和异步:提供了同步和异步的两种模式。这意味着我们既可以等待HTTP响应,也可以使用回调机制处理HTTP响应。
- 链式调用:新的HttpClient 允许链式调用,使得构建和发送请求变得更简单。
- 更好的错误处理机制:新的HttpClient 提供了更好的错误处理机制,当HTTP请求失败时,可以通过异常机制更清晰地了解到发生了什么。
下面是一个使用新的 HttpClient 示例:
@Test
public void testHttpRequest() {
try {
URI uri = URI.create("http://www.baidu.com/"); // new URI("https://www.baidu.com");
HttpClient httpClient = HttpClient.newBuilder()
// 超时时间10秒,
.connectTimeout(Duration.of(10, ChronoUnit.SECONDS))
//HTTP版本为HTTP/2
.version(HttpClient.Version.HTTP_2).build();
// 默认get请求,想用post,增加.POST()
HttpRequest request = HttpRequest.newBuilder(uri).build();
// 1、同步调用
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
int statusCode = response.statusCode();
String body = response.body();
System.out.println(statusCode);
System.out.println(body);
// 2、异步调用
// 使用 sendAsync() 方法发送异步请求,发送请求后会立刻返回 CompletableFuture,使用CompletableFuture中的方法来设置异步处理器
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) // 第二个参数,这里是使用了返回字符串,可以返回数组、行、输入流等
// 申请结果处理,这里是返回body
.thenApply(HttpResponse::body)
// 把结果打印
.thenAccept(System.out::println)
//
.join();
} catch (Exception e) {
e.printStackTrace();
}
}
异步处理如果想直接处理结果可以改成如下方式:
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) // 第二个参数,这里是使用了返回字符串,可以返回数组,按行返回,或者返回流等
// 这里少了thenApply
// 把结果打印
.thenAccept(response -> {
System.out.println(response.statusCode());
System.out.println(response.body());
})
//
.join();
6、移除的APIs、工具、容器
移除 Java EE和CORBA模块
Java 10
变动说明
官网:
更多参考:
1、JEP 286:局部变量的类型推导
目前为止,局部变量类型推导是 Java 10 中最受人瞩目的特性。在经过激烈的争议之后,此特性才被引入 Java 10 中,它允许编译器推导局部变量的类型,而不是要求程序员明确指定变量类型。局部变量类型推导有助于缩短代码,并提高可读性。
简单类型
以String类型为例,示例代码:
// 无类型推导
String name = "四书五经";
// 使用类型推导
var name = "四书五经";
可以看到,唯一的区别在于使用了保留类型名称 var 。使用右侧的表达式,编译器可以推导出变量 name 的类型为 String 。
复杂类型
以List为例,示例代码:
// 无类型推导
List<String> list = new ArrayList<>();
// 使用类型推导,注意右侧泛型也可以不加,不加就相当于泛型为Object
var list = new ArrayList<String>();
编译器可以推导出变量 list 的类型为 ArrayList 。
在早期Java引入泛型的时候,我们申明具体类型的时候需要这样写(等号两边都需要):
List<String> list = new ArrayList<String>();
Map<String, String> map = new HashMap<String,String>();
后来在Java 7推出之后,简化为(只需要在左边申明类型即可):
List<String> list = new ArrayList<>();
Map<String, String> map = new HashMap<>();
而这次Java 10中,对类型的推导进一步优化,只需要这样即可(类型在等号右边决定):
var list = new ArrayList<String>();
var map = new HashMap<String, String>();
对于var的使用还有几点要注意的:
- 定义的时候必须初始化
- 只能用于定义局部变量
- 不能用于定义成员变量、方法参数、返回类型
- 每次只能定义一个变量,不能复合声明变量
- 大量使用var可能会导致代码的可读性下降,因此尽量在情况明显的情况下才使用它。
顾名思义,局部变量类型推导特性只能用于局部变量。不能将它用于定义实例或类变量,也不能在方法参数或返回类型中使用它。但是,在可以从迭代器推导类型的经典和增强的 for 循环中, 可以 使用 var 。
for(var user : users){
}
for(var i = 0; i < 10; i++){
}
var的问题
1. var 让类型变得模糊
前面已经提到var 有一些好处,但是另一方面,它也有可能让类型变得模糊。
var result = userService.search();
上面的代码中,我们必须猜测返回类型。让代码更加难以维护。
2. var 无法与 lambda 结合使用
在用于 lambda 表达式时,类型推导的效果不是很好,主要由于缺乏可供编译器使用的类型信息。清单 8 中的 lambda 表达式不会执行编译。
示例代码:
Function<String, String> msgWarpper = m -> "'" + m + "'";
var msgWarpper1 = m -> "'" + m + "'"; //这行编译会报错,需要使用上一行的形式
在上述代码中,右侧表达式中没有足够的类型信息供编译器推导变量类型。Lambda 语句必须始终声明一个显式类型。
Java官方表示,它不能用于以下几个地方:
- 方法参数
- 构造函数参数
- 方法返回类型
- 字段
- 捕获表达式(或任何其他类型的变量声明)
2、删除的工具
Java 10 删除了一些工具:
- 命令行工具
javah,但您可以使用javac -h代替它。 - 命令行选项
-X:prof,但您可以使用jmap工具来访问探查信息。 policytool。
一些由于 Java 1.2 被永久删除而被标记为弃用的 API。这些 API 包括 java.lang.SecurityManager.inCheck 字段和以下方法:
java.lang.SecurityManager.classDepth(java.lang.String)java.lang.SecurityManager.classLoaderDepth()java.lang.SecurityManager.currentClassLoader()java.lang.SecurityManager.currentLoadedClass()java.lang.SecurityManager.getInCheck()java.lang.SecurityManager.inClass(java.lang.String)java.lang.SecurityManager.inClassLoader()java.lang.Runtime.getLocalizedInputStream(java.io.InputStream)java.lang.Runtime.getLocalizedOutputStream(java.io.OutputStream)
3、弃用的API
JDK 10也弃用了一些 API。 java.security.acl 包被标记为弃用, java.security 包中的各种相关类( Certificate 、 Identity 、 IdentityScope 、 Singer 、 auth.Policy )也是如此。此外, javax.management.remote.rmi.RMIConnectorServer 类中的 CREDENTIAL_TYPES 也被标记为弃用。 java.io.FileInputStream 和 java.io.FileOutputStream 中的 finalize() 方法已被标记为弃用。 java.util.zip.Deflater / Inflater / ZipFile 类中的 finalize() 方法也是如此。
Java 9
变动说明
官网:
更多内容:docs.oracle.com/javase/9/
1、JEP 222:交互式编程环境Jshell
交互式编程环境是一种让程序员能够即时输入代码并立即获得反馈的开发环境。每输入一行代码,系统就会立刻执行并显示结果,使得用户可以快速验证想法、进行简单计算等操作。尽管这种环境不太适合处理复杂的工程需求,但在快速验证和简单计算等场景下非常实用。尽管其他高级编程语言(比如Python)早就拥有了交互式编程环境,Java直到Java 9才正式推出了类似的工具。
下面就来一起学习下,这个Java中的交互式编程环境Jshell。
使用
启动Jshell
打开终端,然后执行命令:jshell,执行效果如下:我这里以java11为例
F:\Program Files\Java\jdk-11.0.12\bin>jshell
| 欢迎使用 JShell -- 版本 11.0.12
| 要大致了解该版本, 请键入: /help intro
jshell>
执行计算
在jshell中可以快速的执行计算操作并获得结果,比如这样:
jshell> 1+1
$1 ==> 2
# 注意上面的返回,这是里一个临时变量。下面是定义变量计算的情况,注意返回结果的不同。
jshell> int c = 1+1
c ==> 2
jshell> 2*3
$2 ==> 6
# 注意char类型和String类型的区别
jshell> 'a'+'b'
$3 ==> 195
jshell> "a"+"b"
$4 ==> "ab"
定义变量
在jshell中也可以定义变量与方法:
jshell> int a=1, b=2;
a ==> 1
b ==> 2
jshell> a+b
$7 ==> 3
定义方法
在jshell中也可以函数来封装操作,比如下面就是一个定义求和函数并调用它的例子:
jshell> int sum(int a, int b){
...> return a + b;
...> }
| 已创建 方法 sum(int,int)
jshell> sum(1,2)
$10 ==> 3
定义类
既然变量和方法都可以定义,那么合理推测,在jshell中,定义类,也是可以的,上示例:
jshell> public class MyCls {
...> public int a;
...> public int b;
...>
...> public int add(){
...> return a+b;
...> }
...>
...> public int mul(){
...> return a*b;
...> }
...>
...> }
| 已创建 类 MyCls
jshell> MyCls m = new MyCls()
m ==> MyCls@39c0f4a
jshell> m.a = 2
$21 ==> 2
jshell> m.b = 3
$22 ==> 3
# 相加
jshell> m.add()
$23 ==> 5
# 相乘
jshell> m.mul()
$24 ==> 6
帮助命令:/help
关于jshell常用命令,我们可以通过/help来查看
jshell> /help
| 键入 Java 语言表达式, 语句或声明。
| 或者键入以下命令之一:
| /list [<名称或 id>|-all|-start]
| 列出您键入的源
| /edit <名称或 id>
| 编辑源条目
| /drop <名称或 id>
| 删除源条目
| /save [-all|-history|-start] <文件>
| 将片段源保存到文件
| /open <file>
| 打开文件作为源输入
| /vars [<名称或 id>|-all|-start]
| 列出已声明变量及其值
| /methods [<名称或 id>|-all|-start]
| 列出已声明方法及其签名
| /types [<名称或 id>|-all|-start]
| 列出类型声明
| /imports
| 列出导入的项
| /exit [<integer-expression-snippet>]
| 退出 jshell 工具
| /env [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>] ...
| 查看或更改评估上下文
| /reset [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>]...
| 重置 jshell 工具
| /reload [-restore] [-quiet] [-class-path <路径>] [-module-path <路径>]...
| 重置和重放相关历史记录 -- 当前历史记录或上一个历史记录 (-restore)
| /history [-all]
| 您键入的内容的历史记录
| /help [<command>|<subject>]
| 获取有关使用 jshell 工具的信息
| /set editor|start|feedback|mode|prompt|truncation|format ...
| 设置配置信息
| /? [<command>|<subject>]
| 获取有关使用 jshell 工具的信息
| /!
| 重新运行上一个片段 -- 请参阅 /help rerun
| /<id>
| 按 ID 或 ID 范围重新运行片段 -- 参见 /help rerun
| /-<n>
| 重新运行以前的第 n 个片段 -- 请参阅 /help rerun
|
| 有关详细信息, 请键入 '/help', 后跟
| 命令或主题的名称。
| 例如 '/help /list' 或 '/help intro'。主题:
|
| intro
| jshell 工具的简介
| keys
| 类似 readline 的输入编辑的说明
| id
| 片段 ID 以及如何使用它们的说明
| shortcuts
| 片段和命令输入提示, 信息访问以及
| 自动代码生成的按键说明
| context
| /env /reload 和 /reset 的评估上下文选项的说明
| rerun
| 重新评估以前输入片段的方法的说明
查看定义的变量:/vars
jshell> /vars
| int $1 = 2
| int $2 = 6
| int $3 = 195
| String $4 = "ab"
| int a = 1
| int b = 2
| int $7 = 3
| int $10 = 3
| int c = 2
| int $15 = 2
| MyCls m = MyCls@39c0f4a
| int $21 = 2
| int $22 = 3
| int $23 = 5
| int $24 = 6
查看定义的函数:/methods
jshell> /methods
| int sum(int,int)
查看定义的类:/types
jshell> /types
| class MyCls
列出输入源条目:/list
这个命令看到之前在jshell中输入的所有执行内容:
jshell> /list
1 : 1+1
2 : 2*3
3 : 'a'+'b'
4 : "a"+"b"
5 : int a=1, b=2;
6 : int a=1, b=2;
7 : a+b
9 : int sum(int a,int b){
return a+b;
}
10 : sum(1,2)
12 : int c = 1+1;
19 : public class MyCls {
public int a;
public int b;
public int add(){
return a+b;
}
public int mul(){
return a*b;
}
}
20 : MyCls m = new MyCls();
21 : m.a = 2
22 : m.b = 3
23 : m.add()
24 : m.mul()
左侧的数字为条目id,可以利用该id,进行编辑和删除操作
编辑源条目:/edit
通过/list列出了所有输入的条目信息,下面我们通过/edit修改一下第一条内容:
jshell> /edit 1
此时会弹出修改框:
修改完成后,点击Accept。此时可以看到jshell窗口的输出结果发生了变化。
退出,点击Exit按钮。
删除源条目:/drop
/drop命令可以用来删除某个源条目,我们删除6条int a=1, b=2;
jshell> /drop 6
| 已删除 变量 b
执行后可以看到提示,实际上这种多个变量定义到一行上的,只会删除最后一个变量,可以试一下a还在,b已经没有了。
jshell> b
| 错误:
| 找不到符号
| 符号: 变量 b
| 位置: 类
| b
| ^
jshell> a
a ==> 1
保存文件:/save
如果想把这次编辑的内容保存下来,以便下次继续使用,可以通过/save来保存到文件里,比如这样:
# 路径是jdk的bin目录
jshell> /save my1.txt
打开文件:/open
当我们换了jshell环境后,可以通过打开之前保存的文件来快速还原之前的执行内容,比如:
jshell> /open my1.txt
重置jshell:/reset
当我们要换一个内容编写的时候,需要清空之前执行的条目(清空/list的内容),这个时候就可以这样来实现:
jshell> /reset
| 正在重置状态。
查看引入的包:/imports
jshell> /imports
| import java.io.*
| import java.math.*
| import java.net.*
| import java.nio.file.*
| import java.util.*
| import java.util.concurrent.*
| import java.util.function.*
| import java.util.prefs.*
| import java.util.regex.*
| import java.util.stream.*
退出jshell:/exit
jshell> /exit
| 再见
2、JEP 269:集合工厂方法
用过谷歌Guava类库的知道,Guava提供了创建不可变集合的静态工厂方法,而且能够推断泛型,举个例子:
List<String> list = ImmutableList.of("诗", "书", "礼", "易", "春秋");
// ImmutableSet、ImmutableMap也有类似的方法
在 Java 9 之前,要构建一个不可变集合往往需要经历若干繁琐的步骤,如初始化集合、添加元素以及对其进行封装处理。Java 9 通过引入专门的不可变集合(包括List、Set、Map)特性,旨在简化这一流程,提供了一个既简洁又安全的方法来创建不可变集合,确保了集合内容的不变性和线程安全性。
其内容包括:
-
List.of():创建一个不可变的 List,可以传递任意数量(与其他工具类注意区分)的元素,注意里面不能有null。 -
Set.of():创建一个不可变的 Set,可以传递任意数量(与其他工具类注意区分)的元素,注意里面不能有null。 -
Map.of()和Map.ofEntries():用于创建一个不可变的 Map。Map.of()可以直接传递键值对,而Map.ofEntries()可以通过Map.entry(k, v)创建条目,注意key和value都能为null。以上方法创建的集合为不可变对象,不能添加、修改、删除。
一般写法
以往我们创建一些集合的时候,通常是这样写的:
// Set
Set<String> set = new HashSet<>();
set.add("秦");
set.add("汉");
set.add("唐");
set.add("宋");
// 如果想不可变
set = Collections.unmodifiableSet(set);
// List
List<String> list = new ArrayList<>();
list.add("诗");
list.add("书");
list.add("礼");
list.add("易");
list.add("春秋");
// 如果想不可变
list = Collections.unmodifiableList(list);
Java8的写法
在Java 8中可以使用Stream API简化一下:
Set<String> set = Stream.of("秦", "汉", "唐", "宋").collect(Collectors.toSet());
List<String> list = Stream.of("诗", "书", "礼", "易", "春秋").collect(Collectors.toList());
// 如果想不可变
Set<String> set = Collections.unmodifiableSet(Stream.of("秦", "汉", "唐", "宋").collect(Collectors.toSet()));
List<String> list = Collections.unmodifiableList(Stream.of("诗", "书", "礼", "易", "春秋").collect(Collectors.toList()));
Java9的写法
到了Java 9,这一操作变的更为简单,只需要这样:
Set<String> set = Set.of("秦", "汉", "唐", "宋");
List<String> list = List.of("诗", "书", "礼", "易", "春秋");
Map类型可以这样写:
Map<String, String> map = Map.of("诗仙", "李白", "诗圣", "杜甫", "诗佛", "王维");
需要注意的是,Map.of的参数是key和value成对出现的,所以参数数量一定是偶数:
Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...
List.of与asList的区别
我们也可以使用asList来快速创建集合
List<String> list2 = Arrays.asList(new String[] { "诗", "书", "礼", "易", "春秋" });
看起来看List.of和Arrays.asList比较类似,那他们之间除了长的不一样外,还有什么区别吗?
Java 9中推出List.of创建的是不可变集合,而Arrays.asList是可变集合(长度不可变,值可变)List.of和Arrays.asList都不允许add和remove元素,但Arrays.asList可以调用set更改值,而List.of不可以,会报java.lang.UnsupportedOperationException异常List.of中不允许有null值,Arrays.asList中可以有null值
3、JEP 213:细微调整
接口支持私有方法
接口进化过程:
Java 8 支持接口的默认方法和静态方法》Java 9 可定义 private 私有方法。
接口中私有方法的特点:
- 私有方法不能定义为抽象的
- 私有方法只能在接口内部使用,实现该接口的类或其他外部类无法调用这些方法
- 私有方法不会继承给接口的子接口,每个接口都必须自己定义自己的私有方法。
1、定义接口
public interface IWriter {
/**
* 测试抽象方法,需要被子类实现。
*/
void skill();
/**
* 测试默认实现方法。如果大部分子类都是相同的实现,可直接使用默认方法实现。
*/
default void write() {
System.out.println("=====这是一个默认方法====");
System.out.println("创作ing…………");
// 调用私有方法
thinking();
// 调用私有静态方法。非静态方法可以调用静态方法,反之不行。
hideTag();
System.out.println("创作完成…………");
}
/**
* 测试私有方法
*/
private void thinking() {
System.out.println("=====这是一个私有方法=====");
System.out.println("构思ing…………");
}
/**
* 测试静态方法。
*/
static void tag() {
System.out.println("=====这是一个静态方法=====");
System.out.println("标签是:文人");
// 调用私有静态方法
hideTag();
}
/**
* 测试私有静态方法
*/
private static void hideTag() {
System.out.println("=====这是一个私有静态方法=====");
System.out.println("隐藏标签:读书人");
}
}
2、定义接口实现
public class TangWriter implements IWriter {
@Override
public void skill() {
System.out.println("=====这是一个抽象方法的实现====");
System.out.println("技能:唐诗");
}
}
3、测试类
public class MyTest {
public static void main(String[] args) {
IWriter writer = new TangWriter();
// 调用抽象方法的实现
writer.skill();
// 调用默认方法
writer.write();
// 调用静态方法
writer.tag();
}
}
try-with-resources 优化
在Java 7 中引入了try-with-resources功能,保证了每个声明了的资源在语句结束的时候都会被关闭。
任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。
在Java 7中需要这样写:
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(System.in);
BufferedInputStream bufferedInputStream1 = new BufferedInputStream(System.in)) {
// do something
} catch (IOException e) {
e.printStackTrace();
}
而到了Java 9无需为 try-with-resource 临时声明变量,简化为:
BufferedInputStream bufferedInputStream = new BufferedInputStream(System.in);
BufferedInputStream bufferedInputStream1 = new BufferedInputStream(System.in);
//变量 定义在try外边,在 try 中包裹,也会自动释放
try (bufferedInputStream;
bufferedInputStream1) {
// do something
} catch (IOException e) {
e.printStackTrace();
}
不支持下划线(_)作为标识符
在早期版本的 Java 中,下划线( _ )已用作标识符或创建 变量名称。从 Java 9 开始,下划线字符是一个保留关键字,不能用作标识符或变量名。如果我们使用单个下划线作为标识符,程序将无法编译并抛出编译时错误,因为现在它是一个 关键字,并且在 Java 9 或更高版本中不能用作变量名称。
以下代码java9之前(比如java8)是可以的,java9及其后续版本,编译不通过
int _ = 50;
// 解决办法,多加一个下划线,int __ = 50;
允许匿名类使用 <>(省略泛型的标记)
如果推断类型的参数类型是可忽略的,则允许使用匿名类的菱形。
在 Java 7 中引入的钻石操作符简化了泛型实例的创建,但它不能用于匿名内部类。由于这个限制,开发者不得不在使用匿名内部类时指定泛型参数,这增加了代码的冗余和复杂性。
在 Java 9 中,钻石操作符得到了改进,允许与匿名内部类配合使用。现在,当我们实例化一个具有泛型参数的匿名内部类时,无需显式指定这些参数,因为 Java 编译器能够利用上下文信息自动推导出正确的类型。
下面的代码在 Java 8 及以下版本是无法编译通过的,在 Java 9 及以上版本中可以:
public class MyTest {
class InnerC<T> {}
@Test
public void testJep213() {
InnerC<String> stringValue = new InnerC<>() {
}; // 这里java8会报错
}
}
4、Stream API 增强
Stream API 是在Java 8 中闪亮登场的,Java 9 对 Stream API 做了一些增强。
ofNullable
Stream<T> ofNullable(T t) 返回包含单个元素的顺序Stream ,如果非空,否则返回空Stream 。
这就意味着我们可以使用 Stream.ofNullable() 方法来快速创建一个只包含非空元素的 Stream,且无需担心处理 null 值的边界情况。比如,我们有一个 List 包含一些可能为 null 的字符串,我们可以使用 Stream.ofNullable() 来创建一个只包含非空字符串的 Stream:
public static void main(String[] args) {
Stream<String> stream1 = Stream.ofNullable(null);
System.out.println("Stream记录:");
stream1.forEach(System.out::println);
Stream<List<String>> stream2 = Stream.ofNullable(List.of("大学", "中庸", "论语", "孟子"));
System.out.println("Stream记录:");
stream2.forEach(System.out::println);
// 有null值,flatMap将集合扁平化处理
List<String> list = Arrays.asList("大学", null, "中庸", "论语", null, "孟子");
list.stream().flatMap(Stream::ofNullable).forEach(System.out::println);
}
iterate
Java 8 中的 iterate() 用于创建一个无限流,其元素由给定的初始值和一个生成下一个元素的函数产生,为了终止流我们需要使用一些限制性的函数来操作,例如 limit():
Stream.iterate(1,v -> v * 2).limit(10).forEach(System.out::println);
在 Java 9 中为了限制流的长度,增加了断言谓词,方法定义如下:
Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
这个是用来生成有限流的新迭代实现。
seed初始种子值hasNext用来判断何时结束流,如果hasNext返回true,则next函数就会继续生成下一个元素;一旦hasNext返回false,序列生成将停止。next函数用来计算下一个元素值。
示例:
//大约等于5时停止计算,返回结果 0 1 2 3 4
Stream.iterate(0, i -> i < 5, i -> i + 1).forEach(System.out::println);
等同于传统的:
for (int i = 0; i < 5; ++i) {
System.out.println(i);
}
takeWhile
Stream.takeWhile(Predicate) 从流的开头开始,选择满足断言Predicate条件的所有元素,直到找到第一个不满足条件的元素,一旦找到该元素,将会终止选择元素(即使未断言中的元素有满足条件的),并返回一个新的流,其中包含了前缀元素。
// 输出结果 2, 4, 6, 8。遇到10不满足条件终止,即使后面有2这个符合条件的元素也不再处理。
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 1 < x && x < 10).forEach(System.out::println);
// 返回结果为空。这里如果将1改为3,那么返回结果为空,因为第一个元素就不满足条件,后面的直接跳过
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 3 < x && x < 10).forEach(System.out::println);
// 输出结果 2, 4。遇到6不满足条件终止,即使后面有2这个符合条件的元素也不再处理。
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 1 < x && x < 6).forEach(System.out::println);
dropWhile
这个API和takeWhile机制类似,也用来筛选Stream中的元素。
- 方法会从流的开头开始,跳过满足断言条件的所有元素,直到找到第一个不满足条件的元素。一旦找到第一个不满足条件的元素,
dropWhile()将停止跳过元素,并返回一个新的流,其中包含了剩余的元素。 - 如果流的所有元素都满足谓词条件,那么返回一个空流。
// 输出结果 10 3。大于1小于10的被跳过,知道遇到第一个不满足条件的10,后面的直接返回不再处理。
Stream.of(2, 4, 6, 8, 10, 3).dropWhile(x -> 1 < x && x < 10).forEach(System.out::println);
5、Optional 的增强
Optional增加了三个有用的API。
stream()Optional现在可以转Stream。ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)如果有值了怎么消费,没有值了怎么消费。or(Supplier<? extends Optional<? extends T>> supplier)该方法接受一个Supplier作为参数,如果当前Optional有值就返回当前Optional,当前Optional为空时,将执行Supplier获取一个新的Optional对象。可以用于提供默认值。
ifPresentOrElse
Java 8 有一个 ifPresent(),它用于在 Optional 包含非空值时执行指定的操作,Java 9 对其进行了改进增加了一个else处理。
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
它有两个参数:
action:是一个Consumer函数式接口,用于在Optional包含非空值时执行的操作。emptyAction:是一个 Runnable 函数式接口,用于在Optional为空时执行的操作,通常是一些默认操作或错误处理。
private void testJava9(User user1) {
System.err.println("=====java 9=====");
Optional.ofNullable(user1).ifPresentOrElse(u -> {
System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
}, () -> {
System.err.println("user 对象为null");
});
}
private void testJava8(User user1) {
System.err.println("=====java 8=====");
Optional.ofNullable(user1).ifPresent(u -> {
System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
});
}
private void testCommon(User user1) {
System.err.println("=====直接判断=====");
if (user1 != null) {
System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
} else {
System.err.println("user 对象为null");
}
}
@Test
public void testOptionalIfPresent() {
User user1 = null;
testCommon(user1);
testJava8(user1);
testJava9(user1);
System.err.println();
System.err.println();
System.err.println("*****赋值之后*****");
user1 = User.builder().name("kevin").build();
testCommon(user1);
testJava8(user1);
testJava9(user1);
}
结果
=====直接判断=====
user 对象为null
=====java 8=====
=====java 9=====
user 对象为null
*****赋值之后*****
=====直接判断=====
姓名kevin 年龄null
=====java 8=====
姓名kevin 年龄null
=====java 9=====
姓名kevin 年龄null
or
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
Optional.or()接受一个 Supplier作为参数,如果当前Optional有值就返回当前Optional,当前Optional 为空时,将执行Supplier获取一个新的Optional 对象。可以用于提供默认值。
@Test
public void testOptionalOr() {
// 创建一个 optional
Optional<String> optional = Optional.of("曹植");
// 创建一个 Supplier(生产者)
Supplier<Optional<String>> supplierString = () -> Optional.of("未设置");
// 输出 作者: 曹植
System.out.println("作者: " + optional.or(supplierString).get());
// option置空后。输出的是:作者: 未设置
optional = Optional.empty();
System.out.println("作者: " + optional.or(supplierString).get());
}
stream
public Stream<T> stream()
stream() 方法使得 Optional 对象能够被转换成一个流(Stream)。这样就可以利用 Stream 的各种功能来处理 Optional 中的值。
@Test
public void testOptionalStream() {
List<Optional<String>> list = List.of(
Optional.of("曹操"),
Optional.of("曹植"),
Optional.empty(),
Optional.of("曹丕"),
Optional.empty());
list.stream().flatMap(Optional::stream).filter(o -> o.equals("曹植")).map(val -> val + " —— 建安七子之一").forEach(System.out::println);
}
6、JEP 110:HTTP2客户端
定义一个新的 HTTP 客户端 API 来实现 HTTP/2 和 WebSocket,并且可以替换旧的HttpURLConnectionAPI。Java以前原生的确实难用,所以诞生了Apache Http Components 、OkHttp等好用的客户端。
@Test
public void testHttpRequest() {
try {
URI uri = new URI("https://www.baidu.com");
HttpRequest httpRequest = HttpRequest.newBuilder(uri).header("Content-Type", "*/*").GET().build();
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.of(10, ChronoUnit.SECONDS))
.version(HttpClient.Version.HTTP_2).build();
HttpResponse<String> response = httpClient.send(httpRequest, BodyHandlers.ofString());
int statusCode = response.statusCode();
String body = response.body();
System.out.println(statusCode);
System.out.println(body);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
7、JEP 266:CompletableFuture增强
CompletableFuture 是 Java 8 中引入用于处理异步编程的核心类,它引入了一种基于 Future 的编程模型,允许我们以更加直观的方式执行异步操作,并处理它们的结果或异常。
但是在实际使用过程中,发现 CompletableFuture 还有一些改进空间,所以 Java 9 对它做了一些增强,主要内容包括:
- 新的工厂方法
- 支持延迟执行和超时(timeout)机制
- 支持子类化
支持超时机制
如果执行超时, orTimeout() 方法直接抛出了一个异常,而 completeOnTimeout() 方法则是返回默认值
// 允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,将抛出 TimeoutException 异常
public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)
// 允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,则返回默认值value
public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit)
示例:
@Test
public void testTimeout() {
// 如果执行超时, orTimeout() 方法直接抛出了一个异常,而 completeOnTimeout() 方法则是返回默认值
try {
// orTimeout() 超时抛出了一个异常java.util.concurrent.TimeoutException
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(this::longLongAfter).orTimeout(1, TimeUnit.SECONDS);
future.get(); // 显式等待超时
} catch (Exception e) {
System.out.println(e);
}
try {
int defaultValue = 1987;
// completeOnTimeout() 超时返回默认值
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(this::longLongAfter).completeOnTimeout(defaultValue, 1,
TimeUnit.SECONDS);
Integer result = future.get(); // 显式等待超时
System.out.println(result);
} catch (Exception e) {
System.out.println(e);
}
}
// 这是一个超时方法
private Integer longLongAfter() {
try {
Thread.sleep(Integer.MAX_VALUE); // 一直睡,就是为了超时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 666;
}
执行结果:
java.util.concurrent.ExecutionException: java.util.concurrent.TimeoutException
1987
支持延迟执行
CompletableFuture 类通过 delayedExecutor() 方法提供了对延迟执行的支持,该方法负责生成一个具有延后执行功能的 Executor,使得任务可以在未来指定的时间点才开始执行。
public static Executor delayedExecutor(long delay, TimeUnit unit)
示例:
@Test
public void testDelay() {
System.out.println(LocalDateTime.now());
// 创建一个延迟执行的Executor.5秒后执行
Executor delayedExecutor = CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS);
// 使用延迟的Executor执行一个简单任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println(LocalDateTime.now());
System.out.println("任务延迟后执行...");
}, delayedExecutor);
// 等待异步任务完成
future.join();
}
执行结果:
2024-02-20T16:07:00.314069600
2024-02-20T16:07:05.322898
可以看到相隔5秒。
8、JEP 102:Process API
在 Java 9 之前,Java 在进程控制方面的功能较为匮乏,获取系统进程的详尽信息及管理这些进程具有一定的难度。这迫使开发者不得不依赖特定于平台的解决方案来执行这些操作,从而影响了代码的跨平台兼容性和便捷性。
为了提升在操作系统级别管理和控制进程的便捷性和效率,Java 9 推出了新的 Process API。这一举措旨在增强对操作系统进程的控制和管理,同时确保跨不同操作系统的行为一致性。新 API 的主要功能如下:
- 增强的 Process 类:Java 9 增强了
Process类,提供了更多方法来管理和控制进程。 - ProcessHandle 接口:引入了
ProcessHandle接口,它提供了获取进程的 PID(进程标识符)、父进程、子进程、进程状态等信息的能力。 - 流式 API:利用流式 API,可以更方便地处理进程的信息和状态。
下面是获取本地所有进程的相关信息:
@Test(priority = 1) // 不指定顺序时默认按字母顺序执行
public void testStream() {
ProcessHandle.allProcesses() // 获取所有进程
.forEach(processHandle -> {
System.out.printf("进程ID: %s, 命令: %s, 启动时间: %s, 用户: %s%n", //
processHandle.pid(), // 获取进程ID
processHandle.info().command().orElse("未知"), // 获取进程的命令信息
processHandle.info().startInstant().map(i -> i.toString()).orElse("未知"), // 获取进程的启动时间
processHandle.info().user().orElse("未知")); // 获取运行进程的用户
});
}
执行结果(部分):
进程ID: 2796, 命令: C:\Program Files (x86)\Notepad++\notepad++.exe, 启动时间: 2024-02-20T00:46:59.797Z, 用户: lk-202304051138\LD_001
进程ID: 18516, 命令: C:\Program Files\Typora\Typora.exe, 启动时间: 2024-02-20T00:52:34.161Z, 用户: lk-202304051138\LD_001
进程ID: 22920, 命令: C:\Windows\explorer.exe, 启动时间: 2024-02-20T01:02:48.523Z, 用户: lk-202304051138\LD_001
进程ID: 24468, 命令: C:\Program Files\Mozilla Firefox\firefox.exe, 启动时间: 2024-02-20T01:50:04.012Z, 用户: lk-202304051138\LD_001
进程ID: 24972, 命令: C:\Windows\ImmersiveControlPanel\SystemSettings.exe, 启动时间: 2024-02-20T01:57:29.345Z, 用户: lk-202304051138\LD_001
进程ID: 18072, 命令: C:\Program Files\WindowsApps\Microsoft.Windows.Photos_2023.10030.27002.0_x64__8wekyb3d8bbwe\Microsoft.Photos.exe, 启动时间: 2024-02-20T02:07:29.693Z, 用户: lk-202304051138\LD_001
进程ID: 1452, 命令: C:\Windows\System32\RuntimeBroker.exe, 启动时间: 2024-02-20T02:07:42.743Z, 用户: lk-202304051138\LD_001
进程ID: 22752, 命令: C:\Program Files (x86)\WXWork\4.1.20.6015\updated_web\WXWorkWeb.exe, 启动时间: 2024-02-20T03:11:06.533Z, 用户: lk-202304051138\LD_001
进程ID: 29060, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChat.exe, 启动时间: 2024-02-20T04:07:25.283Z, 用户: lk-202304051138\LD_001
进程ID: 29372, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChatWeb.exe, 启动时间: 2024-02-20T04:07:27.310Z, 用户: lk-202304051138\LD_001
进程ID: 30340, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChatApp.exe, 启动时间: 2024-02-20T04:08:03.707Z, 用户: lk-202304051138\LD_001
进程ID: 28952, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 18232, 命令: C:\Program Files (x86)\Tencent\QQPCMgr\13.10.21936.216\QMUsbGuard.exe, 启动时间: 2024-02-20T04:31:27.884Z, 用户: lk-202304051138\LD_001
进程ID: 14000, 命令: C:\Users\LD_001\AppData\Local\SogouExplorer\SogouExplorer.exe, 启动时间: 2024-02-20T04:44:55.455Z, 用户: lk-202304051138\LD_001
进程ID: 28584, 命令: F:\eclipse-2023-12\eclipse.exe, 启动时间: 2024-02-20T05:40:41.543Z, 用户: lk-202304051138\LD_001
进程ID: 30864, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 33500, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 31136, 命令: F:\Program Files\Java\jdk-11.0.12\bin\javaw.exe, 启动时间: 2024-02-20T05:46:38.871Z, 用户: lk-202304051138\LD_001
9、JEP 259:Stack-Walking API
提供一个堆栈遍历API,允许轻松筛选和延迟访问堆栈跟踪中的信息。
API既支持在符合给定条件的处停止的搜索,也支持遍历整个堆栈。可参阅类java.lang.Stackwalker。
@Test
public void testJep213() {
System.out.println(StackWalker.getInstance().walk(s -> s.limit(5).collect(Collectors.toList())));
System.out.println(StackWalker.getInstance().walk(s -> s.collect(Collectors.toList())));
}
10、JEP 277:增强的@Deprecated注解
@Deprecated 注解用于标记过时的 API,Java 9 对其进行了改进,增加了两个的属性:
since:指明从哪个版本开始 API 被弃用。forRemoval:指出这个 API 是否计划在未来的版本中被移除。
11、JEP 261:模块化系统
设计理念非常好,但目前对于开发人员来说比较鸡肋。
模块化系统是 Java9 架构的一次重大变革,它旨在解决长期以来 Java 应用所面临的一些结构性问题,特别是在大型系统和微服务架构中。
如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。模块系统,项目代号 Jigsaw,最早于 2008 年 8 月提出,2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。
要想充分发挥模块化的优势,整个 Java 生态系统中的开发者必须遵循规范,将各自的 JAR 文件模块化。此外,在日常的开发实践中,确切地定义自己的模块以及妥善管理模块间的依赖关系,本身就是一项颇具挑战性的任务。
那么我们常用的 Spring 有没有被模块化打动,也按规范进行模块化了呢?至少到 Spring5 还没有,但是这里有一些答案:
1:Declare Spring modules with JDK 9 module metadata
SpringFramework 官方的回答: github.com/spring-proj…
机器翻译:JDK 9 的 Jigsaw 计划旨在允许将模块元数据 (module-info.java) 添加到框架和库 jar 中,同时保持它们与 JDK 8 的兼容性。让我们对 Spring Framework 5.0 的模块尽可能地这样做。然而,我们可能无法以这种方式表达我们的可选依赖安排,在这种情况下,我们可能不得不采用 “自动模块” 方法来实现 #18289 中更温和的目的。
2:Any plans for Java 9 Jigsaw (module) of Spring projects?
Oracle JDK和OpenJDK之间的差异
尽管官方已经声明了让OpenJDK和Oracle JDK二进制文件尽可能接近的目标,但截止到JDK 21两者之间仍然存在一些差异。
目前的差异是:
Oracle JDK提供了安装程序(msi、rpm、deb等),它们不仅将JDK二进制文件放置在系统中,还包含更新规则,在某些情况下还可以处理一些常见的配置,如设置常见的环境变量(如Windows中的JAVA_HOME)和建立文件关联(如使用JAVA启动.jar文件)。OpenJDK仅作为压缩档案(tar.gz或.zip)提供。Usage Logging仅在Oracle JDK中可用。Oracle JDK要求使用Java加密扩展(JCE(Java Cryptography Extension ))代码签名证书对第三方加密提供程序进行签名。OpenJDK继续允许使用未签名的第三方加密提供程序。java -version命令输出结果不同。Oracle JDK将输出java并包含LTS。Oracle生成的OpenJDK将显示OpenJDK,不包括Oracle特定的LTS标识符。Oracle JDK 17及之后的版本在Oracle No-Fee Terms and Conditions License协议下发布。OpenJDK将在GPLv2wCP下发布,并将包括GPL许可证。因此,许可证文件是不同的。Oracle JDK将在FreeType许可证下分发FreeType,而OpenJDK则将在GPLv2下分发。因此,\legal\java.desktop\freetype.md的内容将有所不同。Oracle JDK有Java cup和steam图标,而OpenJDK有Duke图标。Oracle JDK源代码包括ORACLE PROPRIETARY/CONFIDENTIAL. 使用受许可条款约束的说明(OTN(Oracle Technology Network License Agreement for Oracle Java SE )协议),OpenJDK源代码包含GPL协议。