天天看點

Java 17 主要特性

作者:一捺夫

密封類正式版本

經過兩個預覽版,密封類最終确定下來了,和 Java 16 中二次預覽版在功能上沒有任何差別。

public sealed interface ConstantDesc
    permits String, Integer, Float, Long, Double,
            ClassDesc, MethodTypeDesc, DynamicConstantDesc { ... }           

密封類用于限制類或接口隻允許指定的子類來繼承或實作,适合于不希望外部擴充功能的元件來增加限制。

switch 模式比對(預覽)

switch 也可以比對執行個體的類型了,之前 16 版本中增加的模式比對隻能在條件語句 if 中使用,現在可以用在 switch 中了。

舊版本的代碼:

if (o instanceof String) {
    String s = (String)o;
    ... use s ...
}           

16 中新增的模式比對:

if (o instanceof String s) {
    ... use s ...
}           

switch 模式比對:

return switch (o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> o.toString();
};           

空值也是可以比對并進行處理的:

switch (s) {
   case null         -> System.out.println("Oops");
   case "Foo", "Bar" -> System.out.println("Great");
   default           -> System.out.println("Ok");
}           

DatagramSocket 可以直接加入多點傳播

DatagramSocket 增加了 joinGroup 和 leaveGroup 方法用于加入和離開多點傳播(multicast), 這使得 DatagramSocket 可以友善的用于多點傳播應用,在此之前隻能使用 MulticastSocket 。

DatagramSocket socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(6789)); // 綁定位址
// 加入組 228.5.6.7
InetAddress mcastaddr = InetAddress.getByName("228.5.6.7");
InetSocketAddress group = new InetSocketAddress(mcastaddr, 0);
NetworkInterface netIf = NetworkInterface.getByName("wlan0");
socket.joinGroup(group, netIf);
byte[] msgBytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
socket.receive(packet);
// ....
// 離開組
socket.leaveGroup(group, netIf);           

增強僞随機數字生成

為所有已經存在和新的僞随機數算法,提示了統一的随機數生成接口 RandomGenerator。

RandomGenerator randomGenerator = RandomGenerator.of("L32X64MixRandom");
int randomInt = randomGenerator.nextInt();
System.out.println("randomInt = " + randomInt);           

在 macos 上提供對 UserDefinedFileAttributeView 的支援

檔案系統已經在 macos 提供了擷取擴充屬性的實作,通過 UserDefinedFileAttributeView 可擷取檔案擴充屬性的視圖。

Files.getFileStore(Paths.get( " ./ " )).supportsFileAttributeView(UserDefinedFileAttributeView.class)           

在之前的版本,上面的代碼在 macos 上傳回 false,下面的代碼傳回 null。

Files.getFileAttributeView(Paths.get( " / " ), UserDefinedFileAttributeView.class)           

Ideal Graph Visualizer (IGV) 的現代化

Ideal Graph Visualizer (IGV) 是一個可以展現 C2 編譯器 just-in-time (JIT) 中間過程的工具。 要可看圖表,可在運作程式的時候添加參數 -XX:PrintIdealGraphLevel=level和-XX:PrintIdealGraphFile=filename 來生成 圖表檔案,然後導入 IGV 檢視。

PrintIdealGraphLevel 的各個等級含義:

  • 0: no output, the default
  • 1: dumps graph after parsing, before matching, and final code (also dumps graphs for failed compilations, if available)
  • 2: more detail, including after loop opts
  • 3: even more detail
  • 4: prints graph after parsing every bytecode (very slow)

也可以不生成檔案,先啟動 IGV ,JVM 啟動後會自動連接配接,通過參數 -XX:PrintIdealGraphAddress= 和 -XX:PrintIdealGraphPort= 可以自定義 IGV 的服務位址。

我電腦上裝的 openjdk 17 ,嘗試了下,沒有成功,報了下面的錯,看樣子這個功能不是給正式版本使用的。 反正目前還用不着,沒有再深入研究。

Error: VM option 'PrintIdealGraphLevel' is notproduct and is available only in debug version of VM.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.           

