Java 9 揭秘(20. JDK 9中API層次的改變)


Tips
做一個終身學習的人。

Java 9

在最后一章內容中,主要介紹以下內容:

  • 下划線作為新關鍵字
  • 改進使用try-with-resources塊的語法
  • 如何在匿名類中使用<>操作符
  • 如何在接口中使用私有方法
  • 如何在私有方法上使用@SafeVarargs注解
  • 如何丟棄子進程的輸出
  • 如何在MathStrictMath類中使用新的方法
  • 如何使用Optionals流以及Optionals上的新的操作
  • 如何使用等待提示(spin-wait hints)
  • 對Time API和MatcherObjects類的增強
  • 如何比較數組和數組的一部分
  • Javadoc的增強功能以及如何使用其新的搜索功能
  • 本地桌面支持JDK 9以及如何使用它們
  • 在對象反序列化過程中如何使用全局和局部過濾器
  • 如何將數據從輸入流傳輸到輸出流以及如何復制和分片緩沖區

Java SE 9有很多小的變化。大的變化包括引入了模塊系統,HTTP/2Client API等。 本章涵蓋了對Java開發人員重要的所有更改。 每個部分涵蓋一個新的主題。 如果興趣了解特定主題,可以直接跳轉到該主題的部分。

示例的源代碼在com.jdojo.misc模塊中,其聲明如下示。

// module-info.java
module com.jdojo.misc {
    requires java.desktop;
    exports com.jdojo.misc;
}

該模塊讀取了java.desktop模塊,需要它來實現特定於平台的桌面功能。

一. 下划線成為關鍵字

在JDK 9中,下划線(_)是一個關鍵字,不能將其本身用作單個字符標識符,例如變量名稱,方法名稱,類型名稱等。但是,仍然可以使用下划線用在多個字符的標識符名稱中。 考慮下面程序。

// UnderscoreTest.java
package com.jdojo.misc;
public class UnderscoreTest {    
    public static void main(String[] args) {
        // Use an underscore as an identifier. It is a compile-time warning in JDK 8 and a
        // compile-time error in JDK 9.
        int _ = 19;
        System.out.println(_);
        // Use an underscore in multi-character identifiers. They are fine in JDK 8 and JDK 9.
        final int FINGER_COUNT = 20;
        final String _prefix = "Sha";
    }
}

在JDK 8中編譯UnderscoreTest類會產生兩個警告,用於使用下划線作為標識符,一個用於變量聲明,一個用於System.out.println()方法調用。 每次使用下划線時都會產生警告。 JDK 8生成以下兩個警告:

com.jdojo.misc\src\com\jdojo\misc\UnderscoreTest.java:8: warning: '_' used as an identifier
        int _ = 19;
            ^
  (use of '_' as an identifier might not be supported in releases after Java SE 8)
com.jdojo.misc\src\com\jdojo\misc\UnderscoreTest.java:9: warning: '_' used as an identifier
        System.out.println(_);
                           ^
  (use of '_' as an identifier might not be supported in releases after Java SE 8)
2 warnings
Compiling the UnderscoreTest class in JDK 9 generates the following two compile-time errors:
com.jdojo.misc\src\com\jdojo\misc\UnderscoreTest.java:8: error: as of release 9, '_' is a keyword, and may not be used as an identifier
        int _ = 19;
            ^
com.jdojo.misc\src\com\jdojo\misc\UnderscoreTest.java:9: error: as of release 9, '_' is a keyword, and may not be used as an identifier
        System.out.println(_);
                           ^
2 errors

JDK 9中的下划線的特殊含義是什么,在哪里使用它? 在JDK 9中,被限制不將其用作標識符。 JDK設計人員打算在未來的JDK版本中給它一個特殊的含義。 所以,等到JDK 10或11,將它看作具有特殊含義的關鍵字。

二. 改進使用try-with-resources塊的語法

JDK 7向java.lang包添加了一個AutoCloseable接口:

public interface AutoCloseable {
    void close() throws Exception;
}

JDK 7還添加了一個名為try-with-resources的新塊,可用於使用以下步驟管理AutoCloseable對象(或資源):

  • 將該資源的引用分配給塊開頭的新聲明的變量。
  • 使用塊中的資源。
  • 當塊的主體被退出時,代表資源的變量的close()方法將被自動調用。

這避免了在JDK 7之前使用finally塊編寫的樣板代碼。以下代碼片段顯示了開發人員如何管理可關閉的資源,假設存在實現AutoCloseable接口的Resource類:

/* Prior to JDK 7*/
Resource res = null;
try{
    // Create the resource
    res = new Resource();
    // Work with res here
} finally {
    try {
        if(res != null) {
            res.close();
        }
    } catch(Exception e) {
        e.printStackTrace();
    }
}

JDK 7中的try-with-resources塊大大改善了這種情況。 在JDK 7中,可以重寫以前的代碼段,如下所示:

try (Resource res = new Resource()) {
    // Work with res here
}

當控制退出try塊時,這段代碼將在res上調用close()方法。 可以在try塊中指定多個資源,每個資源以分號分隔:

try (Resource res1 = new Resource(); Resource res2 = new Resource()) {
     // Work with res1 and res2 here
}

當try塊退出時,兩個資源res1res2上close()方法將被自動調用。 資源以相反的順序關閉。 在這個例子中,將按順序調用res2.close()res1.close()

JDK 7和8要求在try-with-resources塊中聲明引用資源的變量。 如果在方法中收到資源引用作為參數,那么無法編寫如下所示的邏輯:

void useIt(Resource res) {
    // A compile-time error in JDK 7 and 8
    try(res) {
        // Work with res here
    }
}

為了規避此限制,必須聲明另一個新的變量的Resource 類型,並用參數值初始化它。 以下代碼段顯示了這種方法。 它聲明一個新的參考變量res1,當try塊退出時,將調用close()方法:

void useIt(Resource res) {        
    try(Resource res1 = res) {
        // Work with res1 here
    }
}

JDK 9刪除了該限制,必須使用try-with-resource塊為要管理的資源聲明新變量。 現在,可以使用try-with-resources塊來管理final或有效的final變量來引用資源。 如果使用final關鍵字顯式聲明變量,則該變量為final

// res is explicitly final
final Resource res = new Resource();

如果變量在初始化之后從未更改,則該變量實際上是final的。 在下面的代碼片段中,盡管res變量未被聲明為final,但是res變量是有效的。 它被初始化,從不再次更改。

void doSomething() {
    // res is effectively final
    Resource res = new Resource();
    res.useMe();
}

在JDK 9中,可以這樣寫:

Resource res = new Resource();
try (res) {
    // Work with res here
}

如果有多個資源要使用try-with-resources塊來管理,可以這樣做:

Resource res1 = new Resource();
Resource res2 = new Resource();
try (res1; res2) {
    // Use res1 and res2 here
}

也可以將JDK 8和JDK 9方法混合在同一個資源塊中。 以下代碼片段在try-with-resources塊中使用兩個預先聲明的有效的final變量和一個新聲明的變量:

Resource res1 = new Resource();
Resource res2 = new Resource();
try (res1; res2; Resource res3 = new Resource()) {
    // Use res1, res2, and res3 here
}

由於在JDK 7中,在資源塊中聲明的變量是隱含的final的。 以下代碼片段明確聲明了這樣一個final變量:

Resource res1 = new Resource();
Resource res2 = new Resource();
// Declare res3 explicitly final
try (res1; res2; final Resource res3 = new Resource()) {
    // Use res1, res2, and res3 here            
}

我們來看一個完整的例子。 JDK中有幾個類是AutoCloseable,例如java.io包中的InputStreamOutputStream類。 下面包含實現AutoCloseable接口的Resource類的代碼。 Resource類的對象可以作為由try-with-resources管理的資源。 id實例變量用於跟蹤資源。 構造方法和其他方法在調用時簡單地打印消息。

// Resource.java
package com.jdojo.misc;
public class Resource implements AutoCloseable {    
    private final long id;
    public Resource(long id) {        
        this.id = id;                
        System.out.printf("Created resource %d.%n", this.id);
    }
    public void useIt() {    
        System.out.printf("Using resource %d.%n", this.id);        
    }
    @Override
    public void close() {
        System.out.printf("Closing resource %d.%n", this.id);
    }
}

下面包含了ResourceTest類的代碼,它顯示了如何使用JDK 9的新功能,該功能允許使用final或有效的final變量來引用這些資源,並使用try-with-resources塊來管理資源。

// ResourceTest.java
package com.jdojo.misc;
public class ResourceTest {
     public static void main(String[] args) {
         Resource r1 = new Resource(1);
         Resource r2 = new Resource(2);
         try(r1; r2) {
             r1.useIt();
             r2.useIt();
             r2.useIt();
         }
         useResource(new Resource(3));
     }
     public static void useResource(Resource res) {
         try(res; Resource res4 = new Resource(4)) {
             res.useIt();
             res4.useIt();
         }
     }
}

輸出結果為:

Created resource 1.
Created resource 2.
Using resource 1.
Using resource 2.
Using resource 2.
Closing resource 2.
Closing resource 1.
Created resource 3.
Created resource 4.
Using resource 3.
Using resource 4.
Closing resource 4.
Closing resource 3.

三. 如何在匿名類中使用<>操作符

JDK 7引入了一個鑽石操作符(<>),用於調用泛型類的構造方法,只要編譯器可以推斷通用類型即可。 以下兩個語句是一樣的;第二個使用鑽石操作符:

// Specify the generic type explicitly
List<String> list1 = new ArrayList<String>();
// The compiler infers ArrayList<> as ArrayList<String>
List<String> list2 = new ArrayList<>();

創建匿名類時,JDK 7不允許使用鑽石操作符。 以下代碼片段使用帶有鑽石操作符的匿名類來創建Callable<V>接口的實例:

// A compile-time error in JDK 7 and 8
Callable<Integer> c = new Callable<>() {
    @Override
    public Integer call() {
        return 100;
    }
};

上面語句在JDK 7和8中生成以下錯誤:

