1. 程式人生 > >Java 9 揭祕(20. JDK 9中API層次的改變)

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. 修正儒略日格式

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

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()方法宣告兩個名regex