從 2017 年開始,Java 版本更新策略從原來的每兩年一個新版本,改為每六個月一個新版本,以快速驗證新特性,推動 Java 的發展。從 《JVM Ecosystem Report 2021》 中可以看出,目前開發環境中有近半的環境使用 Java8,有近半的人轉移到了 Java11,隨著 Java17 的釋出,相信比例會有所變化。
因此,準備出一個系列,配合示例講解,闡述各個版本的新特性。
概述
相較於 Java8,Java9 沒有新增語法糖,但是其增加的特性也都是非常實用的,比如 Jigsaw 模組化、JShell、釋出-訂閱框架、GC 等。本文將快速、高層次的介紹一些新特性,完整的特性可以參加openjdk.java.net/projects/jd…
這裡需要說明一下,由於 Java9 並不是長期支援版,當前也是從現在看過去,所以筆者偷個懶,文章的示例程式碼都是在 Java11 下寫的,可能會與 Java9 中的定義有些出入,不過,這也沒啥,畢竟我們真正使用的時候還是優先考慮長期支援版。
Jigsaw 模組化
模組化是一個比較大的更新,這讓以前 All-in-One 的 Java 包拆分成幾個模組。這種模組化系統提供了類似 OSGi 框架系統的功能,比如多個模組可以獨立開發,按需引用、按需整合,最終組裝成一個完整功能。
模組具有依賴的概念,可以匯出功能 API,可以隱藏實現細節。
還有一個好處是可以實現 JVM 的按需使用,能夠減小 Java 執行包的體積,讓 JVM 在記憶體更小的裝置上執行。JVM 當時的初衷就是做硬體,也算是不忘初心了。
另外,JVM 中com.sun.*的之類的內部 API,做了更強的封閉,不在允許呼叫,提升了核心安全。
在使用的時候,我們需要在 java 程式碼的頂層目錄中定義一個module-info.java檔案,用於描述模組資訊:
module cn.howardliu.java9.modules.car {
requires cn.howardliu.java9.modules.engines;
exports cn.howardliu.java9.modules.car.handling;
}
複製程式碼
上面描述的資訊是:模組cn.howardliu.java9.modules.car需要依賴模組cn.howardliu.java9.modules.engines,並匯出模組cn.howardliu.java9.modules.car.handling。
更多的資訊可以檢視 OpenJDK 的指引 openjdk.java.net/projects/ji… Jigsaw 模組的使用,內容會貼到評論區。
全新的 HTTP 客戶端
這是一個千呼萬喚始出來的功能,終於有官方 API 可以替換老舊難用的HttpURLConnection。只不過,在 Java9 中,新版 HTTP 客戶端是放在孵化模組中(具體資訊可以檢視 openjdk.java.net/jeps/110)。
老版 HTTP 客戶端存在很多問題,大家開發的時候基本上都是使用第三方 HTTP 庫,比如 Apache HttpClient、Netty、Jetty 等。
新版 HTTP 客戶端的目標很多,畢竟這麼多珠玉在前,如果還是做成一坨,指定是要被笑死的。所以新版 HTTP 客戶端列出了 16 個目標,包括簡單易用、列印關鍵資訊、WebSocket、HTTP/2、HTTPS/TLS、良好的效能、非阻塞 API 等等。
我們先簡單的瞅瞅:
final String url = "https://postman-echo.com/get";
final HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.GET()
.build();
final HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
final HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k + ":" + v));
System.out.println(response.statusCode());
System.out.println(response.body());
複製程式碼
新版 HTTP 客戶端可以在 Java11 中正常使用了,上面的程式碼也是在 Java11 中寫的,API 是在java.net.http包中。
改進的程序 API
在 Java9 中提供的程序 API,可以控制和管理作業系統程序。也就是說,可以在程式碼中管理當前程序,甚至可以銷燬當前程序。
程序資訊
這個功能是由java.lang.ProcessHandle提供的,我們來瞅瞅怎麼用:
final ProcessHandle self = ProcessHandle.current();
final long pid = self.pid();
System.out.println("PID: " + pid);
final ProcessHandle.Info procInfo = self.info();
procInfo.arguments().ifPresent(x -> {
for (String s : x) {
System.out.println(s);
}
});
procInfo.commandLine().ifPresent(System.out::println);
procInfo.startInstant().ifPresent(System.out::println);
procInfo.totalCpuDuration().ifPresent(System.out::println);
複製程式碼
java.lang.ProcessHandle.Info中提供了豐富的程序資訊
銷燬程序
我們還可以使用java.lang.ProcessHandle#destroy方法銷燬程序,我們演示一下銷燬子程序:
ProcessHandle.current().children()
.forEach(procHandle -> {
System.out.println(procHandle.pid());
System.out.println(procHandle.destroy());
});
複製程式碼
從 Java8 之後,我們會發現 Java 提供的 API 使用了Optional、Stream等功能,**Eating your own dog food **也是比較值得學習的。
其他小改動
Java9 中還對做了對已有功能做了點改動,我們來瞅瞅都有哪些。
改進 try-with-resources
從 Java7 開始,我們可以使用try-with-resources語法自動關閉資源,所有實現了java.lang.AutoCloseable介面,可以作為資源。但是這裡會有一個限制,就是每個資源需要宣告一個新變數。
也就是這樣:
public static void tryWithResources() throws IOException {
try (FileInputStream in2 = new FileInputStream("./")) {
// do something
}
}
複製程式碼
對於這種直接使用的還算方便,但如果是需要經過一些列方法定義的呢?就得寫成下面這個樣子:
final Reader inputString = new StringReader("www.howardliu.cn 看山");
final BufferedReader br = new BufferedReader(inputString);
// 其他一些邏輯
try (BufferedReader br1 = br) {
System.out.println(br1.lines());
}
複製程式碼
在 Java9 中,如果資源是final定義的或者等同於final變數,就不用宣告新的變數名,可以直接在try-with-resources中使用:
final Reader inputString = new StringReader("www.howardliu.cn 看山");
final BufferedReader br = new BufferedReader(inputString);
// 其他一些邏輯
try (br) {
System.out.println(br.lines());
}
複製程式碼
改進鑽石運算子 (Diamond Operator)
鑽石運算子(也就是<>)是 Java7 引入的,可以簡化泛型的書寫,比如:
Map<String, List<String>> strsMap = new TreeMap<String, List<String>>();
複製程式碼
右側的TreeMap型別可以根據左側的泛型定義推斷出來,藉助鑽石運算子可以簡化為:
Map<String, List<String>> strsMap = new TreeMap<>();
複製程式碼
看山會簡潔很多,<>的寫法就是鑽石運算子 (Diamond Operator)。
但是這種寫法不適用於匿名內部類。比如有個抽象類:
abstract static class Consumer<T> {
private T content;
public Consumer(T content) {
this.content = content;
}
abstract void accept();
public T getContent() {
return content;
}
}
複製程式碼
在 Java9 之前,想要實現匿名內部類,就需要寫成:
final Consumer<Integer> intConsumer = new Consumer<Integer>(1) {
@Override
void accept() {
System.out.println(getContent());
}
};
intConsumer.accept();
final Consumer<? extends Number> numConsumer = new Consumer<Number>(BigDecimal.TEN) {
@Override
void accept() {
System.out.println(getContent());
}
};
numConsumer.accept();
final Consumer<?> objConsumer = new Consumer<Object>("看山") {
@Override
void accept() {
System.out.println(getContent());
}
};
objConsumer.accept();
複製程式碼
在 Java9 之後就可以使用鑽石運算子了:
final Consumer<Integer> intConsumer = new Consumer<>(1) {
@Override
void accept() {
System.out.println(getContent());
}
};
intConsumer.accept();
final Consumer<? extends Number> numConsumer = new Consumer<>(BigDecimal.TEN) {
@Override
void accept() {
System.out.println(getContent());
}
};
numConsumer.accept();
final Consumer<?> objConsumer = new Consumer<>("看山") {
@Override
void accept() {
System.out.println(getContent());
}
};
objConsumer.accept();
複製程式碼
私有介面方法
如果說鑽石運算子是程式碼的簡潔可讀,那介面的私有方法就是比較實用的一個擴充套件了。
在 Java8 之前,介面只能有常量和抽象方法,想要有具體的實現,就只能藉助抽象類,但是 Java 是單繼承,有很多場景會受到限制。
在 Java8 之後,介面中可以定義預設方法和靜態方法,提供了很多擴充套件。但這些方法都是public方法,是完全對外暴露的。如果有一個方法,只想在介面中使用,不想將其暴露出來,就沒有辦法了。這個問題在 Java9 中得到了解決。我們可以使用private修飾,限制其作用域。
比如:
public interface Metric {
// 常量
String NAME = "METRIC";
// 抽象方法
void info();
// 私有方法
private void append(String tag, String info) {
buildMetricInfo();
System.out.println(NAME + "[" + tag + "]:" + info);
clearMetricInfo();
}
// 預設方法
default void appendGlobal(String message) {
append("GLOBAL", message);
}
// 預設方法
default void appendDetail(String message) {
append("DETAIL", message);
}
// 私有靜態方法
private static void buildMetricInfo() {
System.out.println("build base metric");
}
// 私有靜態方法
private static void clearMetricInfo() {
System.out.println("clear base metric");
}
}
複製程式碼
JShell
JShell 就是 Java 語言提供的 REPL(Read Eval Print Loop,互動式的程式設計環境)環境。在 Python、Node 之類的語言,很早就帶有這種環境,可以很方便的執行 Java 語句,快速驗證一些語法、功能等。
$ jshell
| 歡迎使用 JShell -- 版本 13.0.9
| 要大致瞭解該版本,請鍵入:/help intro
複製程式碼
我們可以直接使用/help檢視命令
jshell> /help
| 鍵入 Java 語言表示式,語句或宣告。
| 或者鍵入以下命令之一:
| /list [<名稱或 id>|-all|-start]
| 列出您鍵入的源
| /edit <名稱或 id>
。很多的內容,鑑於篇幅,先隱藏
複製程式碼
我們看下一些簡單的操作:
jshell> "This is a test.".substring(5, 10);
$2 ==> "is a "
jshell> 3+1
$3 ==> 4
複製程式碼
也可以建立方法:
jshell> int mulitiTen(int i) { return i*10;}
| 已建立 方法 mulitiTen(int)
jshell> mulitiTen(3)
$6 ==> 30
複製程式碼
想要退出 JShell 直接輸入:
jshell> /exit
| 再見
複製程式碼
JCMD 新增子命令
jcmd是用於向本地 jvm 程序傳送診斷命令,這個命令是從 JDK7 提供的命令列工具,常用於快速定位線上環境故障。
在 JDK9 之後,提供了一些新的子命令,檢視 JVM 中載入的所有類及其繼承結構的列表。比如:
$ jcmd 22922 VM.class_hierarchy -i -s java.net.Socket
22922:
java.lang.Object/null
|--java.net.Socket/null
| implements java.io.Closeable/null (declared intf)
| implements java.lang.AutoCloseable/null (inherited intf)
| |--sun.nio.ch.SocketAdaptor/null
| | implements java.lang.AutoCloseable/null (inherited intf)
| | implements java.io.Closeable/null (inherited intf)
複製程式碼
第一個引數是程序 ID,都是針對這個程序執行診斷。我們還可以使用set_vmflag引數線上修改 JVM 引數,這種操作無需重啟 JVM 程序。
有時候還需要檢視當前程序的虛擬機器引數選項和當前值:jcmd 22922 VM.flags -all。
多解析度影象 API
在 Java9 中定義了多解析度影象 API,我們可以很容易的操作和展示不同解析度的影象了。java.awt.image.MultiResolutionImage將一組具有不同解析度的影象封裝到單個物件中。java.awt.Graphics類根據當前顯示 DPI 度量和任何應用的轉換從多解析度影象中獲取變數。
以下是多解析度影象的主要操作方法:
- Image getResolutionVariant(double destImageWidth, double destImageHeight):獲取特定解析度的影象變體-表示一張已知解析度單位為 DPI 的特定尺寸大小的邏輯影象,並且這張影象是最佳的變體。
- List<Image> getResolutionVariants():返回可讀的解析度的影象變體列表。
我們來看下應用:
final List<Image> images = List.of(
ImageIO.read(new URL("https://static.howardliu.cn/about/kanshanshuo_2.png")),
ImageIO.read(new URL("https://static.howardliu.cn/about/hellokanshan.png")),
ImageIO.read(new URL("https://static.howardliu.cn/about/evil%20coder.jpg"))
);
// 讀取所有圖片
final MultiResolutionImage multiResolutionImage = new BaseMultiResolutionImage(images.toArray(new Image[0]));
// 獲取圖片的所有解析度
final List<Image> variants = multiResolutionImage.getResolutionVariants();
System.out.println("Total number of images: " + variants.size());
for (Image img : variants) {
System.out.println(img);
}
// 根據不同尺寸獲取對應的影象解析度
Image variant1 = multiResolutionImage.getResolutionVariant(100, 100);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
100, 100, variant1.getWidth(null), variant1.getHeight(null));
Image variant2 = multiResolutionImage.getResolutionVariant(200, 200);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
200, 200, variant2.getWidth(null), variant2.getHeight(null));
Image variant3 = multiResolutionImage.getResolutionVariant(300, 300);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
300, 300, variant3.getWidth(null), variant3.getHeight(null));
Image variant4 = multiResolutionImage.getResolutionVariant(400, 400);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
400, 400, variant4.getWidth(null), variant4.getHeight(null));
Image variant5 = multiResolutionImage.getResolutionVariant(500, 500);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
500, 500, variant5.getWidth(null), variant5.getHeight(null));
複製程式碼
變數控制代碼(Variable Handles)
變數控制代碼(Variable Handles)的 API 主要是用來替代java.util.concurrent.atomic包和sun.misc.Unsafe類的部分功能,並且提供了一系列標準的記憶體屏障操作,用來更加細粒度的控制記憶體排序。一個變數控制代碼是一個變數(任何欄位、陣列元素、靜態表裡等)的型別引用,支援在不同訪問模型下對這些型別變數的訪問,包括簡單的 read/write 訪問,volatile 型別的 read/write 訪問,和 CAS(compare-and-swap) 等。
這部分內容涉及反射、內聯、併發等內容,後續會單獨介紹,文章最終會發布在 從小工到專家的 Java 進階之旅 中,敬請關注。
釋出-訂閱框架
在 Java9 中增加的java.util.concurrent.Flow支援響應式 API 的釋出-訂閱框架,他們提供在 JVM 上執行的許多非同步系統之間的互操作性。我們可以藉助SubmissionPublisher定製元件。
關於響應式 API 的內容可以先檢視 www.reactive-streams.org/的內容,後續單獨介紹,… 從小工到專家的 Java 進階之旅 中,敬請關注。怎麼感覺給自己刨了這麼多坑,得抓緊時間填坑了。
統一 JVM 日誌記錄
在這個版本中,為 JVM 的所有元件引入了一個通用的日誌系統。它提供了日誌記錄的基礎。這個功能是透過-Xlog啟動引數指定,並且定義很多標籤用來定義不同型別日誌,比如:gc(垃圾收集)、compiler(編譯)、threads(執行緒)等等。比如,我們定義debug等級的 gc 日誌,日誌儲存在gc.log檔案中:
java -Xlog:gc=debug:file=gc.log:none
複製程式碼
因為引數比較多,我們可以透過java -Xlog:help檢視具體定義引數。而且日誌配置可以透過jcmd命令動態修改,比如,我們將日誌輸出檔案修改為gc_other.log:
jcmd ${PID} VM.log output=gc_other.log what=gc
複製程式碼
新的 API
不可變集合
在 Java9 中增加的java.util.List.of()、java.util.Set.of()、java.util.Map.of()系列方法,可以一行程式碼建立不可變集合。在 Java9 之前,我們想要初始化一個有指定值的集合,需要執行一堆add或put方法,或者依賴guava框架。
而且,這些集合物件是可變的,假設我們將值傳入某個方法,我們就沒有辦法控制這些集合的值不會被修改。在 Java9 之後,我們可以藉助ImmutableCollections中的定義實現初始化一個不可變的、有初始值的集合了。如果對這些物件進行修改(新增元素、刪除元素),就會丟擲UnsupportedOperationException異常。
這裡不得不提的是,Java 開發者們也是考慮了效能,針對不同數量的集合,提供了不同的實現類:
- List12、Set12、Map1專門用於少量(List 和 Set 是 2 個,對於 Map 是 1 對)元素數量的場景
- ListN、SetN、MapN用於資料量多(List 和 Set 是超過 2 個,對於 Map 是多餘 1 對)的場景
改進的 Optional 類
Java9 中為Optional添加了三個實用方法:stream、ifPresentOrElse、or。
stream是將Optional轉為一個Stream,如果該Optional中包含值,那麼就返回包含這個值的Stream,否則返回Stream.empty()。比如,我們有一個集合,需要過濾非空資料,在 Java9 之前,寫法如下:
final List<Optional<String>> list = Arrays.asList(
Optional.empty(),
Optional.of("看山"),
Optional.empty(),
Optional.of("看山的小屋"));
final List<String> filteredList = list.stream()
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
.collect(Collectors.toList());
複製程式碼
在 Java9 之後,我們可以藉助stream方法:
final List<String> filteredListJava9 = list.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
複製程式碼
ifPresentOrElse:如果一個Optional包含值,則對其包含的值呼叫函式action,即action.accept(value),這與ifPresent方法一致;如果Optional不包含值,那會呼叫emptyAction,即emptyAction.run()。效果如下:
Optional<Integer> optional = Optional.of(1);
optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));
optional = Optional.empty();
optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));
// 輸出結果為:
// 作者:看山
// 佚名
複製程式碼
or:如果值存在,返回Optional指定的值,否則返回一個預設的值。效果如下:
Optional<String> optional1 = Optional.of("看山");
Supplier<Optional<String>> supplierString = () -> Optional.of("佚名");
optional1 = optional1.or(supplierString);
optional1.ifPresent(x -> System.out.println("作者:" + x));
optional1 = Optional.empty();
optional1 = optional1.or(supplierString);
optional1.ifPresent(x -> System.out.println("作者:" + x));
// 輸出結果為:
// 作者:看山
// 作者:佚名
複製程式碼
文末總結
本文介紹了 Java9 新增的特性,完整的特性清單可以從openjdk.java.net/projects/jd… Java8 到 Java17 的新特性系列完成後補充, 青山不改,綠水長流,我們下次見。
原文連結:https://juejin.cn/post/7061389685699903525