最後這次更新的現代化内容:

  • 支援運作在 JDK 15 上(IGV 基于 Netbeans IDE 實作,支援程度和 Netbeans IDE 有關,實測 jdk8 和 jdk17 都能把 IGV 跑起來)
  • 更快的,基于 maven 的建構系統
  • 穩定化塊形成、組移除和節點跟蹤
  • 預設過濾器中的着色和節點分類更直覺
  • 以更自然的行為方式快速排名節點的搜尋

javadoc 在錯誤資訊中增加來源的詳情

當 JavaDoc 在源檔案中報告錯誤時,将會顯示問題源碼的行數,并使用脫字元标(^)記在行中的具體位置,類似于編譯診斷的資訊。

javadoc 新增頁面 ”New API“,優化頁面 "Deprecated"

JavaDoc 再走可以生成一個概括一個 API 變化的摘要頁面。通過添加參數 --since 啟用, 指令會查找 @since 标簽來生成新頁面,通過 --since-label 參數可以設定新頁面的标題。 在廢棄條目的摘要頁面,條目可以按照廢棄的版本進行分組檢視。

外部函數和記憶體 API(孵化功能)

提供了一套 api 可以調用外部庫和程序,而不必使用脆弱和危險的 JNI。外部調用的的相關功能 之前的版本就有添加,到 17 仍然是孵化版本,目前仍然不成熟。

Console 字元集 api

java.io.Console 增加新方法用來取字元集的。

Console console = System.console();
if(console!=null){
    System.out.println(console.charset());
}           

JFR 增加反序列化事件

JFR(JDK Flight Recorder )新增加事件 jdk.Deserialization,當運作中的程式試圖對一個對象進行反序列化時觸發, 預設關閉。

針對上下文環境的反序列化過濾器

通過類 ObjectInputFilter.Config 可以設定或擷取 JVM 範圍内的反序列化過濾器,相關方法定義:

/**
 * Return the JVM-wide deserialization filter factory.
 *
 * @return the JVM-wide serialization filter factory; non-null
 */
public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();

/**
 * Set the JVM-wide deserialization filter factory.
 *
 * The filter factory is a function of two parameters, the current filter
 * and the next filter, that returns the filter to be used for the stream.
 *
 * @param filterFactory the serialization filter factory to set as the
 * JVM-wide filter factory; not null
 */
public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);           

反序列化過濾器機制可以讓你防止反序列化帶來的漏洞。如果不想設定全局的,隻是簡單的自定義,可以通過 ObjectInputStream 的方法 setObjectInputFilter 進行設定。

下面是一個 BinaryOperator<ObjectInputFilter> 實作的例子。

public class FilterInThread implements BinaryOperator<ObjectInputFilter> {

    // ThreadLocal to hold the serial filter to be applied
    private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();

    // Construct a FilterInThread deserialization filter factory.
    public FilterInThread() {}

    /**
     * The filter factory, which is invoked every time a new ObjectInputStream
     * is created.  If a per-stream filter is already set then it returns a
     * filter that combines the results of invoking each filter.
     *
     * @param curr the current filter on the stream
     * @param next a per stream filter
     * @return the selected filter
     */
    public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
        if (curr == null) {
            // Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
            var filter = filterThreadLocal.get();
            if (filter != null) {
                // Prepend a filter to assert that all classes have been Allowed or Rejected
                filter = ObjectInputFilter.rejectUndecidedClass(filter);
            }
            if (next != null) {
                // Prepend the next filter to the thread filter, if any
                // Initially this is the static JVM-wide filter passed from the OIS constructor
                // Append the filter to reject all UNDECIDED results
                filter = ObjectInputFilter.merge(next, filter);
                filter = ObjectInputFilter.rejectUndecidedClass(filter);
            }
            return filter;
        } else {
            // Called from OIS.setObjectInputFilter with a current filter and a stream-specific filter.
            // The curr filter already incorporates the thread filter and static JVM-wide filter
            // and rejection of undecided classes
            // If there is a stream-specific filter prepend it and a filter to recheck for undecided
            if (next != null) {
                next = ObjectInputFilter.merge(next, curr);
                next = ObjectInputFilter.rejectUndecidedClass(next);
                return next;
            }
            return curr;
        }
    }

    /**
     * Apply the filter and invoke the runnable.
     *
     * @param filter the serial filter to apply to every deserialization in the thread
     * @param runnable a Runnable to invoke
     */
    public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
        var prevFilter = filterThreadLocal.get();
        try {
            filterThreadLocal.set(filter);
            runnable.run();
        } finally {
            filterThreadLocal.set(prevFilter);
        }
    }
}           

增加新的系統屬性擷取原生字元集名稱

新增加 native.encoding 來擷取系統的原生字元集名稱。

// windows 系統預設設定會輸出 GBK
System.out.println(System.getProperty("native.encoding"));           

添加 java.time.InstantSource

引入新接口 java.time.InstantSource ,新接口是 java.time.Clock 的抽象,隻關注目前時刻, 不涉及時區,java.time.Clock 必須要指定時區。

InstantSource system = InstantSource.system();
// 等價于 System.currentTimeMillis()
System.out.println(system.millis());
InstantSource oneDayLater = InstantSource.offset(system, Duration.ofDays(1L));
System.out.println(oneDayLater.millis());           

十六進制格式化和解析

引入工具類 java.util.HexFormat 提供了十六進制和格式化和解析。

System.out.println(HexFormat.fromHexDigits("5e"));//94
// 擷取字元在十六進制中表示的資料
System.out.println(HexFormat.fromHexDigit('A'));// 10
byte b = 94;
System.out.println(HexFormat.of().toHexDigits(b));// 5e
System.out.println(HexFormat.fromHexDigit('X'));// 異常 :not a hexadecimal digit           

實驗性功能編輯器黑洞( Compiler Blackholes )

這個功能對底層的基準測試很有用,它可以防止死代碼( dead-code )被消除。

例如下面這段代碼:

public void test(int x,int y){
    int z = x + y;
}           

由于方法沒有傳回結果,方法内的代碼會被消除,編譯後的位元組碼檔案中并沒有方法内的内容,也就不會在運作時執行。 編譯器的優化可以提升程式的性能,但是如果你想要做基準測試,這可能不是你想要的效果,因為觀察測試的過程比程式執行的結果重要。那麼現在可以通過指令行參數啟動編譯黑洞,來避免這個優化:-XX:CompileCommand=blackhole,<method>。

JMH 已經可以自動檢查,當指令可用時,使用這個功能。JMH 是一款由 OpenJDK 開發的基準測試工具,在之前都是通過 Blackhole::consume() 方法消除死代碼,這種方式産生副作用,會産生額外的消耗。

基于 vtable 的 CHA 實作

引入一個新的類層級分析(Class Hierarchy Analysis ,簡寫 CHA)實作,以提升對抽象和預設方法的處理為特色, 改進 JIT 編譯器的内聯決策,新實作取代了原來的版本成為預設實作。

為了幫助診斷錯誤是否與新版本有關,原來的實作可以通過參數來切換:-XX:+UnlockDiagnosticVMOptions -XX:-UseVtableBasedCHA ,原本的實作會在未來删除。

新版本的實作在查詢目标方法時會依賴于 vtable 的資訊。vtable 是 c++ 中的虛拟表(virtual table), 用于查找函數,解決函數通過動态或延遲綁定的方式調用的問題。

macOS/AArch64 架構移植

Jdk 終于可以支援 Mac m1 了,從 17 版本開始,可以下載下傳官方的 macOS/AArch64 版本 JDK。 想使用更早的版本,隻能尋找第三方的 JDK 了,比如 Azul JDK。如果你使用了 Idea 來做 Java 開發, Idea 很早就支援了 m1 版本的 Mac,使用内置 JDK 即可,非常友善。

統一日志支援異步日志重新整理

為了避免線程在使用統一日志(Unified Logging)造成令人讨厭的延遲,使用者可以請求統一日志系統 使用異步操作模式,使用參數 -Xlog:async 開啟。在異步模式下,所有的日志都會以消息隊列的形式存放于一個緩沖區内, 緩沖區耗盡,則會丢棄入隊的消息。通過參數 -XX:AsyncLogBufferSize= 可設定緩沖區大小。

Keytool -genkeypair 指令支援指定簽名者

keytool 工具新增加 -signer 和 -signerkeypass 兩個參數到 -genkeypair 指令中。-genkeypair 選項指定 簽名者 PrivateKeyEntry(ketstore 中的私鑰條目) 别名,-signerkeypass 指定用于保護簽名者私鑰的密碼。 新特性主要是為了支援密鑰協商算法來生成公鑰。

SunJCE 對 AES 加密提供了 KW 和 KWP 模式

SunJCE 進一步增強,支援了 RFC 3394 和 RFC 5649,也是以增加了 KW 和 KWP 兩種塊密碼模式。

新的 SunPKCS11 配置項

SunPKCS11 provider 添加了新的配置屬性,可以更好的控制本地資源的使用。為了更好的 管理本地資源,新加選項用來控制本地引用的清理頻率以及登出後是否銷毀本地的 PKCS11 令牌。

三個新的配置項:

  1. destroyTokenAfterLogout ,(boolean 類型,預設 false),設定為 true ,當 java.security.AuthProvider.logout() 調用,底層的令牌對象會被銷毀,資源也會被釋放。
  2. cleaner.shortInterval,(integer 類型,預設 2000,機關毫秒),忙碌期間清理本地引用的周期。
  3. cleaner.longInterval,(integer類型, 預設 60000, 機關毫秒),非忙碌期間清理本地引用的周期。

要使用 SunPKCS11 需要先安裝 PKCS11 動态庫到本地,然後進行配置。下面是簡單的示例:

pkcs.cfg 配置:

library = C:\mypkcs11.dll
name = SunPKCS11-FooAccelerator
destroyTokenAfterLogout = true
cleaner.shortInterval = 2000
cleaner.longInterval = 60000           

name 和 library 必填,library 是本地 pkcs11 的連結庫位置。

AuthProvider sunpkcs11 = (AuthProvider) Security.getProvider("SunPKCS11");
sunpkcs11.configure("C:\\pkcs.cfg");           

官方參考文檔:PKCS#11 Reference Guide

javadoc 包摘要頁增加關聯的包(Related Packages)

Package Summary 中增加 Related Packages 章節,相關包的的合集是根據命名來決定的, 可能會包含以下的部分:

  1. 父包
  2. 兄弟包
  3. 子包
Java 17 主要特性

JDK 内部強封裝

将所有 JDK 内部元素強封裝,除了 critical internal APIs,critical internal APIs 如下:

  • sun.misc.{Signal,SignalHandler}
  • sun.misc.Unsafe (The functionality of many of the methods in this class is available via variable handles (JEP 193).)
  • sun.reflect.Reflection::getCallerClass(int) (The functionality of this method is available in the stack-walking API defined by JEP 259.)
  • sun.reflect.ReflectionFactory
  • com.sun.nio.file.{ExtendedCopyOption,ExtendedOpenOption, ExtendedWatchEventModifier,SensitivityWatchEventModifier}

也是以 --illegal-access 選項被廢棄了,如果使用了會抛出警告,并且也不會生效。 對于已經存在的代碼,必須要使用 jdk 内部的類、方法或成員變量,仍然可以使用 --add-opens 來通路 jdk 内部 api ,讓代碼繼續運作。

其它移除功能

  • 将 Telia Company's Sonera Class2 證書從 cacerts 信任庫中移除
  • sun.misc.Unsafe::defineAnonymousClass 被删除,可使用 MethodHandles 相關 api 替代
  • 删除 RMI Activation,從 java8 開始就已經廢棄了部分,15 中标記為 deprecated,在 17 中删除
  • 移除實驗性質的 AOT 和 JIT 編譯器