體驗 Java 9(2):更新與非重大改動

jywhltj發表於2018-04-02

本文原發於我的個人部落格:https://hltj.me/java/2018/04/02/experience-java9-update-minors.html。本副本只用於圖靈社群,禁止第三方轉載。

本篇介紹 Java 9 更新以及一些非重大改動。

更新

Java 9 已經正式釋出半年多了。這期間不僅 Java 9 釋出了更新,就連 Java 10 也已正式釋出。上一篇中提到的工具也都有更新。其中 IDEA 新版改進了不少 Java 9 支援,Eclipse 新版內建了 Java 9 支援(不再需要 Beta 版外掛,但可能需要重灌,不能直接從舊版升級)。最值得一提的是 lombok 1.16.20 釋出。

Lombok 相容性改善

期待已久的 lombok 1.16.20 終於在 2018-01-09 正式釋出。這一版解決了上一篇中提到的大多數相容問題。具體如下: 1. 可以與 Gradle 4.1 及以上版本配合使用。 這點很重要,因為 Gradle 4.2 及以上版本才能支援 Java 9.0.1/9.0.4。 2. 改進 IDE 支援。 將上一篇中提到的 Lombok 示例 中的 Lombok 版本升級到 1.16.20 後,在新版 IDEA 中無論建立普通專案、Maven 專案還是 Gradle 專案,都能正常編輯與執行;在新版 Eclipse 中,Maven、Gradle 專案正常,普通專案還是有問題;在新版 NetBeans 開發版中普通專案、Maven 專案均正常,Gradle 專案在 JDK 9 環境正常,JDK 9.0.1/9.0.4 環境有問題。

結合一篇的內容,彙總 Lombok 1.16.18 到 1.16.20 相容情況變化如下表所示:

enter image description here

表中箭頭(⇨)之前表示 Lombok 1.16.18 對工具的相容情況,箭頭之後表示 Lombok 1.16.20 的相容情況。

Java 9 更新與版本號模式

Java 9 的安全更新版 9.0.1、9.0.4 分別於 2017-10-17、2018-01-16 釋出。可以看出這裡的 Java 版本號與以往 1.8.0_162 這種版本號差異很明顯。Java 9 的版本號模式(version schema)為:

$MAJOR.$MINOR.$SECURITY

即“主版本號.次版本號.安全級別”,這樣很容易看出 9.0.1 與 9.0.4 都是安全修復版本,應該升級。

Java 9 還增加了用於獲取版本號的 API,以前取 Java 版本號主要通過系統屬性來取,現在可以直接呼叫 API 了。java.lang.Runtime 新添了一個靜態方法 version(),其返回值型別為 Runtime.Version,通過它可以很方便地獲取不同格式的版本號。例如:

jshell> Runtime.version()
$1 ==> 9.0.4+11

jshell> Runtime.version().version()
$2 ==> [9, 0, 4]

jshell> Runtime.version().major()
$3 ==> 9

jshell> Runtime.version().security()
$4 ==> 4

jshell> Runtime.version().build()
$5 ==> Optional[11]

值得補充的是,Java 10 再次修改版本號模式為:

$FEATURE.$INTERIM.$UPDATE.$EMERG

即“特性版本號.中間版本號.更新版本號.緊急修復版本號”。 同時 Runtime.Version 的相應方法 major()minor()security() 也被棄用,建議分別使用新增的 feature()interim()update() 方法取代,另外新增 patch() 方法用於取緊急修復版本號。例如:

jshell> Runtime.version()
$1 ==> 10+46

jshell> Runtime.version().feature()
$2 ==> 10

jshell> Runtime.version().build()
$3 ==> Optional[46]

當然 Java 10 與 Java 9 的前三位版本號的含義並不完全相同。特別是自 Java 10 起將嚴格按照 6 個月的節奏發版,即今年三月份釋出 Java 10、九月份釋出 Java 11、明年三月份釋出 Java 12……以此類推。

關於 Java 9 與 Java 10 版本號模式的更多內容請參見 JEP 223: New Version-String Scheme 以及 JEP 322: Time-Based Release Versioning

非重大改動

接下來介紹以下內容:

  • 下劃線成為保留字
  • try-with-resource 改進
  • InputStream 改進
  • 介面的私有預設方法
  • 集合的工廠方法
  • Optional 改進
  • Stream 改進
  • 程式 API 改進

如果已經瞭解過就不需要再往下看了。

下劃線成為保留字

單獨一個下劃線(_)作為識別符號在 Java 8 中就已經棄用,但是仍然可以通過編譯。例如下述程式碼:

public class Demo0 {
    public static void main(String[] args) {
        int _ = 1;
        System.out.println(_);
    }
}

在 Java 8 環境中編譯會出現以下警告:

$ javac Demo0.java 
Demo0.java:3: warning: '_' used as an identifier
        int _ = 1;
            ^
  (use of '_' as an identifier might not be supported in releases after Java SE 8)
Demo0.java:4: 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
$ java Demo0
1

而在 Java 9 環境中則不能通過編譯:

$ javac Demo0.java
Demo0.java:3: error: as of release 9, '_' is a keyword, and may not be used as an identifier
        int _ = 1;
            ^
Demo0.java:4: error: as of release 9, '_' is a keyword, and may not be used as an identifier
        System.out.println(_);
                           ^
2 errors

在 JShell 中使用也會出現同樣的報錯:

jshell> String _ = "foo";
|  Error:
|  as of release 9, '_' is a keyword, and may not be used as an identifier
|  String _ = "foo";
|         ^

這是因為 _ 已經成為保留字,在未來的 Java 版本中有可能用作佔位符,類似 Scala 的模式匹配語法或者 Kotlin 的解構語法中的佔位符。

try-with-resource 改進

Java 7 引入了 try-with-resource 語法,對於支援自動清理的資源(實現了 AutoCloseable 介面的型別),將引用建立的語句放在 try 與左花括號之間圓括號中,就可以執行自動清理。例如

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

對於既有引用,在 Java 7/8 中需要引入臨時變數,例如:

BufferedReader br = new BufferedReader(new FileReader(path));
try (BufferedReader br1 = br) {
    return br1.readLine();
}

而在 Java 9 中,使用既有 final(或相當於 final)的可自動清理資源引用時無需引入臨時變數。上述程式碼可以簡化為:

BufferedReader br = new BufferedReader(new FileReader(path));
try (br) {
    return br.readLine();
}

InputStream 改進

Java 9 的 InputStream 新添了一個 transferTo() 方法,可以從輸入流讀取並寫到輸出流。例如,在 JShell 中用一行程式碼實現終端輸入回顯(按 Ctrl-C 結束):

jshell> System.in.transferTo(System.out)
Hello
Hello
0123456789
0123456789

$1 ==>

有了這個函式,實現類似 IOUtils.toString() 的邏輯就很方便了:

jshell> String inputToString(InputStream is) throws IOException {
   ...>    ByteArrayOutputStream os = new ByteArrayOutputStream();
   ...>    is.transferTo(os);
   ...>    return os.toString();
   ...> }
|  created method inputToString(InputStream)

jshell> inputToString(new ByteArrayInputStream("Hello".getBytes()))
$2 ==> "Hello"

介面的私有預設方法

Java 8 引入了介面的預設方法,可以為介面提供預設實現。例如我們有一個介面 Person 以及很多實現該介面的類,當我們為 Person 介面新增 greeting()farewell() 兩個方法時,可以為其提供預設實現,而無需每個實現 Person 介面的類都實現一遍:

public interface Person {
    // 其他介面 ……

    String getName();

    default void greeting() {
        System.out.println(getName() + ": Hello!");
    }

    default void farewell() {
        System.out.println(getName() + ": Goodbye!");
    }
}

Java 9 的介面支援私有預設方法,可以讓多個預設方法複用程式碼:

public interface Person {
    // 其他介面 ……

    String getName();

    default void greeting() {
        say("Hello!");
    }

    default void farewell() {
        say("Goodbye!");
    }

    private void say(String words) {
        System.out.println(getName() + ": " + words);
    }
}

假如這裡的私有預設方法 say() 並沒有呼叫例項方法 getName(),那麼還可以使用靜態私有方法:

public interface Person {
    // 其他介面 ……

    default void greeting() {
        say("Hello!");
    }

    default void farewell() {
        say("Goodbye!");
    }

    private static void say(String words) {
        System.out.println(words);
    }
}

集合的工廠方法

在 Java 9 之前,建立帶有初始字面值的只讀 List、Set 與 Map 的程式碼語法風格各異,甚至有些繁瑣麻煩。例如:

// import static java.util.Collections.unmodifiableSet;
// import static java.util.Collections.unmodifiableMap;
// import static java.util.stream.Collectors.collectingAndThen;
// import static java.util.stream.Collectors.toSet
// import static java.util.stream.Collectors.toMap
// import static java.util.AbstractMap.SimpleEntry;

final List<String> list = Arrays.asList("foo", "bar", "baz");

final Set<Integer> numbers = unmodifiableSet(new HashSet<Integer>() {
    {
        add(1); add(2); add(3);
    }
});

// 或者
final Set<Integer> numbers1 =
    unmodifiableSet(new HashSet<Integer>(Arrays.asList(1, 2, 3)));


// 或者(Java 8)
final Set<Integer> numbers2 =
Stream.of(1, 2, 3)
    .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));

final Map<Integer, String> map =
    unmodifiableMap(new HashMap<Integer, String>() {
        {
            put(1, "foo");
            put(2, "bar");
            put(3, "baz");
        }
    });

// 或者(Java 8)
final Map<Integer, String> map1 =
    unmodifiableMap(Stream.of(
        new SimpleEntry<>(1, "foo"),
        new SimpleEntry<>(2, "bar"),
        new SimpleEntry<>(3, "baz"))
            .collect(toMap((e) -> e.getKey(), (e) -> e.getValue()))
    );

而在 Java 9 中可以使用更簡潔、更直觀、更統一的方式:

// import static java.util.Map.entry

final List<String> list = List.of("foo", "bar", "baz");

final Set<Integer> numbers = Set.of(1, 2, 3);

final Map<Integer, String> map = Map.of(1, "foo", 2, "bar", 3, "barz");

// 或者
final Map<Integer, String> map1 = Map.ofEntries(
    entry(1, "foo"),
    entry(2, "bar"),
    entry(3, "baz")
);

Optional 改進

新增 or 方法

Optional 類新增 or() 方法,它與 orElseGet() 類似,只是其引數 lambda 的返回值以及自身的返回值都是 Optional,可以用於從一系列可選值中選取第一個有值的。例如:

jshell> import static java.util.Optional.*;

jshell> empty().or(() -> empty()).or(() -> empty())
$2 ==> Optional.empty

jshell> empty().or(() -> empty()).or(() -> of("Hello")).or(() -> empty())
$3 ==> Optional[Hello]

jshell> empty().or(() -> of("Wolrd")).or(() -> of("Hello")).or(() -> empty())
$4 ==> Optional[Wolrd]

注:只有 Optional 類新添了這個方法,而 OptionalInt 等可選類並未新增。

新增 ifPresentOrElse 方法

在 Java 8 中可選類都實現了 ifPresent,可以很容易實現有值時輸出其內容:

jshell> OptionalDouble.of(Math.PI).ifPresent(System.out::println)
3.141592653589793

但如果要同時實現無值時輸出預設值,它就無能為力了。這時 Java 9 的 ifPresentOrElse 就派上用場了:

jshell> OptionalDouble.of(Math.PI).ifPresentOrElse(System.out::println, () -> System.out.println("No Value"))
3.141592653589793

jshell> OptionalDouble.empty().ifPresentOrElse(System.out::println, () -> System.out.println("No Value"))
No Value

新增 stream 方法

在 Java 8 中如果想對 OptionalInt 等基本型別可選值做流式集合處理會非常麻煩:

jshell> OptionalLong optlong = OptionalLong.of(1L)
optlong ==> OptionalLong[1]

jshell> (optlong.isPresent()? LongStream.of(optlong.getAsLong()): LongStream.empty()).map(x ->x + 1).boxed().collect(Collectors.toList())
$2 ==> [2]

Java 9 新增了 stream() 方法可使其簡化很多:

jshell> optlong.stream().map(x ->x + 1).boxed().collect(Collectors.toList())
$3 ==> [2]

Stream 類改進

新增 ofNullable 工廠方法

在 Java 9 中如需對可空物件進行流式集合處理,可以藉助 Optional 來實現:

jshell> Optional.ofNullable("hello").stream().count()
$1 ==> 1

jshell> Optional.ofNullable(null).stream().count()
$2 ==> 0

另外,Java 9 也為 Stream 類新增了 ofNullable() 方法,上述程式碼還可以簡化為:

jshell> Stream.ofNullable("hello").count()
$3 ==> 1

jshell> Stream.ofNullable(null).count()
$4 ==> 0

注:只有 Stream 類新添了這個方法,而 IntStream 等並未新增。

新增 takeWhiledropWhile 方法

分別用於“取元素直到指定值出現”、“直到指定值出現開始取元素”,例如:

jshell> import static java.util.stream.Collectors.toList;

jshell> IntStream.of(9, 6, 3, 0, 2, 4, 6).takeWhile(n -> n != 0).boxed().collect(toList())
$2 ==> [9, 6, 3]

jshell> IntStream.of(9, 6, 3, 0, 2, 4, 6).dropWhile(n -> n != 0).boxed().collect(toList())
$3 ==> [0, 2, 4, 6]

iterate 方法新增過載

使用 Java 8 的 iterate 以及上述 takeWhile 實現輸出 10 以內的偶數如下:

jshell> IntStream.iterate(0, i -> i + 2).takeWhile(i -> i <= 10).forEach(System.out::println)
0
2
4
6
8
10

不過 Java 9 還為 iterate 新添了支援判斷迭代條件的過載形式,因此上述程式碼可改為:

jshell> IntStream.iterate(0, i -> i <= 10, i -> i + 2).forEach(System.out::println)
0
2
4
6
8
10

程式 API 改進

Java 9 新增了 ProcessHandle 介面,可以很方便地獲得程式的 pid 與程式資訊。ProcessHandle 還提供了兩個靜態方法,分別用於獲取當前程式與指定 pid 的 ProcessHandle 例項。例如:

jshell> ProcessHandle.current().pid()
$1 ==> 27137

jshell> ProcessHandle.current().parent()
$2 ==> Optional[27112]

jshell> ProcessHandle.of(27112).get().children().count()
$3 ==> 1

jshell> ProcessHandle.current().info().command()
$4 ==> Optional[/usr/local/jdk-9.0.4/bin/java]

jshell> ProcessHandle.current().info().totalCpuDuration()
$5 ==> Optional[PT0.81S]

關於這些新特性的更多細節請參見官方文件。

本系列未完待續。

相關文章