error: cannot infer type arguments for Callable<V>
        Callable<Integer> c = new Callable<>() {
                                          ^
  reason: cannot use '<>' with anonymous inner classes
  where V is a type-variable:
    V extends Object declared in interface Callable
1 error

可以通過指定通用類型代替鑽石運算符來解決此錯誤:

// Works in JDK 7 and 8
Callable<Integer> c = new Callable<Integer>() {
    @Override
    public Integer call() {
        return 100;
    }
};

JDK 9就添加了對匿名類中的鑽石操作符的支持,只要推斷的類型是可表示的。 不能使用具有匿名類的鑽石操作符 —— 即使在JDK 9中,如果推斷的類型是不可表示的。 Java編譯器使用許多不能用Java程序編寫的類型。 可以用Java程序編寫的類型稱為可表示類型。 編譯器知道但不能用Java程序編寫的類型稱為非可表示類型。 例如,String是一個可表示類型,因為可以在程序中使用它來表示類型;然而,Serializable&CharSequence不是一個可表示類型的,即使它是編譯器的有效類型。 它是一種交叉類型,表示實現兩個接口SerializableCharSequence的類型。 通用類型定義允許使用交集類型,但不能使用此交集類型聲明變量:

// Not allowed in Java code. Cannot declare a variable of an intersection type.
Serializable & CharSequence var;
// Allowed in Java code
class Magic<T extends Serializable & CharSequence> {        
    // More code goes here
}

在JDK 9中,以下是允許使用具有匿名類的鑽石操作符的代碼片段:

// A compile-time error in JDK 7 and 8, but allowed in JDK 9.
Callable<Integer> c = new Callable<>() {
    @Override
    public Integer call() {
        return 100;
    }
};

使用Magic類的這個定義,JDK 9允許使用像這樣的匿名類:

// Allowed in JDK 9. The <> is inferred as <String>.
Magic<String> m1 = new Magic<>(){
    // More code goes here
};

以下使用Magic類不會在JDK 9中進行編譯,因為編譯器將通用類型推斷為不可表示類型的交集類型:

// A compile-time error in JDK 9. The <> is inferred as <Serializable & CharSequence>,
// which is non-denotable
Magic<?> m2 = new Magic<>(){
    // More code goes here
};

上面的代碼生成以下編譯時錯誤:

error: cannot infer type arguments for Magic<>
        Magic<?> m2 = new Magic<>(){
                               ^
  reason: type argument INT#1 inferred for Magic<> is not allowed in this context
    inferred argument is not expressible in the Signature attribute
  where INT#1 is an intersection type:
    INT#1 extends Object,Serializable,CharSequence
1 error

四. 接口中使用私有方法

JDK 8在接口中引入了靜態和默認的方法。 如果必須在這些方法中多次執行相同的邏輯,則只能重復邏輯或將邏輯移動到另一個類來隱藏實現。 考慮名為Alphabet的接口,如下所示。

// Alphabet.java
package com.jdojo.misc;
public interface Alphabet {
    default boolean isAtOddPos(char c) {
        if (!Character.isLetter(c)) {
            throw new RuntimeException("Not a letter: " + c);
        }
        char uc = Character.toUpperCase(c);
        int pos = uc - 64;
        return pos % 2 == 1;
    }
    default boolean isAtEvenPos(char c) {
        if (!Character.isLetter(c)) {
            throw new RuntimeException("Not a letter: " + c);
        }
        char uc = Character.toUpperCase(c);
        int pos = uc - 64;
        return pos % 2 == 0;
    }
}

isAtOddpos()isAtEvenPos()方法檢查指定的字符是否為奇數或偶數字母順序,假設我們只處理英文字母。邏輯假定A和a位於位置1,B和b位於位置2等。請注意,兩種方法中的邏輯僅在返回語句中有所不同。這些方法的整體是相同的,除了最后的語句。你會同意需要重構這個邏輯。將常用邏輯轉移到另一種方法,並從兩種方法調用新方法將是理想的情況。但是,不希望在JDK 8中執行此操作,因為接口僅支持公共方法。這樣做會使第三種方式公開,這將暴露給你不想做的外部世界。

JDK 9允許在接口中聲明私有方法。下顯示了使用包含兩種方法使用的通用邏輯的專用方法的Alphabet接口的重構版本。這一次,命名了接口AlphabetJdk9,以確保可以在源代碼中包含這兩個版本。現有的兩種方法成為一行代碼。

// AlphabetJdk9.java
package com.jdojo.misc;
public interface AlphabetJdk9 {
    default boolean isAtOddPos(char c) {
        return getPos(c) % 2 == 1;
    }
    default boolean isAtEvenPos(char c) {
        return getPos(c) % 2 == 0;
    }
    private int getPos(char c) {
        if (!Character.isLetter(c)) {
            throw new RuntimeException("Not a letter: " + c);
        }
        char uc = Character.toUpperCase(c);
        int pos = uc - 64;
        return pos;
    }
}

在JDK 9之前,接口中的所有方法都被隱式公開。 記住這些適用於Java中所有程序的簡單規則:

  • private 方法不能被繼承,因此不能被重寫。
  • final方法不能被重寫。
  • abstract 方法是可以繼承的,意圖是被重寫。
  • default 方法是一個實例方法,並提供默認實現。 這意味着可以被重寫。

通過在JDK 9中引入私有方法,需要在接口聲明方法時遵循一些規則。 修飾符的所有組合——abstractpublicprivatestatic。 下表列出了在JDK 9中的接口的方法聲明中支持和不支持的修飾符的組合。請注意,接口的方法聲明中不允許使用fjinal修飾符。 根據這個列表,可以在一個非抽象,非默認的實例方法或一個靜態方法的接口中有一個私有方法。

Modifiers Supported? Description
public static Yes 從JDK 8開始支持
public abstract Yes 從JDK 1開始支持
public default Yes 從JDK 8開始支持
private static Yes 從JDK 9開始支持
private Yes 從JDK 9開始支持,這是一個非抽象的實例方法
private abstract No 這種組合沒有意義
private default No 這種組合沒有意義,私有方法不被繼承,因此不能被重寫,而如果需要,默認方法的本意是需要重寫的。

五. 私有方法上的@SafeVarargs注解

具體化類型表示其信息在運行時完全可用,例如StringIntegerList等。非具體化類型表示其信息已由編譯器使用類型擦除(例如List<String>)刪除, 編譯后成為List

當使用非具體化類型的可變(var-args)參數時,該參數的類型僅供編譯器使用。 編譯器將擦除參數化類型,並將其替換為無界類型的實際類型為Object []的數組,其類型為有界類型的上限的特定數組。 編譯器不能保證對方法體內的這種非具體化可變參數執行的操作是安全的。 考慮以下方法的定義:

<T> void print(T... args) {
    for(T element : args) {
        System.out.println(element);
    }
}

編譯器將用print(Object[] args)替換 print(T… args)。 該方法的主體對args參數不執行任何不安全的操作。考慮執行以下不安全操作的方法聲明:

public static void unsafe(List<Long>... rolls) {
    Object[] list = rolls;        
    list[0] = List.of("One", "Two");
    // Unsafe!!! Will throw a ClassCastException at runtime
    Long roll = rolls[0].get(0);
}

unsafe()方法將rolls(它是List<String>的數組)分配給一個Object []數組。 它將List<String>存儲到Object []的第一個元素中,這也是允許的。 rolls [0]的類型被推斷為List <Long>get(0)方法應該返回一個Long。 但是,運行時會拋出一個ClassCastException,因為rolls[0].get(0)返回的實際類型是String,而不是Long

當聲明使用非具體化的可變參數類型的print()unsafe()方法時,Java編譯器會發出如下所示的未經檢查的警告:

warning: [unchecked] Possible heap pollution from parameterized vararg type List<Long>
    public static void unsafe(List<Long>... rolls) {
                                            ^

編譯器會為此類方法聲明生成警告,並為每次調用該方法發出警告。 如果unsafe()方法被調用五次,將收到六個警告(一個用於聲明,五個調用)。 可以在方法聲明和調用站點上使用@SafeVarargs注解來抑制這些警告。 通過將此注解添加到方法聲明中,確保方法的用戶和編譯器在方法的主體中,不對非具體化的可變參數類型執行任何不安全的操作。 你的保證是足夠好的,編譯器不發出警告。 但是,如果你的保證在運行時證明是不真實的,則運行時將拋出適當類型的異常。

在JDK 9之前,可以在以下可執行的(構造函數和方法)上使用@SafeVarargs注解:

  • 構造方法
  • static方法
  • final方法

構造方法,static方法和final方法是不可重寫的。 允許@SafeVarargs注解僅適用於不可重寫的可執行的代碼的想法,是為了保護開發人員在重寫可執行代碼上違反注解約束的重寫可執行文件上使用此注解。 假設有一個類X,它包含一個方法m1(),它包含一個@SafeVarargs。 進一步假設有一個從類X繼承的類Y。類Y可以重寫繼承的方法m1(),並可能有不安全的操作。 這將產生運行時驚喜,因為開發人員可以根據父類X編寫代碼,並且可能不會期望任何不安全的操作,如其方法m1()所承諾的。

私有方法也是不可重寫的,所以JDK 9決定在私有方法上允許@SafeVarargs注解。 下面顯示了一個使用@SafeVarargs注解的私有方法的類。 在JDK 9中可以具有@SafeVarargs注釋的可執行列表如下所示:

  • 構造方法
  • static方法
  • final方法
  • 私有方法
// SafeVarargsTest.java
package com.jdojo.misc;
public class SafeVarargsTest {
    // Allowed in JDK 9
    @SafeVarargs
    private <T> void print(T... args) {
        for(T element : args) {
            System.out.println(element);
        }
    }
    // More code goes here
}

在JDK 8中編譯此類會生成以下錯誤,它指出@SafeVarargs不能在非final方法中使用,這是一種私有方法。 需要使用-Xlint:unchecked選項編譯源代碼以查看錯誤。

com\jdojo\misc\SafeVarargsTest.java:6: error: Invalid SafeVarargs annotation. Instance method <T> print(T...) is not final.
    private <T> void print(T... args) {
                     ^
  where T is a type-variable:
    T extends Object declared in method <T>print(T...)

六. 丟棄子進程的輸出

JDK 9向ProcessBuilder.Redirect嵌套類添加了一個DISCARD新常量。 它的類型是ProcessBuilder.Redirect。 當要丟棄輸出時,可以將其用作子進程的輸出和錯誤流的目標。 實現通過寫入操作系統特定的“空文件(null file)”來丟棄輸出。下面包含一個完整的程序,顯示如何丟棄子進程的輸出。

// DiscardProcessOutput.java
package com.jdojo.misc;
import java.io.IOException;
public class DiscardProcessOutput {
    public static void main(String[] args) {
        System.out.println("Using Redirect.INHERIT:");
        startProcess(ProcessBuilder.Redirect.INHERIT);
        System.out.println("\nUsing Redirect.DISCARD:");
        startProcess(ProcessBuilder.Redirect.DISCARD);
    }
    public static void startProcess(ProcessBuilder.Redirect outputDest) {        
        try {
            ProcessBuilder pb = new ProcessBuilder()
                    .command("java", "-version")                    
                    .redirectOutput(outputDest)
                    .redirectError(outputDest);
            Process process = pb.start();
            process.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果為:

Using Redirect.INHERIT:
java version "9-ea"
Java(TM) SE Runtime Environment (build 9-ea+157)
Java HotSpot(TM) 64-Bit Server VM (build 9-ea+157, mixed mode)
Using Redirect.DISCARD:
Listing 20-8.
Discarding a Process’ Outputs

startProcess()方法通過使用-version參數啟動java程序來開始一個進程。 該方法通過輸出目的地參數。 第一次,Redirect.INHERIT作為輸出目的地傳遞,這允許子進程使用標准輸出和標准錯誤來打印消息。 第二次,Redirect.DISCARD作為輸出目標傳遞,沒有子進程的輸出。

七. StrictMath類中的新方法

JDK在java.lang包中包含兩個類MathStrictMath。 這兩個類只包含靜態成員,它們包含提供基本數字操作(如平方根,絕對值,符號,三角函數和雙曲線函數)的方法。 為什么有兩個類來提供類似的操作? Math類不需要在所有實現中返回相同的結果。 這允許它使用庫的本地實現來進行操作,這可能會在不同的平台上返回稍微不同的結果。StrictMath類必須在所有實現中返回相同的結果。 Math類中的許多方法都調用StrictMath類的方法。 JDK 9將以下靜態方法添加到MathStrictMath類中:

long floorDiv(long x, int y)
int floorMod(long x, int y)
double fma(double x, double y, double z)
float fma(float x, float y, float z)
long multiplyExact(long x, int y)
long multiplyFull(int x, int y)
long multiplyHigh(long x, long y)

floorDiv()方法返回小於或等於將x除以y的代數商的最大長度值。 當兩個參數具有相同的符號時,除法結果將向零舍入(截斷模式)。 當它們具有不同的符號時,除法結果將朝向負無窮大。 當被除數為Long.MIN_VALUE而除數為-1時,該方法返回Long.MIN_VALUE。 當除數為零時拋出ArithmeticException

floorMod()方法返回最小的模數,等於

 x - (floorDiv(x, y) * y)

最小模數的符號與除數y相同,在-abs(y) < r < +abs(y)范圍內。

fma()方法對應於IEEE 754-2008中定義的fusedMultiplyAdd操作。 它返回(a * b + c)的結果,如同無限范圍和精度一樣,並舍入一次到最接近的doublefloat值。 舍入是使用到最近的偶數舍入模式完成的。 請注意,fma()方法返回比表達式(a * b + c)更准確的結果,因為后者涉及兩個舍入誤差——一個用於乘法,另一個用於加法,而前者僅涉及一個舍入誤差。

multiplyExact()方法返回兩個參數的乘積,如果結果超過long類型最大能表示的數字,則拋出ArithmeticException異常。

multiplyFull()方法返回兩個參數的確切乘積。

multiplyHigh()方法返回長度是兩個64位參數的128位乘積的最高有效64位。 當乘以兩個64位長的值時,結果可能是128位值。 因此,該方法返回significant (high) 64位。 下面包含一個完整的程序,用於說明在StrictMath類中使用這些新方法。

// StrictMathTest.java
package com.jdojo.misc;
import static java.lang.StrictMath.*;
public class StrictMathTest {
    public static void main(String[] args) {
        System.out.println("Using StrictMath.floorDiv(long, int):");
        System.out.printf("floorDiv(20L, 3) = %d%n", floorDiv(20L, 3));
        System.out.printf("floorDiv(-20L, -3) = %d%n", floorDiv(-20L, -3));
        System.out.printf("floorDiv(-20L, 3) = %d%n", floorDiv(-20L, 3));
        System.out.printf("floorDiv(Long.Min_VALUE, -1) = %d%n", floorDiv(Long.MIN_VALUE, -1));
        System.out.println("\nUsing StrictMath.floorMod(long, int):");
        System.out.printf("floorMod(20L, 3) = %d%n", floorMod(20L, 3));
        System.out.printf("floorMod(-20L, -3) = %d%n", floorMod(-20L, -3));
        System.out.printf("floorMod(-20L, 3) = %d%n", floorMod(-20L, 3));
        System.out.println("\nUsing StrictMath.fma(double, double, double):");
        System.out.printf("fma(3.337, 6.397, 2.789) = %f%n", fma(3.337, 6.397, 2.789));
        System.out.println("\nUsing StrictMath.multiplyExact(long, int):");
        System.out.printf("multiplyExact(29087L, 7897979) = %d%n",
                multiplyExact(29087L, 7897979));
        try {
            System.out.printf("multiplyExact(Long.MAX_VALUE, 5) = %d%n",
                    multiplyExact(Long.MAX_VALUE, 5));
        } catch (ArithmeticException e) {
            System.out.println("multiplyExact(Long.MAX_VALUE, 5) = " + e.getMessage());
        }
        System.out.println("\nUsing StrictMath.multiplyFull(int, int):");
        System.out.printf("multiplyFull(29087, 7897979) = %d%n", multiplyFull(29087, 7897979));
        System.out.println("\nUsing StrictMath.multiplyHigh(long, long):");
        System.out.printf("multiplyHigh(29087L, 7897979L) = %d%n",
                multiplyHigh(29087L, 7897979L));
        System.out.printf("multiplyHigh(Long.MAX_VALUE, 8) = %d%n",
                multiplyHigh(Long.MAX_VALUE, 8));
    }
}

輸出結果為:

Using StrictMath.floorDiv(long, int):
floorDiv(20L, 3) = 6
floorDiv(-20L, -3) = 6
floorDiv(-20L, 3) = -7
floorDiv(Long.Min_VALUE, -1) = -9223372036854775808
Using StrictMath.floorMod(long, int):
floorMod(20L, 3) = 2
floorMod(-20L, -3) = -2
floorMod(-20L, 3) = 1
Using StrictMath.fma(double, double, double):
fma(3.337, 6.397, 2.789) = 24.135789
Using StrictMath.multiplyExact(long, int):
multiplyExact(29087L, 7897979) = 229728515173
multiplyExact(Long.MAX_VALUE, 5) = long overflow
Using StrictMath.multiplyFull(int, int):
multiplyFull(29087, 7897979) = 229728515173
Using StrictMath.multiplyHigh(long, long):
multiplyHigh(29087L, 7897979L) = 0
multiplyHigh(Long.MAX_VALUE, 8) = 3

八. 對ClassLoader類的更改

JDK 9將以下構造方法和方法添加到java.lang.ClassLoader類中:

protected ClassLoader(String name, ClassLoader parent)
public String getName()
protected Class<?> findClass(String moduleName, String name)
protected URL findResource(String moduleName, String name) throws IOException
public Stream<URL> resources(String name)
public final boolean isRegisteredAsParallelCapable()
public final Module getUnnamedModule()
public static ClassLoader getPlatformClassLoader()
public final Package getDefinedPackage(String name)
public final Package[] getDefinedPackages()

這些方法具有直觀的名稱。受保護的構造方法和方法適用於開發人員創建新的類加載器。

一個類加載器可以有一個可選的名稱,可以使用getName() 方法。 當類加載器沒有名稱時,該方法返回null。 Java運行時將包括堆棧跟蹤和異常消息中的類加載程序名稱(如果存在)。 這將有助於調試。

resources() 方法返回使用特定資源名稱找到的所有資源的URL流。

每個類加載器都包含一個未命名的模塊,該模塊包含該類加載器從類路徑加載的所有類型。 getUnnamedModule()方法返回類加載器的未命名模塊的引用。

靜態getPlatformClassLoader()方法返回平台類加載器的引用。

九. Optional<T>類中的新方法

JDK 9中的java.util.Optional<T>類已經添加了三種新方法:

void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
Stream<T> stream()

在描述這些方法並提供一個顯示其使用的完整程序之前,請考慮以下Optional<Integer>列表:

List<Optional<Integer>> optionalList = List.of(Optional.of(1),
                                               Optional.empty(),
                                               Optional.of(2),
                                               Optional.empty(),
                                               Optional.of(3));

該列表包含五個元素,其中兩個為空的Optional ,三個包含值為1,2和3。

ifPresentOrElse()方法可以提供兩個備選的操作。 如果存在值,則使用該值執行指定的操作。 否則,它執行指定的可選值。 以下代碼片段使用流打印列表中的所有元素,如果Optional 不為空,則打印其具體的值,為空的話,替換為“Empty”字符串。

optionalList.stream()
            .forEach(p -> p.ifPresentOrElse(System.out::println,
                                            () -> System.out.println("Empty")));

打印結果為:

1
Empty
2
Empty
3

of方法如果Optional有值則返回Optional本身。否則,返回指定supplierOptional。以下代碼從Optional列表中返回一個流,並使用of()方法映射空的Optionals為帶有默認值0的Optionals.

optionalList.stream()
            .map(p -> p.or(() -> Optional.of(0)))
            .forEach(System.out::println);
Optional[1]
Optional[0]
Optional[2]
Optional[0]
Optional[3]

stream()方法返回包含Optional中存在的值的元素的順序流。 如果Optional為空,則返回一個空的流。 假設有一個Optional的列表,並且想收集另一個列表中的所有存在的值。 可以在Java 8中如下實現:

// list8 will contain 1, 2, and 3
List<Integer> list8 = optionalList.stream()
                                  .filter(Optional::isPresent)
                                  .map(Optional::get)
                                  .collect(toList());

必須使用過濾器過濾掉所有空的Optionals ,並將剩余的可選項映射到其值。 使用JDK 9中的新的stream()方法,可以將filter()map()操作組合成一個flatMap()操作,如下所示:

// list9 contain 1, 2, and 3
List<Integer> list9 = optionalList.stream()
                                  .flatMap(Optional::stream)
                                  .collect(toList());

下面包含一個完整的程序來演示使用這些方法。

// OptionalTest.java
package com.jdojo.misc;
import java.util.List;
import java.util.Optional;
import static java.util.stream.Collectors.toList;
public class OptionalTest {
    public static void main(String[] args) {
        // Create a list of Optional<Integer>
        List<Optional<Integer>> optionalList = List.of(
                Optional.of(1),
                Optional.empty(),
                Optional.of(2),
                Optional.empty(),
                Optional.of(3));
        // Print the original list
        System.out.println("Original List: " + optionalList);
        // Using the ifPresentOrElse() method
        optionalList.stream()
                    .forEach(p -> p.ifPresentOrElse(System.out::println,
                                                    () -> System.out.println("Empty")));
        // Using the or() method
        optionalList.stream()
                    .map(p -> p.or(() -> Optional.of(0)))
                    .forEach(System.out::println);
        // In Java 8
        List<Integer> list8 = optionalList.stream()
                                          .filter(Optional::isPresent)
                                          .map(Optional::get)
                                          .collect(toList());
        System.out.println("List in Java 8: " + list8);
        // In Java 9
        List<Integer> list9 = optionalList.stream()
                                          .flatMap(Optional::stream)
                                          .collect(toList());
        System.out.println("List in Java 9: " + list9);
    }
}

輸出結果為:

Original List: [Optional[1], Optional.empty, Optional[2], Optional.empty, Optional[3]]
1
Empty
2
Empty
3
Optional[1]
Optional[0]
Optional[2]
Optional[0]
Optional[3]
List in Java 8: [1, 2, 3]
List in Java 9: [1, 2, 3]

十. CompletableFuture<T>中的新方法

在JDK 9 中,java.util.concurrent包中的CompletableFuture<T>類添加了以下新方法:

<U> CompletableFuture<U> newIncompleteFuture()
Executor defaultExecutor()
CompletableFuture<T> copy()
CompletionStage<T> minimalCompletionStage()
CompletableFuture<T> completeAsync(Supplier<? extends T> supplier, Executor executor)
CompletableFuture<T> completeAsync(Supplier<? extends T> supplier)
CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)
CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit)
static Executor delayedExecutor(long delay, TimeUnit unit, Executor executor)
static Executor delayedExecutor(long delay, TimeUnit unit)
static <U> CompletionStage<U> completedStage(U value)
static <U> CompletableFuture<U> failedFuture(Throwable ex)
static <U> CompletionStage<U> failedStage(Throwable ex)

有關這些方法的更多信息,請查閱類的Javadoc。

十一. 旋轉等待提示(Spin-Wait Hints)

在多線程程序中,線程通常需要協調。一個線程可能必須等待另一個線程來更新volatile變量。 當volatile變量以某個值更新時,第一個線程可以繼續。 如果等待可能更長,建議第一個線程通過睡眠或等待來放棄CPU,並且可以在恢復工作時通知它。 然而,使線程睡眠或等待具有延遲。 為了短時間等待並減少延遲,線程通常通過檢查某個條件為真來循環等待。 考慮使用循環等待為dataReadyvolatile變量等於true的類中代碼:

volatile boolean dataReady;
...
@Override
public void run() {
    // Wait until data is ready
    while (!dataReady) {
        // No code
    }
    processData();
}
private void processData() {
    // Data processing logic goes here
}

該代碼中的while循環稱為spin-loop,busy-spin,busy-wait或spin-wait。 while保持循環,直到dataReady變量為true。
由於不必要的資源使用而不耐心等待,因此通常是需要的。 在這個例子中,優點是一旦dataReady變量變為true,線程就會開始處理數據。 然而,犧牲性能和功耗,因為線程正在活躍地循環。

某些處理器可以暗示線程處於旋轉等待狀態,如果可能,可以優化資源使用。 例如,x86處理器支持一個PAUSE指令來指示一個旋轉等待。 該指令延遲下一條指令對線程的執行有限的少量時間,從而提高了資源的使用。

JDK 9向Thread類添加了一個新的靜態onSpinWait()方法。 對處理器來說,這是一個純粹的提示,即調用者線程暫時無法繼續,因此可以優化資源使用。 當底層平台不支持這種提示時,此方法的可能實現可能是無效的。

下面包含示例代碼。 請注意,程序的語義不會通過使用旋轉等待提示來更改。 如果底層硬件支持提示,它可能會更好。

// SpinWaitTest.java
package com.jdojo.misc;
public class SpinWaitTest implements Runnable {
    private volatile boolean dataReady = false;
    @Override
    public void run() {
        // Wait while data is ready
        while (!dataReady) {
            // Hint a spin-wait
            Thread.onSpinWait();
        }
        processData();
    }
    private void processData() {
        // Data processing logic goes here
    }
    public void setDataReady(boolean dataReady) {
        this.dataReady = dataReady;
    }
}

十二. Time API 增強

Time API已在JDK 9中得到增強,並在多個接口和類中使用了大量新方法。 Time API由java.time.*包組成,它們位於java.base模塊中。

1. Clock

Clock類中已經添加了以下方法:

static Clock tickMillis(ZoneId zone)

tickMillis()方法返回一個時鍾,提供了整個毫秒的當前瞬間記錄。 時鍾使用最好的系統時鍾。時鍾以高於毫秒的精度截斷時間值。 調用此方法等同於以下內容:

Clock.tick(Clock.system(zone), Duration.ofMillis(1))

2. Duration

可以根據用途將Duration類中的新方法分為三類:

  • 將持續時間划分另一個持續時間的方法
  • 根據特定時間單位獲取持續時間的方法和獲取特定部分持續時間(如天,小時,秒等)的方法。
  • 將持續時間縮短到特定時間單位的方法
    在這里使用持續時間為23天,3小時45分30秒。 以下代碼片段將其創建Duration對象,並將其引用保存在compTime的變量中:
// Create a duration of 23 days, 3 hours, 45 minutes, and 30 seconds
Duration compTime = Duration.ofDays(23)
                            .plusHours(3)
                            .plusMinutes(45)
                            .plusSeconds(30);
System.out.println("Duration: " + compTime);

輸出結果為:

Duration: PT555H45M30S

通過將這些日期乘以24小時后,輸出顯示,此持續時間代表555小時,45分鍾和30秒。

(1). 將持續時間划分另一個持續時間

此類別中只有一種方法:

long dividedBy(Duration divisor)

divideBy()方法可以將持續時間划分另一個持續時間。 它返回特定除數在調用該方法的持續時間內發生的次數。 要知道在這段時間內有多少整周,可以使用七天作為持續時間來調用divideBy()方法。 以下代碼片段顯示了如何計算持續時間內的整天,周和小時數:

long wholeDays = compTime.dividedBy(Duration.ofDays(1));
long wholeWeeks = compTime.dividedBy(Duration.ofDays(7));
long wholeHours = compTime.dividedBy(Duration.ofHours(7));
System.out.println("Number of whole days: " + wholeDays);
System.out.println("Number of whole weeks: " + wholeWeeks);
System.out.println("Number of whole hours: " + wholeHours);

輸出結果為:

Number of whole days: 23
Number of whole weeks: 3
Number of whole hours: 79
(2). 轉換和檢索部分持續時間

此類別中的Duration類添加了幾種方法:

long toDaysPart()
int toHoursPart()
int toMillisPart()
int toMinutesPart()
int toNanosPart()
long toSeconds()
int toSecondsPart()

Duration類包含兩組方法。它們被命名為toXxx()toXxxPart(),其中Xxx可以是Days,Hours,Minutes,Seconds,Millis和Nanos。在此列表中,可能會注意到包含toDaysPart(),但是丟失了toDays()。如果看到某些Xxx中缺少一個方法,則表示這些方法已經存在於JDK 8中。例如,從JDK 8開始,toDays()方法已經在Duration類中。

名為toXxx()的方法將持續時間轉換為Xxx時間單位並返回整個部分。名為toXxxPart()的方法會以幾天為單位,以時間為單位分解持續時間:小時:分鍾:秒:毫秒:納秒,並從中返回Xxx部分。在這個例子中,toDays()將會將持續時間轉換為天數並返回整個部分,這是23。toDaysPart()會將持續時間分解為23天:3Hours:45Minutes:30Seconds:0Millis:0Nanos,並返回第一部分,這是23。我們將相同的規則應用於toHours()toHoursPart()方法。 toHours()方法會將持續時間轉換為小時,並返回整個小時數,這是555。toHoursPart()方法會將持續時間與toDaysPart()方法一樣分分解為一部分,並返回小時部分,這是。以下代碼片段顯示了幾個例子:

System.out.println("toDays(): " + compTime.toDays());
System.out.println("toDaysPart(): " + compTime.toDaysPart());
System.out.println("toHours(): " + compTime.toHours());
System.out.println("toHoursPart(): " + compTime.toHoursPart());
System.out.println("toMinutes(): " + compTime.toMinutes());
System.out.println("toMinutesPart(): " + compTime.toMinutesPart());

輸出結果為:

toDays(): 23
toDaysPart(): 23
toHours(): 555
toHoursPart(): 3
toMinutes(): 33345
toMinutesPart(): 45
(3). 截取持續時間

此類別中的Duration類只添加了一種方法:

Duration truncatedTo(TemporalUnit unit)

truncatedTo()方法返回一個持續時間的副本,其概念時間單位小於被截斷的指定單位。 指定的時間單位必須為DAYS或更小。 指定大於DAYS(如WEEKS和YEARS)的時間單位會引發運行時異常。

Tips
JDK 8中的LocalTimeInstant類中已經存在truncatedTo(TemporalUnit unit)方法。

以下代碼片段顯示了如何使用此方法:

System.out.println("Truncated to DAYS: " + compTime.truncatedTo(ChronoUnit.DAYS));
System.out.println("Truncated to HOURS: " + compTime.truncatedTo(ChronoUnit.HOURS));
System.out.println("Truncated to MINUTES: " + compTime.truncatedTo(ChronoUnit.MINUTES));

輸出結果為:

Truncated to DAYS: PT552H
Truncated to HOURS: PT555H
Truncated to MINUTES: PT555H45M

持續時間為23Days:3Hours:45Minutes:30Seconds:0Millis:0Nanos。 當將其截斷為DAYS時,小於天數的所有部分將被刪除,並返回23天,這與輸出中顯示的552小時相同。 當截斷到HOURS時,它會將所有小於小時的部分刪除掉,並返回555小時。 將其截斷到MINUTES可保留分鍾的部分,刪除小於分鍾的部分。

3. ofInstant() 工廠方法

Time API旨在提高開發人員的便利和效率。 有一些經常使用的用例,日期和時間之間的轉換強制開發人員使用更多的方法調用而不是必需的。 兩個這樣的用例是:

  • java.util.Date轉換為LocalDate
  • Instant轉換為LocalDateLocalTime

JDK 9在LocalDateLocalTime中添加了一個靜態工廠方法,ofInstant(Instant instant, ZoneId zone),以簡化這兩種類型的轉換。 在ZonedDateTimeOffsetDateTimeLocalDateTimeOffsetTime類中,JDK 8已經有了這種工廠方法。 以下代碼片段顯示了JDK 8和JDK 9的兩種方法——將java.util.Date轉換為LocalDate

// In JDK 8
Date dt = new Date();
LocalDate ld= dt.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .toLocalDate();
System.out.println("Current Local Date: " + ld);
        
// In JDK 9
LocalDate ld2 = LocalDate.ofInstant(dt.toInstant(), ZoneId.systemDefault());
System.out.println("Current Local Date: " + ld2);

輸出結果為:

Current Local Date: 2017-02-11
Current Local Date: 2017-02-11

以下代碼片段顯示了兩種方式,在DK 8和JDK 9,將Instant轉換為LocalDateLocalTime

// In JDK 8
Instant now = Instant.now();
ZoneId zone = ZoneId.systemDefault();
ZonedDateTime zdt = now.atZone(zone);
LocalDate ld3 = zdt.toLocalDate();
LocalTime lt3 = zdt.toLocalTime();
System.out.println("Local Date: " + ld3 + ", Local Time:" + lt3);
// In JDK 9        
LocalDate ld4 = LocalDate.ofInstant(now, zone);
LocalTime lt4 = LocalTime.ofInstant(now, zone);
System.out.println("Local Date: " + ld4 + ", Local Time:" + lt4);
Local Date: 2017-02-11, Local Time:22:13:31.919339400
Local Date: 2017-02-11, Local Time:22:13:31.919339400

輸出結果為:

Local Date: 2017-02-11, Local Time:22:13:31.919339400
Local Date: 2017-02-11, Local Time:22:13:31.919339400

4. 獲取紀元秒

有時想從LocalDate,LocalTimeOffsetTime獲取自1970-01-01T00:00:00Z的時代以來的秒數。 在JDK 8中,OffsetDateTime類包含一個toEpochSecond()方法。 如果要從ZonedDateTime獲取時代以來的秒數,則必須使用它的toOffsetDateTime()方法將其轉換為OffsetDateTime,並使用OffsetDateTime類的toEpochSecond()方法。 JDK 8沒有包含用於LocalDateLocalTimeOffsetTime類的toEpochSecond()方法。 JDK 9添加了這些方法:

LocalDate.toEpochSecond(LocalTime time, ZoneOffset offset)
LocalTime.toEpochSecond(LocalDate date, ZoneOffset offset)
OffsetTime.toEpochSecond(LocalDate date)

為什么這些類的toEpochSecond()方法的簽名不同? 要從時代1970-01-01T00:00:00Z獲得秒數,需要定義另一個Instant。 一個Instant可以用三個部分定義:日期,時間,區域偏移。 LocalDateLocalTime類只包含一個Instant的三個部分之一。 OffsetTime類包含兩個部分,一個時間和一個偏移量。 缺少的部分需要被這些類指定為參數。 因此,這些類包含toEpochSecond()方法,該方法的參數指定了用於定義Instant的缺失部分。 以下代碼片段使用相同的Instant從三個類中獲取時代的秒數:

LocalDate ld = LocalDate.of(2017, 2, 12);
LocalTime lt = LocalTime.of(9, 15, 45);
ZoneOffset offset = ZoneOffset.ofHours(6);
OffsetTime ot = OffsetTime.of(lt, offset);
long s1 = ld.toEpochSecond(lt, offset);
long s2 = lt.toEpochSecond(ld, offset);
long s3 = ot.toEpochSecond(ld);
System.out.println("LocalDate.toEpochSecond(): " + s1);
System.out.println("LocalTime.toEpochSecond(): " + s2);
System.out.println("OffsetTime.toEpochSecond(): " + s3);
LocalDate.toEpochSecond(): 1486869345
LocalTime.toEpochSecond(): 1486869345
OffsetTime.toEpochSecond(): 1486869345

5. LocalDate流

JDK 9可以輕松地跨越兩個給定日期之間的所有日期,可以是某時的一天或給定一個區間時段。 以下兩種方法已添加到LocalDate類中:

Stream<LocalDate> datesUntil(LocalDate endExclusive)
Stream<LocalDate> datesUntil(LocalDate endExclusive, Period step)

這些方法產生LocalDates的順序排序流。 流中的第一個元素是調用該方法的LocalDatedatesUntil(LocalDate endExclusive)方法一次一天地增加流中的元素,而datesUntil(LocalDate endExclusive, Period step)方法會按照指定的步驟增加它們。 指定的結束日期是排他的。 可以在返回的流上執行幾個有用的計算。 以下代碼片段計算了2017年的星期數。請注意,代碼使用2018年1月1日作為最后一個日期,它是排他的,這將使流返回2017年的所有日期。

long sundaysIn2017 = LocalDate.of(2017, 1, 1)
                              .datesUntil(LocalDate.of(2018, 1, 1))
                              .filter(ld -> ld.getDayOfWeek() == DayOfWeek.SUNDAY)
                              .count();        
System.out.println("Number of Sundays in 2017: " + sundaysIn2017);

打印的結果為:

Number of Sundays in 2017: 53

以下代碼片段將於2017年1月1日(含)之間打印至2022年1月1日(不包含),即星期五落在本月十三日的日期:

LocalDate.of(2017, 1, 1)
         .datesUntil(LocalDate.of(2022, 1, 1))
         .filter(ld -> ld.getDayOfMonth() == 13 && ld.getDayOfWeek() == DayOfWeek.FRIDAY)
         .forEach(System.out::println);

輸出結果為:

Fridays that fall on 13th of the month between 2017 - 2021 (inclusive):
2017-01-13
2017-10-13
2018-04-13
2018-07-13
2019-09-13
2019-12-13
2020-03-13
2020-11-13
2021-08-13

以下代碼片段打印2017年每月的最后一天:

System.out.println("Last Day of months in 2017:");
LocalDate.of(2017, 1, 31)                
         .datesUntil(LocalDate.of(2018, 1, 1), Period.ofMonths(1))
         .map(ld -> ld.format(DateTimeFormatter.ofPattern("EEE MMM dd, yyyy")))
         .forEach(System.out::println);

輸出結果為:

Last Day of months in 2017:
Tue Jan 31, 2017
Tue Feb 28, 2017
Fri Mar 31, 2017
Sun Apr 30, 2017
Wed May 31, 2017
Fri Jun 30, 2017
Mon Jul 31, 2017
Thu Aug 31, 2017
Sat Sep 30, 2017
Tue Oct 31, 2017
Thu Nov 30, 2017
Sun Dec 31, 2017

6. 新的格式化選項

JDK 9向Time API添加了一些格式化選項。 以下部分將詳細介紹這些改動。

1. 修正儒略日格式

可以在日期時間格式化程序模式中使用小寫字母g,它將日期部分格式化為修正儒略日作為整數。 可以多次重復多次使用g,例如ggg,如果結果中的位數小於g指定的數目,則會對結果進行零填充。 http://www.unicode.org/reports/tr35/tr35-41/tr35-dates.html#Date_Format_Patterns上的定義了格式化程序中字母g的含義如下:

修正儒略日。 這與以往的修正儒略日不同。 首先,它在當地時區午夜,而不是格林尼治標准時間中午划定天數。 二是本地數字; 也就是說,這取決於當地的時區。 它可以被認為是包含所有日期相關字段的單個數字。

Tips
大寫字母G被定義為JDK 8中的日期和時間格式化器符號。

以下代碼片段顯示了如何使用修正儒略日字符g格式化ZonedDateTime

ZonedDateTime zdt = ZonedDateTime.now();
System.out.println("Current ZonedDateTime: " + zdt);               
System.out.println("Modified Julian Day (g): " +
                zdt.format(DateTimeFormatter.ofPattern("g")));
System.out.println("Modified Julian Day (ggg): " +
                zdt.format(DateTimeFormatter.ofPattern("ggg")));
System.out.println("Modified Julian Day (gggggg): " +
                zdt.format(DateTimeFormatter.ofPattern("gggggg")));
Current ZonedDateTime: 2017-02-12T11:49:03.364431100-06:00[America/Chicago]

輸出結果為:

Modified Julian Day (g): 57796
Modified Julian Day (ggg): 57796
Modified Julian Day (gggggg): 057796

2. 通用時區名稱

JDK 8有兩個字母V和z來格式化日期和時間的時區。 字母V產生區域ID,例如“America / Los_Angeles; Z; -08:30”,字母z產生區域名稱,如中央標准時間和CST。

JDK 9將小寫字母v添加為格式化符號,生成通用的非定位區域名稱,如中央時間或CT。 “非定位”意味着它不會識別與UTC的偏移量。 它指的是牆上的時間——牆壁上的時鍾顯示的時間。 例如,中央時間上午8時,2017年3月1日將有UTC-06的偏移量,而2017年3月19日的UTC-05偏移量。通用非定位區域名稱不指定時區偏移量。 可以使用兩種格式-v和vvvv來分別以短格式(例如CT)和長格式(例如中央時間)生成通用非定位區域名稱。 以下代碼片段顯示了由V,Z和V格式化符號產生的格式化結果的差異:

ZonedDateTime zdt = ZonedDateTime.now();
System.out.println("Current ZonedDateTime: " + zdt);               
System.out.println("Using VV: " +
                zdt.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm VV")));
System.out.println("Using z: " +
                zdt.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm z")));
System.out.println("Using zzzz: " +
                zdt.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm zzzz")));
System.out.println("Using v: " +
                zdt.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm v")));
System.out.println("Using vvvv: " +
                zdt.format(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm vvvv")));

輸出結果為:

Current ZonedDateTime: 2017-02-12T12:30:08.975373900-06:00[America/Chicago]
Using VV: 02/12/2017 12:30 America/Chicago
Using z: 02/12/2017 12:30 CST
Using zzzz: 02/12/2017 12:30 Central Standard Time
Using v: 02/12/2017 12:30 CT
Using vvvv: 02/12/2017 12:30 Central Time

十三. 使用Scanner進行流操作

JDK 9將以下三個方法添加到java.util.Scanner中。 每個方法返回一個Stream

Stream<MatchResult> findAll(String patternString)
Stream<MatchResult> findAll(Pattern pattern)
Stream<String> tokens()

findAll()方法返回具有所有匹配結果的流。 調用findAll(patternString) 相當於調用findAll(Pattern.compile(patternString))tokens()方法使用當前的分隔符從scanner返回令牌流。下面包含一個程序,顯示如何僅使用findAll()方法從字符串中收集單詞。

// ScannerTest.java
package com.jdojo.misc;
import java.util.List;
import java.util.Scanner;
import java.util.regex.MatchResult;
import static java.util.stream.Collectors.toList;
public class ScannerTest {
    public static void main(String[] args) {
        String patternString = "\\b\\w+\\b";
        String input = "A test string,\n which contains a new line.";
        List<String> words = new Scanner(input)
                .findAll(patternString)
                .map(MatchResult::group)
                .collect(toList());
        System.out.println("Input: " + input);
        System.out.println("Words: " + words);
    }
}

輸出結果為:

Input: A test string,
 which contains a new line.
Words: [A, test, string, which, contains, a, new, line]

十四. Matcher類的增強

java.util.regex.Matcher類在JDK 9中添加了一些新的方法:

Matcher appendReplacement(StringBuilder sb,  String replacement)
StringBuilder appendTail(StringBuilder sb)
String replaceAll(Function<MatchResult,String> replacer)
String replaceFirst(Function<MatchResult,String> replacer)
Stream<MatchResult> results()

JDK 8中的Matcher類在此列表中已經有前四個方法。 在JDK 9中,它們已經重載了。 appendReplacement()appendTail()方法用於使用StringBuffer。 現在他們也可以使用StringBuilderreplaceAll()replaceFirst()方法將String作為參數。 在JDK 9中,它們已經被重載,以Function<T,R>作為參數。

results()方法返回其元素為MatchResult類型的流中的匹配結果。 可以查詢MatchResult獲取結果作為字符串。 可以將Matcher的結果作為JDK 8中的流進行處理。但是邏輯並不簡單。 results()方法不會重置matcher。 如果要重置matcher,不要忘記調用其reset()方法將其重置為所需的位置。下面顯示了這種方法的一些有趣的用法。

// MatcherTest.java
package com.jdojo.misc;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
public class MatcherTest {
    public static void main(String[] args) {
        // A regex to match 7-digit or 10-digit phone numbers
        String regex = "\\b(\\d{3})?(\\d{3})(\\d{4})\\b";
        // An input string
        String input = "1, 3342229999, 2330001, 6159996666, 123, 3340909090";
        // Create a matcher
        Matcher matcher = Pattern.compile(regex)
                                  .matcher(input);
        // Collect formatted phone numbers into a list
        List<String> phones = matcher.results()
                          .map(mr -> (mr.group(1) == null ? "" : "(" + mr.group(1) + ") ")
                                      + mr.group(2) + "-" + mr.group(3))
                          .collect(toList());
        System.out.println("Phones: " + phones);
        // Reset the matcher, so we can reuse it from start
        matcher.reset();
        // Get distinct area codes
        Set<String> areaCodes = matcher.results()
                                       .filter(mr -> mr.group(1) != null)
                                       .map(mr -> mr.group(1))
                                       .collect(toSet());
        System.out.println("Distinct Area Codes:: " + areaCodes);                
    }
}

輸出的結果為:

Phones: [(334) 222-9999, 233-0001, (615) 999-6666, (334) 090-9090]
Distinct Area Codes:: [334, 615]

main()方法聲明兩個名regexinput的局部變量。 正則表達式變量包含一個正則表達式,以匹配7位數或10位數字。 將使用它在輸入字符串中查找電話號碼。 input變量保存有嵌入電話號碼的文本。

// A regex to match 7-digit or 10-digit phone numbers
String regex = "\\b(\\d{3})?(\\d{3})(\\d{4})\\b";
// An input string
String input = "1, 3342229999, 2330001, 6159996666, 123, 3340909090";

接下來,將正則表達式編譯為Pattern對象並獲取matcher

// Create a matcher
Matcher matcher = Pattern.compile(regex)
                         .matcher(input);

要將10位電話號碼格式化為(nnn)nnn-nnnn和7位數電話號碼為nnn-nnnn的格式。 最后,要將所有格式化的電話號碼收集到List<String>中。 以下語句執行:

 // Collect formatted phone numbers into a list
 List<String> phones = matcher.results()
                              .map(mr -> (mr.group(1) == null ? "" : "(" + mr.group(1) + ") ")
                                      + mr.group(2) + "-" + mr.group(3))
                              .collect(toList());

請注意使用接收MatchResultmap()方法,並將格式化的電話號碼返回為String。當一個匹配是一個7位數的電話號碼時,組1將為空現在,要重新使用matcher, 以10位數的電話號碼查找不同的區號。必須重置matcher,所以下一個匹配從輸入字符串的開始處開始:

// Reset the matcher, so we can reuse it from start
matcher.reset();

MatchResult中的第一個組包含區號。 需要濾除7位數的電話號碼,並在Set <String>中收集組1的值,以獲得一組不同的區號。 以下語句是這樣做的:

// Get distinct area codes
Set<String> areaCodes = matcher.results()
                               .filter(mr -> mr.group(1) != null)
                               .map(mr -> mr.group(1))
                               .collect(toSet());

十五. Object類的增強

java.util.Objects類包含對對象進行操作的靜態實用方法。 通常,它們用於驗證方法的參數,例如,檢查方法的參數是否為空。 JDK 9將以下靜態方法添加到此類中:

<T> T requireNonNullElse(T obj, T defaultObj)
<T> T requireNonNullElseGet(T obj, Supplier<? extends T> supplier)
int checkFromIndexSize(int fromIndex, int size, int length)
int checkFromToIndex(int fromIndex, int toIndex, int length)
int checkIndex(int index, int length)

JDK 8已經有了三個requireNonNull()重載方法。 該方法用於檢查值為非空值。 如果值為null,則會拋出NullPointerException。 JDK 9添加了這個方法的兩個版本。

如果obj為非空,則requireNonNullElse(T obj, T defaultObj)方法返回obj。 如果obj為空,並且defaultObj為非空,則返回defaultObj。 如果objdefaultObj都為空,則會拋出NullPointerException異常。

requireNonNullElseGet(T obj, Supplier<? extends T> supplier)方法的工作方式與requireNonNullElse(T obj, T defaultObj)方法相同,前者使用Supplier獲取默認值。 如果非空,它返回obj。 如果Supplier非空,並返回非空值,則返回從Supplier返回的值。 否則,拋出NullPointerException異常。

checkXxx()的方法意在用於檢查索引或子范圍是否在某一范圍內。當使用數組和集合時,它們很有用,需要處理索引和子范圍。如果索引或子范圍超出范圍,這些方法將拋出IndexOutOfBoundsException

checkFromIndexSize(int fromIndex,int size,int length)方法檢查指定的子范圍,從inIndex(包括)到fromIndex + size(不包括)是否在范圍內,范圍是從0(含)到length。如果任何參數為負整數或子范圍超出范圍,則拋出IndexOutOfBoundsException。如果子范圍在范圍內,則返回fromIndex。假設有一個接受索引和大小的方法,並從數組或列表返回一個子范圍。可以使用此方法來檢查所請求的子范圍是否在數組或列表的范圍內。

checkFromToIndex(int fromIndex, int toIndex, int length) 方法檢查指定的子范圍,從inIndex(包括)到toIndex(不包含)是否在范圍內,范圍為為0(含)到length(不包含)。如果任何參數是負整數或子范圍超出范圍,則拋出IndexOutOfBoundsException。如果子范圍在范圍內,則返回fromIndex。在使用數組和List時用於子范圍檢查是非常有用的。

checkIndex(int index, int length)方法檢查指定的索引是否在范圍內,為0(含)到length(不包含)。如果任何參數為負整數或索引超出范圍,則拋出IndexOutOfBoundsException。如果index在范圍內,則返回索引。當方法接收到索引並返回數組中的值或該索引的List時,它很有用。

十六. 數組比較

java.util.Arrays類由靜態實用方法組成,可用於對數組執行各種操作,例如排序,比較,轉換為流等。在JDK 9中,此類已經獲得了幾種方法,可以比較數組和切片(slices)。 新方法分為三類:

  • 比較兩個數組或它們的切片是否相等性
  • 按字典順序比較兩個數組
  • 查找兩個數組中的第一個不匹配的索引

添加到此類的方法列表是很大的。 每個類別中的方法對於所有原始類型和對象數組都是重載的。 有關完整列表,請參閱Arrays類的API文檔。

equals()方法可以比較兩個數組的相等性。 如果數組或部分數組中的元素數量相同,並且數組或部分數組中所有對應的元素對相等,則兩個數組被認為是相等的。 以下是int的兩個版本的equals()方法:

boolean equals(int[] a, int[] b)
boolean equals(int[] a, int aFromIndex, int aToIndex, int[] b, int bFromIndex, int bToIndex)

第一個版本允許比較兩個數組之間的相等性,並且存在於JDK 9之前。第二個版本允許將兩個數組的部分進行比較,以便在JDK 9中添加相等。fromIndex(包含)和toIndex(不包含)參數決定要比較的兩個數組的范圍。 如果兩個數組相等,則該方法返回true,否則返回false。 如果兩個數組都為空,則認為兩個數組相等。

JDK 9添加了幾個compare()compareUnsigned()的方法。 這兩種方法都按字典順序比較數組或部分數組中的元素。

compareUnsigned()方法將整數值視為無符號。 空數組的字符拼寫小於非空數組。 兩個空數組相等。 以下是對於intcompare()方法的兩個版本:

int compare(int[] a, int[] b)
int compare(int[] a, int aFromIndex, int aToIndex, int[] b, int bFromIndex, int bToIndex)

如果第一個和第二個數組相等並且包含相同的元素,compare()方法返回0; 如果第一個數組在字典上小於第二個數組,則返回小於0的值; 並且如果第一個數組在字典上大於第二個數組則返回大於0的值。

mismatch()方法比較兩個數組或數組的一部分。 以下是int的兩個版本的mismatch()方法:

int mismatch(int[] a, int[] b)
int mismatch (int[] a, int aFromIndex, int aToIndex, int[] b, int bFromIndex, int bToIndex)

mismatch()方法返回第一個不匹配的索引。 如果沒有不匹配,則返回-1。 如果任一數組為空,則拋出NullPointerException。 下包含一個比較兩個數組及其部分數組的完整程序。 該程序使用兩個int數組。

// ArrayComparision.java
package com.jdojo.misc;
import java.util.Arrays;
public class ArrayComparison {
    public static void main(String[] args) {
        int[] a1 = {1, 2, 3, 4, 5};
        int[] a2 = {1, 2, 7, 4, 5};
        int[] a3 = {1, 2, 3, 4, 5};
        // Print original arrays
        System.out.println("Three arrays:");
        System.out.println("a1: " + Arrays.toString(a1));
        System.out.println("a2: " + Arrays.toString(a2));
        System.out.println("a3: " + Arrays.toString(a3));
        // Compare arrays for equality
        System.out.println("\nComparing arrays using equals() method:");
        System.out.println("Arrays.equals(a1, a2): " + Arrays.equals(a1, a2));
        System.out.println("Arrays.equals(a1, a3): " + Arrays.equals(a1, a3));
        System.out.println("Arrays.equals(a1, 0, 2, a2, 0, 2): " +
                           Arrays.equals(a1, 0, 2, a2, 0, 2));
        // Compare arrays lexicographically
        System.out.println("\nComparing arrays using compare() method:");
        System.out.println("Arrays.compare(a1, a2): " + Arrays.compare(a1, a2));
        System.out.println("Arrays.compare(a2, a1): " + Arrays.compare(a2, a1));
        System.out.println("Arrays.compare(a1, a3): " + Arrays.compare(a1, a3));
        System.out.println("Arrays.compare(a1, 0, 2, a2, 0, 2): " +
                           Arrays.compare(a1, 0, 2, a2, 0, 2));
        // Find the mismatched index in arrays
        System.out.println("\nFinding mismatch using the mismatch() method:");                
        System.out.println("Arrays.mismatch(a1, a2): " + Arrays.mismatch(a1, a2));
        System.out.println("Arrays.mismatch(a1, a3): " + Arrays.mismatch(a1, a3));
        System.out.println("Arrays.mismatch(a1, 0, 5, a2, 0, 1): " +
                            Arrays.mismatch(a1, 0, 5, a2, 0, 1));
    }
}

輸出結果為:

a1: [1, 2, 3, 4, 5]
a2: [1, 2, 7, 4, 5]
a3: [1, 2, 3, 4, 5]
Comparing arrays using equals() method:
Arrays.equals(a1, a2): false
Arrays.equals(a1, a3): true
Arrays.equals(a1, 0, 2, a2, 0, 2): true
Comparing arrays using compare() method:
Arrays.compare(a1, a2): -1
Arrays.compare(a2, a1): 1
Arrays.compare(a1, a3): 0
Arrays.compare(a1, 0, 2, a2, 0, 2): 0
Finding mismatch using the mismatch() method:
Arrays.mismatch(a1, a2): 2
Arrays.mismatch(a1, a3): -1
Arrays.mismatch(a1, 0, 5, a2, 0, 1): 1

十七. Applet API已經廢棄

Java applets需要Java瀏覽器插件才能正常工作。 許多瀏覽器供應商已經刪除了對Java瀏覽器插件的支持,或者將在不久的將來刪除它。 如果瀏覽器不支持Java插件,則不能使用applet,因此沒有理由使用Applet API。 JDK 9棄用了Applet API。 但是,它將不會在JDK 10中被刪除。如果計划在將來的版本中被刪除,開發人員將提前發布一個通知。 以下類和接口已被棄用:

java.applet.AppletStub
java.applet.Applet
java.applet.AudioClip
java.applet.AppletContext
javax.swing.JApplet

在JDK 9中,所有AWT和Swing相關類都打包在java.desktop模塊中。 這些不推薦的類和接口也在同一個模塊中。

appletviewer工具隨其JDK在bin目錄中提供,用於測試applet。 該工具也在JDK 9中不推薦使用。在JDK 9中運行該工具會打印一個棄用警告。

十八. Javadoc增強

JDK 9引入了Javadoc的編寫,生成和使用方式的一些增強功能。 JDK 9在Javadoc中支持HTML5。 默認情況下,javadoc工具仍然在HTML4中生成輸出。 一個新的選項-html5已添加到該工具中,表明希望HTML5中的輸出:

javadoc -html5 <other-options>

javadoc工具位於JDK_HOME\bin目錄中。 使用--help選項運行工具打印其使用說明和所有選項。

NetBeans IDE可以為項目生成Javadoc。 在項目的“屬性”對話框中,選擇“Build”➤“Documenting”以獲取Javadoc屬性頁,可以在其中指定javadoc工具的所有選項。 要生成Javadoc,請從項目的右鍵菜單選項中選擇“Generate Javadoc”。

JDK 9保留了三個Frame或無Frame的Javadoc布局。 左上角的框架包含三個鏈接:所有類,所有包和所有模塊。 在JDK 9中添加了ALL MODULES鏈接,其中顯示了所有模塊的列表。 ALL CLASSES鏈接可以查看左下框架中的所有類。 其他兩個鏈接可查看所有軟件包,模塊中的所有軟件包以及所有模塊。 下面顯示了Javadoc頁面的更改。

javadoc

考慮這種情況。 正在尋找在Java中實現某些內容的邏輯,並在Internet上找到一段代碼,該代碼使用類,但不顯示導入該類的導入語句。 可以訪問Java SE的Javadoc,並希望了解更多關於該類的信息。 如何獲取類的包名,這是需要獲取類的文檔? 再次搜索互聯網。 這次,搜索類名,這可能會獲得該類的Javadoc的鏈接。 或者,可以將這段代碼復制並粘貼到Java IDE(如NetBeans和Eclipse)中,IDE將生成導入語句,提供類的包名稱。 不要擔心在JDK 9中搜索類的包名稱的這種不便。

右邊的主Frame還有另一個補充。 此框中的所有頁面都顯示右上角的“搜索”框。 搜索框可搜索Javadoc。 javadoc工具准備可搜索的術語索引。 要知道可搜索的內容,需要知道索引的條款:

  • 可以搜索模塊,軟件包,類型和成員的聲明名稱。 構造方法和方法的形式參數的類型被索引,但不是這些參數的名稱。 所以,可以搜索形式參數的類型。 如果在搜索框中輸入“(String, int, int)”,將會找到使用String,int和int三個形式參數的構造函數和方法的列表。 如果輸入“util”作為搜索項,它將顯示包含名稱中的“util”一詞的所有包,類型和成員的列表。
  • JDK 9引入了一個新的內聯Javadoc標簽@index,可以用來告訴javadoc工具對關鍵字進行索引。 它可以作為{@index <keyword> <description>}出現在Javadoc中,其中<keyword>是要被索引的關鍵字,而<description>是關鍵字的描述。 以下Javadoc標記是使用帶有關鍵字jdojo的@index標記的示例:jdojo: {@index jdojo Info site ( [www.jdojo.com ](http://www.jdojo.com/)) for the Java 9 Revealed book!}

此列表中未列出的其他所有內容都不可使用Javadoc搜索框進行搜索。 當輸入搜索字詞時,搜索框會將搜索結果顯示為列表。 結果列表分為類別,如模塊,包,類型,成員和搜索標簽。 SearchTags類別包含從使用@index標記指定的索引關鍵字中找到的結果。

Tips
Javadoc搜索不支持正則表達式。

下圖顯示了使用結果列表的Javadoc搜索框。 為com.jdojo.misc模塊生成了Javadoc,並使用它來搜索jdojo。 使用Java SE 9 的Javadoc來搜索術語Module,如右圖所示。

搜索結果

可以使用向上和向下箭頭鍵瀏覽搜索結果。 可以通過以下兩種方式查看搜索結果的詳細信息:

  • 單擊搜索結果以打開該主題的Javadoc。
  • 當使用向上/向下箭頭突出顯示搜索結果時,按Enter打開該主題的詳細信息。

Tips
可以使用-noindex選項與javadoc工具來禁用Javadoc搜索。 將不會生成索引,並且生成的Javadoc中不會有搜索框可用。

使用客戶端JavaScript本地執行Javadoc搜索。 在服務器中沒有實現計算或搜索邏輯。 如果在瀏覽器中禁用JavaScript,則無法使用Javadoc搜索功能。

十九. 本地桌面功能

Java SE 6通過java.awt.Desktop類添加了特定於平台的桌面支持。 該類支持從Java應用程序執行以下操作:

  • 在用戶默認瀏覽器中打開URI
  • 在用戶默認郵件客戶端中打開mailto URI
  • 使用注冊的應用程序打開,編輯和打印文件

如果Java SE 9在當前平台上可用,那么Java SE 9推出面向特定於平台的桌面支持,並為許多系統和應用程序事件通知添加公共API支持。 java.awt.Desktop類仍然是使用平台特定的桌面功能的中心類。 為了支持這么多新的桌面功能,Java SE 9向java.desktop模塊添加了一個新的包java.awt.desktop。java.awt.Desktop類也有很多新方法。 新包包含30個類和接口。 在JDK 9中,Desktop API支持24個特定於桌面的桌面操作和通知,它們由Desktop.Action枚舉的常量定義。 舉幾個例子,它們如下:

  • 當附件顯示進入或退出節電時的通知
  • 當系統進入睡眠或系統喚醒后的通知
  • 用戶會話更改時的通知,例如鎖定/解鎖用戶會話
  • 當應用程序的狀態更改為或不是前台應用程序時的通知
  • 要求應用程序顯示其“關於”對話框時的通知

可以使用這些功能來優化應用程序的資源使用情況。 例如,如果系統進入睡眠模式,您可以停止動畫,並在系統喚醒時恢復。 有關詳細信息,請參閱java.awt.Desktop類的API文檔以及java.awt.desktop包中的類和接口。 使用桌面功能時,以下是典型的步驟:

  • 使用此類的靜態isDesktopSupported()方法檢查當前平台是否支持Desktop類。 如果該方法返回false,則不能使用任何桌面功能。
  • 如果支持Desktop類,請使用Desktop類的靜態getDesktop()方法獲取Desktop類的引用。
  • 並非所有桌面功能都可在所有平台上使用。 在桌面對象上使用isSupported(Desktop.Action action) 方法來檢查是否支持特定的桌面操作。 受支持的桌面操作由Desktop.Action枚舉中的常量表示。
  • 如果支持桌面操作,可以調用Desktop類的一個方法來執行諸如打開文件的操作,也可以使用addAppEventListener(SystemEventListener listener) 方法注冊事件處理程序。

Tips
java.awt和java.awt.desktop包在java.desktop模塊中。 當使用平台特定的桌面功能時,請確保你的模塊讀取java.desktop模塊。

下面包含一個演示桌面功能的完整程序。 應用程序注冊用戶會話更改監聽器。 當用戶會話更改時,通知應用程序,並在標准輸出上打印消息。 可以通過遠程登錄/注銷或通過鎖定和解鎖計算機來更改用戶會話。 可以使用此桌面通知來暫停昂貴的處理,例如用戶會話被停用時的動畫,並在激活該過程時重新啟動該過程。 以下詳細說明本程序的輸出。 運行程序時,需要鎖定和解鎖您的計算機與用戶會話相關的輸出。 顯示的輸出是在Windows上運行這個程序,並在程序運行時鎖定和解鎖我的計算機一次。 你可能得到不同的輸出。 兩分鍾后,程序自行退出。

// DeskTopFrame.java
package com.jdojo.misc;
import java.awt.Desktop;
import java.awt.desktop.UserSessionEvent;
import java.awt.desktop.UserSessionListener;
import java.util.concurrent.TimeUnit;
public class DeskTopFrame {
    public static void main(String[] args) {
        // Check if Desktop class is available
        if (!Desktop.isDesktopSupported()) {
            System.out.println("Current Platform does not support Desktop.");
            return;
        }
        System.out.println("Current platform supports Desktop.");
        // Get the desktop reference
        Desktop desktop = Desktop.getDesktop();
        // Check if user session event notification is supported
        if (!desktop.isSupported(Desktop.Action.APP_EVENT_USER_SESSION)) {
            System.out.println("User session notification is not " +
                               "supported by the current desktop");
            return;
        }
        System.out.println("Lock and unlock your session to see " +
                           "user session change notification in action.");
        // Add an event handler for a change in user session
        desktop.addAppEventListener(new UserSessionListener() {
            @Override
            public void userSessionDeactivated(UserSessionEvent e) {
                System.out.println("User session deactivated. Reason: " + e.getReason());
            }
            @Override
            public void userSessionActivated(UserSessionEvent e) {
                System.out.println("User session activated. Reason: " + e.getReason());
            }
        });
        // Make the current thread sleep for 2 minutes
        try {            
            TimeUnit.SECONDS.sleep(120);            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出的結果為:

Current platform supports Desktop.
Lock and unlock your session to see user session change notification in action.
User session deactivated. Reason: LOCK
User session activated. Reason: LOCK

main()方法檢查當前平台上的Desktop類是否可用。 如果不可用,程序退出。 如果可用,則獲得其引用。

if (!Desktop.isDesktopSupported()) {
    System.out.println("Current Platform does not support Desktop.");
    return;
}
// Get the desktop reference
Desktop desktop = Desktop.getDesktop();

如有興趣在用戶會話更改時收到通知,因此需要檢查此功能是否受支持。 如果不支持,程序退出。

// Check if user session event notification is supported
if (!desktop.isSupported(Desktop.Action.APP_EVENT_USER_SESSION)) {
    System.out.println("User session notification is not " +
                       "supported by the current desktop");
     return;
}

如果支持用戶會話更改通知,則需要注冊UserSessionListener類型的事件監聽器,如下所示:

// Add an event handler for a change in user session
desktop.addAppEventListener(new UserSessionListener() {
    @Override
    public void userSessionActivated(UserSessionEvent e) {
        System.out.println("Use session activated. Reason: " + e.getReason());
    }
    @Override
    public void userSessionDeactivated(UserSessionEvent e) {
        System.out.println("User session deactivated. Reason: " + e.getReason());
    }
});

分別激活和停用用戶會話時,會調用注冊的UserSessionListeneruserSessionActivated()userSessionDeactivated()方法。 兩個方法都將一個UserSessionEvent對象作為參數。 UserSessionEvent類的getReason()方法返回一個UserSessionEvent.Reason,它是一個枚舉,它的常量定義了用戶會話更改的原因。 枚舉有四個常量:CONSOLELOCKREMOTEUNSPECIFIEDCONSOLEREMOTE常數表示用戶會話分別與控制台終端和遠程終端連接/斷開的原因。 LOCK常數表示指示用戶會話已被鎖定或解鎖的原因。 顧名思義,UNSPECIFIED常數表示用戶會話更改的所有其他原因。

最后,main()方法使當前線程休眠兩分鍾,所以有機會鎖定和解鎖會話以查看程序的工作。 如果刪除程序的這一部分,程序將退出,而不等待更改用戶會話。

二十. 對象反序列化過濾器

Java可以對對象進行序列化和反序列化。 為了解決反序列化帶來的安全風險,JDK 9引入了可以用來驗證反序列化對象的對象輸入過濾器的概念,如果不通過測試,則可以停止反序列化過程。 對象輸入過濾器是添加到JDK 9的新接口java.io.ObjectInputFilter的實例。過濾器可以基於以下一個或多個條件:

  • 數組的長度反序列化
  • 嵌套對象的深度反序列化
  • 對象引用數反序列化
  • 對象的類被反序列化
  • 從輸入流消耗的字節數

ObjectInputFilter接口只包含一個方法:

ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo)

可以指定要用於反序列化所有對象的全局過濾器。 可以通過為對象輸入流設置本地過濾器來重寫每個ObjectInputStream上的全局過濾器。 可以沒有全局過濾器,並為每個對象輸入流指定本地過濾器。 有幾種方法來創建和指定過濾器。 本節首先介紹添加到JDK 9中的類和接口,需要使用這些類和接口來處理過濾器:

ObjectInputFilter
ObjectInputFilter.Config
ObjectInputFilter.FilterInfo
ObjectInputFilter.Status

ObjectInputFilter接口的實例表示過濾器。 可以通過在類中實現此接口來創建過濾器。 或者,可以使用ObjectInputFilter.Config類的createFilter(String pattern) 方法從字符串獲取其實例。

ObjectInputFilter.Config是一個嵌套的靜態實用類,用於兩個目的:

  • 獲取並設置全局過濾器
  • 從指定字符串的模式中創建過濾器

ObjectInputFilter.Config類包含以下三種靜態方法:

ObjectInputFilter createFilter(String pattern)
ObjectInputFilter getSerialFilter()
void setSerialFilter(ObjectInputFilter filter)

createFilter()方法接受一個描述過濾器的模式,並返回ObjectInputFilter接口的實例。 以下代碼片段創建一個過濾器,指定反序列化數組的長度不應超過4:

String pattern = "maxarray=4";
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);

可以在一個過濾器中指定多個模式。 它們用分號(;)分隔。 以下代碼片段從兩種模式創建一個過濾器。 如果遇到長度大於4的數組或串行化對象的大小大於1024字節,則過濾器將拒絕對象反序列化。

String pattern = "maxarray=4;maxbytes=1024";
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);

指定過濾器模式有幾個規則。 如果喜歡在Java代碼中編寫過濾器邏輯,可以通過創建實現ObjectInputFilter接口的類並將其寫入其checkInput()方法來實現。 如果要從字符串中的模式創建過濾器,請遵循以下規則:
有五個過濾條件,其中四個是限制。 它們是maxarraymaxdepthmaxrefsmaxbytes。 可以使用name = value來設置它們,其中name是這些關鍵字,value是限制。 如果模式包含等號(=),則模式必須使用這四個關鍵字作為名稱。 第五個過濾條件用於指定類名形式的模式:

<module-name>/<fully-qualified-class-name>
  • 如果一個類是未命名的模塊,則該模式將與類名匹配。 如果對象是一個數組,則數組的組件類型的類名用於匹配模式,而不是數組本身的類名。 以下是匹配類名稱的模式的所有規則:
  • 如果類名與模式匹配,則允許對象反序列化。
  • 以“!” 模式開頭的字符被視為邏輯NOT。
  • 如果模式包含斜杠(/),斜杠之前的部分是模塊名稱。 如果模塊名稱與類的模塊名稱相匹配,則斜線后面的部分將被用作匹配類名稱的模式。 如果模式中沒有斜線,則在匹配模式時不考慮類的模塊名稱。
  • 以“.**”結尾的模式匹配包中的任何類和所有子軟件包。
  • 以“.*”結尾的模式匹配包中的任何類。
  • 以“*”結尾的模式匹配任何具有模式作為前綴的類。
  • 如果模式等於類名稱,則它匹配。
  • 另外,模式不匹配,對象被拒絕。

如果將com.jdojo.**設置為過濾器模式,它允許com.jdojo包中的所有類及其子包都被反序列化,並將拒絕所有其他類的反序列化對象。 如果將“com.jdojo.**”設置為過濾器模式,它將拒絕com.jdojo包中的所有類及其子包以進行反序列化,並允許反序列化所有其他類的對象。

getSerialFilter()setSerialFilter()方法用於獲取和設置全局過濾器。 可以使用以下三種方式之一設置全局過濾器:

  • 通過設置名為jdk.serialFilter的系統屬性,該屬性的值是以分號分隔的一系列過濾器模式。
  • 通過在java.security文件中設置一個存儲在JAVA_HOME\conf\security目錄中的jdk.serialFilter屬性。 如果正在使用JDK運行程序,請將JAVA_HOME作為JDK_HOME讀取。 否則,將其讀為JRE_HOME。
  • 通過調用ObjectInputFilter.Config類的setSerialFilter()靜態方法。

以下命令在運行類時將jdk.series屬性設置為命令行選項。 不要擔心這個命令的其他細節。

C:\Java9Revealed>java -Djdk.serialFilter=maxarray=100;maxdepth=3;com.jdojo.** --module-path com.jdojo.misc\build\classes --module com.jdojo.misc/com.jdojo.misc.ObjectFilterTest

下面顯示了JAVA_HOME\conf\security\java.security配置文件的部分內容。 該文件包含更多的條目。 只顯示一個設置過濾器的條目,這與設置jdk.serialFilter系統屬性具有相同的效果,如上一個命令所示。

maxarray=100;maxdepth=3;com.jdojo.**

Tips
如果在系統屬性和配置文件中設置過濾器,則優先使用系統屬性中的值。

當運行具有全局過濾器的java命令時,會注意到stderr上的消息類似於此處顯示的消息:

Feb 17, 2017 9:23:45 AM java.io.ObjectInputFilter$Config lambda$static$0
INFO: Creating serialization filter from maxarray=20;maxdepth=3;!com.jdojo.**

這些消息使用java.io.serialization的Logger作為平台消息記錄java.base模塊。 如果指定了平台Logger,這些消息將被記錄到Logger中。 其中一條消息在系統屬性或配置文件中打印全局過濾器集。

還可以使用ObjectInputFilter.Config類的靜態setSerialFilter()方法在代碼中設置全局過濾器:

// Create a filter
String pattern = "maxarray=100;maxdepth=3;com.jdojo.**";
ObjectInputFilter globalFilter = ObjectInputFilter.Config.createFilter(pattern);
// Set a global filter
ObjectInputFilter.Config.setSerialFilter(globalFilter);

Tips
只能設置一次全局過濾器。 例如,如果使用jdk.serialFilter系統屬性設置過濾器,則在代碼中調用Config.setSerialFiter()將拋出IllegalStateException。 當使用`Config.setSerialFiter()方法設置全局過濾器時,必須設置非空值過濾器。 存在這些規則,以確保在代碼中無法覆蓋使用系統屬性或配置文件的全局過濾器集。

可以使用ObjectInputFilter.Config類的靜態getSerialFilter()方法獲取全局過濾器,而不考慮過濾器的設置方式。 如果沒有全局過濾器,則此方法返回null。

ObjectInputFilter.FilterInfo是一個嵌套的靜態接口,其實例包裝了反序列化的當前上下文。ObjectInputFilter.FilterInfo的實例被創建並傳遞給過濾器的checkInput()方法。 不必在程序中實現此接口並創建其實例。 該接口包含以下方法,將在自定義過濾器的checkInput()方法中使用以讀取當前反序列化上下文:

Class<?> serialClass()
long arrayLength()
long depth();
long references();
long streamBytes();

serialClass()方法返回反序列化對象的類。對於數組,它返回數組的類,而不是數組的組件類型的類。在反序列化期間未創建新對象時,此方法返回null。

arrayLength()方法返回反序列化數組的長度。它被反序列化的對象不是數組,它返回-1。

depth()方法返回被反序列化的對象的嵌套深度。它從1開始,對於每個嵌套級別遞增1,當嵌套對象返回時,遞減1。

references()方法返回反序列化的對象引用的當前數量。

streamBytes()方法返回從對象輸入流消耗的當前字節數。

對象可能根據指定的過濾條件會通過,也可能會失敗。根據測試結果,應該返回ObjectInputFilter.Status枚舉的以下常量。通常,在自定義過濾器類的checkInput()方法中使用這些常量作為返回值。

ALLOWED
REJECTED
UNDECIDED

這些常量表示反序列化允許,拒絕和未定。 通常,返回UNDECIDED表示一些其他過濾器將決定當前對象的反序列化是否繼續。 如果正在創建一個過濾器以將類列入黑名單,則可以返回REJECTED以獲取黑名單類別的匹配項,而對其他類別則為UNDECIDED

下面包含一個基於數組長度進行過濾的簡單過濾器。

// ArrayLengthObjectFilter.java
package com.jdojo.misc;
import java.io.ObjectInputFilter;
public class ArrayLengthObjectFilter implements ObjectInputFilter {
    private long maxLenth = -1;
    public ArrayLengthObjectFilter(int maxLength) {
        this.maxLenth = maxLength;
    }
    @Override
    public Status checkInput(FilterInfo info) {
        long arrayLength = info.arrayLength();
        if (arrayLength >= 0 && arrayLength > this.maxLenth) {
            return Status.REJECTED;
        }
        return Status.ALLOWED;
    }
}

以下代碼片段通過將數組的最大長度指定為3來使用自定義過濾器。如果對象輸入流包含長度大於3的數組,則反序列化將失敗,並顯示java.io.InvalidClassException。 代碼不顯示異常處理邏輯。

ArrayLengthObjectFilter filter = new ArrayLengthObjectFilter(3);
File inputFile = ...
ObjectInputStream in =  new ObjectInputStream(new FileInputStream(inputFile))) {            
in.setObjectInputFilter(filter);
Object obj = in.readObject();

下面包含一個Item類的代碼。為保持代碼簡潔,省略了getter和setter方法。 使用它的對象來演示反序列化過濾器。

// Item.java
package com.jdojo.misc;
import java.io.Serializable;
import java.util.Arrays;
public class Item implements Serializable {
    private int id;    
    private String name;
    private int[] points;
    public Item(int id, String name, int[] points) {
        this.id = id;
        this.name = name;
        this.points = points;
    }
    /* Add getters and setters here */
    @Override
    public String toString() {
        return "[id=" + id + ", name=" + name + ", points=" + Arrays.toString(points) + "]";
    }
}

下面包含ObjectFilterTest類的代碼,用於演示在對象反序列化過程中使用過濾器。 代碼中有詳細的說明。

// ObjectFilterTest.java
package com.jdojo.misc;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputFilter.Config;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectFilterTest {
    public static void main(String[] args)  {         
        // Relative path of the output/input file
        File file = new File("serialized", "item.ser");
        // Make sure directories exist
        ensureParentDirExists(file);
        // Create an Item used in serialization and deserialization
        Item item = new Item(100, "Pen", new int[]{1,2,3,4});
        // Serialize the item
        serialize(file, item);
        // Print the global filter
        ObjectInputFilter globalFilter = Config.getSerialFilter();
        System.out.println("Global filter: " + globalFilter);
        // Deserialize the item
        Item item2 = deserialize(file);
        System.out.println("Deserialized using global filter: " + item2);
        // Use a filter to reject array size > 2
        String maxArrayFilterPattern = "maxarray=2";
        ObjectInputFilter maxArrayFilter = Config.createFilter(maxArrayFilterPattern);         
        Item item3 = deserialize(file, maxArrayFilter);
        System.out.println("Deserialized with a maxarray=2 filter: " + item3);
        // Create a custom filter
        ArrayLengthObjectFilter customFilter = new ArrayLengthObjectFilter(5);                
        Item item4 = deserialize(file, customFilter);
        System.out.println("Deserialized with a custom filter (maxarray=5): " + item4);
    }
    private static void serialize(File file, Item item) {        
        try (ObjectOutputStream out =  new ObjectOutputStream(new FileOutputStream(file))) {            
            out.writeObject(item);
            System.out.println("Serialized Item: " + item);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static Item deserialize(File file) {
        try (ObjectInputStream in =  new ObjectInputStream(new FileInputStream(file))) {                        
            Item item = (Item)in.readObject();
            return item;
        } catch (Exception e) {
            System.out.println("Could not deserialize item. Error: " + e.getMessage());
        }
        return null;
    }
    private static Item deserialize(File file, ObjectInputFilter filter) {
        try (ObjectInputStream in =  new ObjectInputStream(new FileInputStream(file))) {            
            // Set the object input filter passed in
            in.setObjectInputFilter(filter);
            Item item = (Item)in.readObject();
            return item;
        } catch (Exception e) {
            System.out.println("Could not deserialize item. Error: " + e.getMessage());            
        }
        return null;
    }
    private static void ensureParentDirExists(File file) {
        File parent = file.getParentFile();
        if(!parent.exists()) {
            parent.mkdirs();
        }
        System.out.println("Input/output file is " + file.getAbsolutePath());
    }
}

ObjectFilterTest使用不同的過濾器序列化Item類,隨后使用相同Item類多個反序列化。ensureParentDirExists() 方法接受一個文件,並確保其父目錄存在,如果需要創建它。 該目錄還打印序列化文件的路徑。

serialize() 方法將指定的Item對象序列化為指定的文件。 這個方法從main()方法調用一次序列化一個Item對象。

deserialize() 方法是重載的。 deserialize(File file)版本使用全局過濾器(如果有的話)反序列化保存在指定文件中的Item對象。 deserialize(File file, ObjectInputFilter filter) 版本使用指定的過濾器反序列化保存在指定文件中的Item對象。 注意在此方法中使用in.setObjectInputFilter(filter)方法調用。 它為ObjectInputStream設置指定的過濾器。 此過濾器將覆蓋全局過濾器(如果有)。

main()方法打印全局過濾器,創建一個Item對象並對其進行序列化,創建多個本地過濾器,並使用不同的過濾器對同一個Item對象進行反序列化。 以下命令運行ObjectFilterTest類而不使用全局過濾器。 可能得到不同的輸出。

C:\Java9Revealed>java --module-path com.jdojo.misc\build\classes
--module com.jdojo.misc/com.jdojo.misc.ObjectFilterTest

輸出結果為:

Input/output file is C:\Java9Revealed\serialized\item.ser
Serialized Item: [id=100, name=Pen, points=[1, 2, 3, 4]]
Global filter: null
Deserialized using global filter: [id=100, name=Pen, points=[1, 2, 3, 4]]
Could not deserialize item. Error: filter status: REJECTED
Deserialized with a maxarray=2 filter: null
Deserialized with a custom filter (maxarray=2): [id=100, name=Pen, points=[1, 2, 3, 4]]

以下命令使用全局過濾器maxarray = 1運行ObjectFilterTest類,這將防止具有多個元素的數組被反序列化。 全局過濾器是使用jdk.serialFilter系統屬性設置的。 因為正在使用全局過濾器,JDK類將在stderr上記錄消息。

C:\Java9Revealed>java -Djdk.serialFilter=maxarray=1
--module-path com.jdojo.misc\build\classes
--module com.jdojo.misc/com.jdojo.misc.ObjectFilterTest

輸出結果為:

Input/output file is C:\Java9Revealed\serialized\item.ser
Serialized Item: [id=100, name=Pen, points=[1, 2, 3, 4]]
Feb 17, 2017 1:09:57 PM java.io.ObjectInputFilter$Config lambda$static$0
INFO: Creating serialization filter from maxarray=1
Global filter: maxarray=1
Could not deserialize item. Error: filter status: REJECTED
Deserialized using global filter: null
Could not deserialize item. Error: filter status: REJECTED
Deserialized with a maxarray=2 filter: null
Deserialized with a custom filter (maxarray=5): [id=100, name=Pen, points=[1, 2, 3, 4]]

注意使用全局過濾器時的輸出。 因為Item對象包含一個包含四個元素的數組,所以全局過濾器阻止它反序列化。 但是,可以使用ArrayLengthObjectFilter對同一對象進行反序列化,因為此過濾器覆蓋全局過濾器,並允許數組中最多有五個元素。 這在輸出的最后一行是顯而易見的。

二十一. Java I/O API新增方法

JDK 9向I/O API添加了一些方便的方法。 第一個是InputStream類中的一種新方法:

long transferTo(OutputStream out) throws IOException

編寫的代碼從輸入流讀取所有字節,以便寫入輸出流。 現在,不必編寫一個循環來從輸入流讀取字節並將其寫入輸出流。 transferTo()方法從輸入流讀取所有字節,並將它們讀取時依次寫入指定的輸出流。 該方法返回傳輸的字節數。

Tips
transferTo()方法不會關閉任何一個流。 當此方法返回時,輸入流將在流的末尾。

忽略異常處理和流關閉邏輯,這里是一行代碼,將log.txt文件的內容復制到log_copy.txt文件。

new FileInputStream("log.txt").transferTo(new FileOutputStream("log_copy.txt"));

java.nio.Buffer類在JDK 9中增加了兩種新方法:

abstract Buffer duplicate()
abstract Buffer slice()

兩種方法返回一個Buffer,它共享原始緩沖區的內容。 僅當原始緩沖區是直接的或只讀時,返回的緩沖區將是直接的或只讀的。 duplicate()方法返回一個緩沖區,其容量,臨界,位置和標記值將與原始緩沖區的值相同。 slice()方法返回一個緩沖區,其位置將為零,容量和臨界是此緩沖區中剩余的元素數量,標記不定義。 返回的緩沖區的內容從原始緩沖區的當前位置開始。 來自這些方法的返回緩沖區保持與原始緩沖區無關的位置,限定和標記。 以下代碼片段顯示了duplicatedsliced緩沖區的特征:

IntBuffer b1 = IntBuffer.wrap(new int[]{1, 2, 3, 4});
IntBuffer b2 = b1.duplicate();
IntBuffer b3 = b1.slice();
System.out.println("b1=" + b1);
System.out.println("b2=" + b2);
System.out.println("b2=" + b3);
// Move b1 y 1 pos
b1.get();
IntBuffer b4 = b1.duplicate();
IntBuffer b5 = b1.slice();
System.out.println("b1=" + b1);
System.out.println("b4=" + b4);
System.out.println("b5=" + b5);
b1=java.nio.HeapIntBuffer[pos=0 lim=4 cap=4]
b2=java.nio.HeapIntBuffer[pos=0 lim=4 cap=4]
b2=java.nio.HeapIntBuffer[pos=0 lim=4 cap=4]
b1=java.nio.HeapIntBuffer[pos=1 lim=4 cap=4]
b4=java.nio.HeapIntBuffer[pos=1 lim=4 cap=4]
b5=java.nio.HeapIntBuffer[pos=0 lim=3 cap=3]

二十二. 總結

在JDK 9中,下划線(_)是一個關鍵字,不能將其本身用作單字符標識符,例如變量名稱,方法名稱,類型名稱等。但是,仍然可以使用下划線多個字符的標識符名稱。

JDK 9刪除了限制,必須使用try-with-resource塊為要管理的資源聲明新變量。現在,可以使用final或有效的final變量來引用資源由try-with-resources塊來管理。

只要推斷的類型是可表示的,JDK 9就添加了對匿名類中的鑽石操作符的支持。

可以在接口中具有非抽象非默認實例方法或靜態方法的私有方法。

JDK 9允許在私有方法上使用@SafeVarargs注解。 JDK 8已經允許它在構造方法,stati方法和final`方法上。

JDK 9向ProcessBuilder.Redirect嵌套類添加了DISCARD的新常量。它的類型是ProcessBuilder.Redirect。當要丟棄輸出時,可以將其用作子進程的輸出和錯誤流的目標。實現通過寫入操作系統特定的“空文件”來丟棄輸出。

JDK 9為MathStrictMath類添加了幾種方法來支持更多的數學運算,如floorDiv(long x, int y)floorMod(long x, int y)multiplyExact(long x, int y)multiplyFull(int x, int y)multiplyHigh(long x, long y) 等。

JDK 9向java.util.Optional類添加了三個方法:ifPresentOrElse()of()stream()ifPresentOrElse()方法可以提供兩個備選的操作。如果存在值,則執行一個操作。否則,它執行另一個操作。如果存在值,則or()方法返回Optional。否則返回指定Supplier返回的可選項。 stream()方法返回包含可選中存在的值的元素的順序流。如果Optional為空,則返回一個空的流。 stream()方法在扁平映射中(flat maps)很有用。

JDK 9向Thread類添加了一個新的靜態onSpinWai()方法。對處理器來說,這是一個純粹的提示,即調用者線程暫時無法繼續,因此可以優化資源使用。在自旋循環中使用它。

Time API在JDK 9中得到了一個提升。在DurationLocalDateLocalTimeOffsetTime類中添加了幾種方法。 LocalDate類接收到一個新的datesUntil()方法,它返回兩個日期之間的日期流,以一天或給定期間的增量。 Time API中有幾個新的格式化符號。

Matcher類新增幾個現有方法的重載版本,它們用於與StringBuffer一起工作,以支持使用StringBuilder。一個為results()的新方法返回一個Stream<MatchResult>Objects類收到了幾個新的實用方法來檢查數組和集合的范圍。

ava.util.Arrays新增了幾種方法,可以比較數組和部分數組的相等性和不匹配性。

Javadoc在JDK 9中得到了增強。它支持HTML5。可以使用一個新的選項-html5與javadoc工具一起生成HTML5格式的Javadoc。對所有模塊,包,類型,成員和形式參數類型的名稱進行索引,並使用新的搜索功能進行搜索。 Javadoc在每個主頁的右上角顯示一個搜索框,可用於搜索索引條款。還可以在Javadoc中使用一個新的標簽@index來創建用戶定義的術語。使用客戶端JavaScript執行搜索,並且不進行服務器通信。

許多瀏覽器供應商已經刪除了對Java瀏覽器插件的支持,或者將在不久的將來刪除它。記住這一點,JDK 9不贊成使用Applet API。 java.applet包和javax.swing.JApplet類中的所有類型已被棄用。 appletviewer工具也已被棄用。

JDK 6通過java.awt.Desktop類添加了對平台特定桌面功能的有限支持,例如在用戶默認瀏覽器中打開URI,在用戶默認郵件客戶端中打開mailto URI,以及使用注冊的應用打開,編輯和打印文件。如果Java SE 9在當前平台上可用,許多系統和應用程序事件通知都會提供特定於平台的桌面支持,並為其添加了公共API支持。為了支持這么多新的桌面功能,Java SE 9向java.desktop模塊添加了一個新的包java.awt.desktop。 java.awt.Desktop類也增加了很多新的方法。在JDK 9中,Desktop API支持24個平台特定的桌面操作和通知,例如當附加的顯示進入或退出節電模式,系統進入睡眠模式或系統喚醒后的通知等。
為了解決反序列化帶來的安全風險,JDK 9引入了一個對象輸入過濾器的概念,可以用來驗證被反序列化的對象,如果沒有通過測試,則可以停止反序列化過程。對象輸入過濾器是新接口java.io.ObjectInputFilter的實例。可以指定可以在反序列化任何對象時使用的全系統全局過濾器。可以使用新的jdk.serialFilter系統屬性,使用JAVA_HOME\conf\security\java.security文件中jdk.serialFilter的屬性,或使用ObjectInputFilter.Config類的setSerialFilter()方法來指定全局過濾器。可以使用其setObjectInputFilter()方法在ObjectInputStream上設置本地過濾器,該方法將覆蓋全局過濾器。

java.io.InputStream類新增一個稱為transferTo(OutputStream out)的方法,可用於從輸入流讀取所有字節,並將它們順序寫入指定的輸出流。該方法不關閉任一流。 java.nio.Buffer類接收到兩個方法,duplicate()slice()——可用於復制和拼接緩沖區。復制和分片緩沖區與原始緩沖區共享其內容。但是他們保持自己的位置,限定和標記,獨立於原始緩沖區。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM