全網最通透的Java8版本特性講解

我沒有三顆心臟發表於2020-08-19

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高質量的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取連結,您的支援是我前進的最大的動力!

特性總覽

以下是 Java 8 中的引入的部分新特性。關於 Java 8 新特性更詳細的介紹可參考這裡

  • 介面預設方法和靜態方法
  • Lambda 表示式
  • 函式式介面
  • 方法引用
  • Stream
  • Optional
  • Date/Time API
  • 重複註解
  • 擴充套件註解的支援
  • Base64
  • JavaFX
  • 其它
    • JDBC 4.2 規範
    • 更好的型別推測機制
    • HashMap 效能提升
    • IO/NIO 的改進
    • JavaScript 引擎 Nashorn
    • 併發(Concurrency)
    • 類依賴分析器 jdeps
    • JVM 的 PermGen 空間被移除

一. 介面預設方法和靜態方法

介面預設方法

Java 8 中,允許為介面方法提供一個預設的實現。必須用 default 修飾符標記這樣一個方法,例如 JDK 中的 Iterator 介面:

public interface Iterator<E> {
      boolean hasNext();
      E next();
      default void remove() { throw new UnsupportedOperationExceition("remove"); }
}

這將非常有用!如果你要實現一個迭代器,就需要提供 hasNext()next() 方法。這些方法沒有預設實現——它們依賴於你要遍歷訪問的資料結構。不過,如果你的迭代器是 只讀 的,那麼就不用操心實現 remove() 方法。

預設方法也可以呼叫其他方法,例如,我們可以改造 Collection 介面,定義一個方便的 isEmpty() 方法:

public interface Collection {
      int size(); // an abstract method
      default boolean isEmpty() { return size() == 0; }
}

這樣,實現 Collection 的程式設計師就不用再操心實現 isEmpty() 方法了。

在 JVM 中,預設方法的實現是非常高效的,並且通過位元組碼指令為方法呼叫提供了支援。預設方法允許繼續使用現有的 Java 介面,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被新增到java.util.Collection介面中去:stream()parallelStream()forEach()removeIf()等。儘管預設方法非常強大,但是在使用預設方法時我們需要小心注意一個地方:在宣告一個預設方法前,請仔細思考是不是真的有必要使用預設方法

解決預設方法衝突

如果先在一個介面中將一個方法定義為預設方法,然後又在類或另一個介面中定義同樣的方法,會發生什麼?

// 測試介面 1
public interface TestInterface1 {
    default void sameMethod() { System.out.println("Invoke TestInterface1 method!"); }
}
// 測試介面 2
public interface TestInterface2 {
    default void sameMethod() { System.out.println("Invoke TestInterface2 method!"); }
}
// 繼承兩個介面的測試類
public class TestObject implements TestInterface1, TestInterface2 {

    @Override
    public void sameMethod() {
          // 這裡也可以選擇兩個介面中的一個預設實現
          // 如: TestInterface1.super.sameMethod();
        System.out.println("Invoke Object method!");
    }
}
// 測試類
public class Tester {

    public static void main(String[] args) {
        TestObject testObject = new TestObject();
        testObject.sameMethod();
    }
}

測試輸出:

Invoke Object method!

➡️ 對於 Scale 或者 C++ 這些語言來說,解決這種具有 二義性 的情況規則會很複雜,Java 的規則則簡單得多:

  1. 類優先。如果本類中提供了一個具體方法符合簽名,則同名且具有相同引數列表的介面中的預設方法會被忽略;
  2. 介面衝突。如果一個介面提供了一個預設方法,另一個介面提供了一個同名且引數列表相同的方法 (順序和型別都相同) ,則必須覆蓋這個方法來解決衝突 (就是?程式碼的情況,不覆蓋編譯器不會編譯..)

Java 設計者更強調一致性,讓程式設計師自己來解決這樣的二義性似乎也顯得很合理。如果至少有一個介面提供了一個實現,編譯器就會報告錯誤,程式設計師就必須解決這個二義性。(如果兩個介面都沒有為共享方法提供預設實現,則不存在衝突,要麼實現,要麼不實現..)

➡️ 我們只討論了兩個介面的命名衝突。現在來考慮另一種情況,一個類繼承自一個類,同時實現了一個介面,從父類繼承的方法和介面擁有同樣的方法簽名,又將怎麼辦呢?

// 測試介面
public interface TestInterface {
    default void sameMethod() { System.out.println("Invoke TestInterface Method!"); }
}
// 父類
public class Father {
    void sameMethod() { System.out.println("Invoke Father Method!"); }
}
// 子類
public class Son extends Father implements TestInterface {
    @Override
    public void sameMethod() {
        System.out.println("Invoke Son Method!");
    }
}
// 測試類
public class Tester {
    public static void main(String[] args) { new Son().sameMethod(); }
}

程式輸出:

COPYInvoke Son Method!

還記得我們說過的方法呼叫的過程嗎 (先找本類的方法找不到再從父類找)?加上這裡提到的 “類優先” 原則 (本類中有方法則直接呼叫),這很容易理解!

千萬不要讓一個預設方法重新定義 Object 類中的某個方法。例如,不能為 toString()equals() 定義預設方法,儘管對於 List 之類的介面這可能很有吸引力,但由於 類優先原則,這樣的方法絕對無法超越 Object.toString() 或者 Object.equals()

介面靜態方法

Java 8 中,允許在介面中增加靜態方法 (允許不構建物件而直接使用的具體方法)。理論上講,沒有任何理由認為這是不合法的,只是這有違將介面作為抽象規範的初衷

例子:

public interface StaticInterface {
    static void method() {
        System.out.println("這是Java8介面中的靜態方法!");
    }
}

呼叫:

public class Main {
    public static void main(String[] args) {
        StaticInterface.method(); // 輸出 這是Java8介面中的靜態方法!
    }
}

目前為止,通常的做法都是將靜態方法放在 伴隨類 (可以理解為操作繼承介面的實用工具類) 中。在標準庫中,你可以看到成對出現的介面和實用工具類,如 Collection/ CollectionsPath/ Paths

Java 11 中,Path 介面就提供了一個與之工具類 Paths.get() 等價的方法 (該方法用於將一個 URI 或者字串序列構造成一個檔案或目錄的路徑)

COPYpublic interface Path {
    public static Path of(String first, String... more) { ... }
    public static Path of(URI uri) { ... }
}

這樣一來,Paths 類就不再是必要的了。類似地,如果實現你自己的介面時,沒有理由再額外提供一個帶有實用方法的工具類。

➡️ 另外,在 Java 9 中,介面中的方法可以是 privateprivate 方法可以是靜態方法或例項方法。由於私有方法只能在介面本身的方法中使用,所以它們的用法很有限,只能作為介面中其他方法的輔助方法。

二. Lambda 表示式

Lambda表示式 (也稱為閉包) 是整個 Java 8 發行版中最受期待的在 Java 語言層面上的改變,Lambda 允許把函式作為一個方法的引數,即 行為引數化,函式作為引數傳遞進方法中。

什麼是 Lambda 表示式

我們知道,對於一個 Java 變數,我們可以賦給一個 「值」

如果你想把 「一塊程式碼」 賦給一個 Java 變數,應該怎麼做呢?

比如,我想把右邊的程式碼塊,賦值給一個叫做 blockOfCode 的 Java 變數:

在 Java 8 之前,這個是做不到的,但是 Java 8 問世之後,利用 Lambda 特性,就可以做到了。

當然,這個並不是一個很簡潔的寫法,所以為了讓這個賦值操作變得更加優雅,我們可以移除一些沒有必要的宣告。

這樣,我們就成功的非常優雅的把「一塊程式碼」賦給了一個變數。而「這塊程式碼」,或者說「這個被賦給一個變數的函式」,就是一個 Lambda 表示式

但是這裡仍然有一個問題,就是變數 blockOfCode 的型別應該是什麼?

在 Java 8 裡面,所有的 Lambda 的型別都是一個介面,而 Lambda 表示式本身,也就是「那段程式碼」,需要是這個介面的實現。這是理解 Lambda 的一個關鍵所在,簡而言之就是,Lambda 表示式本身就是一個介面的實現。直接這樣說可能還是有點讓人困擾,我們繼續看看例子。我們給上面的 blockOfCode 加上一個型別:

這種只有一個介面函式需要被實現的介面型別,我們叫它「函式式介面」。

為了避免後來的人在這個介面中增加介面函式導致其有多個介面函式需要被實現,變成「非函式介面」,我們可以在這個上面加上一個宣告 @FunctionalInterface, 這樣別人就無法在裡面新增新的介面函式了:

這樣,我們就得到了一個完整的 Lambda 表示式宣告:

Lambda 表示式的作用

Lambda 最直觀的作用就是使程式碼變得整潔.。

我們可以對比一下 Lambda 表示式和傳統的 Java 對同一個介面的實現:

這兩種寫法本質上是等價的。但是顯然,Java 8 中的寫法更加優雅簡潔。並且,由於 Lambda 可以直接賦值給一個變數,我們就可以直接把 Lambda 作為引數傳給函式, 而傳統的 Java 必須有明確的介面實現的定義,初始化才行。

有些情況下,這個介面實現只需要用到一次。傳統的 Java 7 必須要求你定義一個“汙染環境”的介面實現 MyInterfaceImpl,而相較之下 Java 8 的 Lambda, 就顯得乾淨很多。

三. 函式式介面

上面我們說到,只有一個介面函式需要被實現的介面型別,我們叫它「函式式介面」。Lambda 表示式配合函式式介面能讓我們程式碼變得乾淨許多。

Java 8 API 包含了很多內建的函式式介面,在老 Java 中常用到的比如Comparator或者Runnable介面,這些介面都增加了@FunctionalInterface註解以便能用在Lambda上。

Java 8 API 同樣還提供了很多全新的函式式介面來讓工作更加方便,有一些介面是來自 Google Guava 庫裡的,即便你對這些很熟悉了,還是有必要看看這些是如何擴充套件到 Lambda 上使用的。

1 - Comparator(比較器介面)

Comparator是老Java中的經典介面, Java 8 在此之上新增了多種預設方法。原始碼及使用示例如下:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

2 - Consumer(消費型介面)

Consumer 介面表示執行在單個引數上的操作。原始碼及使用示例如下:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

更多的Consumer介面

  • BiConsumer:void accept(T t, U u);: 接受兩個引數的二元函式
  • DoubleConsumer:void accept(double value);: 接受一個double引數的一元函式
  • IntConsumer:void accept(int value);: 接受一個int引數的一元函式
  • LongConsumer:void accept(long value);: 接受一個long引數的一元函式
  • ObjDoubleConsumer:void accept(T t, double value);: 接受一個泛型引數一個double引數的二元函式
  • ObjIntConsumer:void accept(T t, int value);: 接受一個泛型引數一個int引數的二元函式
  • ObjLongConsumer:void accept(T t, long value);: 接受一個泛型引數一個long引數的二元函式

3 - Supplier(供應型介面)

Supplier 介面是不需要引數並返回一個任意範型的值。其簡潔的宣告,會讓人以為不是函式。這個抽象方法的宣告,同 Consumer 相反,是一個只宣告瞭返回值,不需要引數的函式。也就是說 Supplier 其實表達的不是從一個引數空間到結果空間的對映能力,而是表達一種生成能力,因為我們常見的場景中不止是要consume(Consumer)或者是簡單的map(Function),還包括了 new 這個動作。而 Supplier 就表達了這種能力。原始碼及使用示例如下:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

更多Supplier介面

  • BooleanSupplier:boolean getAsBoolean();: 返回boolean的無參函式
  • DoubleSupplier:double getAsDouble();: 返回double的無參函式
  • IntSupplier:int getAsInt();: 返回int的無參函式
  • LongSupplier:long getAsLong();: 返回long的無參函式

4 - Predicate(斷言型介面)

Predicate 介面只有一個引數,返回 boolean 型別。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:)。Streamfilter 方法就是接受 Predicate 作為入參的。這個具體在後面使用 Stream 的時候再分析深入。原始碼及使用示例如下:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo");            // true
predicate.negate().test("foo");     // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

更多的Predicate介面

  • BiPredicate:boolean test(T t, U u);: 接受兩個引數的二元斷言函式
  • DoublePredicate:boolean test(double value);: 入參為double的斷言函式
  • IntPredicate:boolean test(int value);: 入參為int的斷言函式
  • LongPredicate:boolean test(long value);: 入參為long的斷言函式

5 - Function(功能型介面)

Function 介面有一個引數並且返回一個結果,並附帶了一些可以和其他函式組合的預設方法(compose, andThen)。原始碼及使用示例如下:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

更多的Function介面

  • BiFunction :R apply(T t, U u);: 接受兩個引數,返回一個值,代表一個二元函式;
  • DoubleFunction :R apply(double value);: 只處理double型別的一元函式;
  • IntFunction :R apply(int value);: 只處理int引數的一元函式;
  • LongFunction :R apply(long value);: 只處理long引數的一元函式;
  • ToDoubleFunction:double applyAsDouble(T value);: 返回double的一元函式;
  • ToDoubleBiFunction:double applyAsDouble(T t, U u);: 返回double的二元函式;
  • ToIntFunction:int applyAsInt(T value);: 返回int的一元函式;
  • ToIntBiFunction:int applyAsInt(T t, U u);: 返回int的二元函式;
  • ToLongFunction:long applyAsLong(T value);: 返回long的一元函式;
  • ToLongBiFunction:long applyAsLong(T t, U u);: 返回long的二元函式;
  • DoubleToIntFunction:int applyAsInt(double value);: 接受double返回int的一元函式;
  • DoubleToLongFunction:long applyAsLong(double value);: 接受double返回long的一元函式;
  • IntToDoubleFunction:double applyAsDouble(int value);: 接受int返回double的一元函式;
  • IntToLongFunction:long applyAsLong(int value);: 接受int返回long的一元函式;
  • LongToDoubleFunction:double applyAsDouble(long value);: 接受long返回double的一元函式;
  • LongToIntFunction:int applyAsInt(long value);: 接受long返回int的一元函式;

6 - Operator

Operator 其實就是 Function,函式有時候也叫作運算元。運算元在Java8中介面描述更像是函式的補充,和上面的很多型別對映型函式類似。運算元 Operator 包括:UnaryOperatorBinaryOperator。分別對應單(一)元運算元和二元運算元。

運算元的介面宣告如下:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

Operator只需宣告一個泛型引數 T 即可。對應的使用示例如下:

UnaryOperator<Integer> increment = x -> x + 1;
System.out.println("遞增:" + increment.apply(2)); // 輸出 遞增:3

BinaryOperator<Integer> add = (x, y) -> x + y;
System.out.println("相加:" + add.apply(2, 3)); // 輸出 相加:5

BinaryOperator<Integer> min = BinaryOperator.minBy((o1, o2) -> o1 - o2);
System.out.println("最小值:" + min.apply(2, 3)); // 輸出 最小值:2

更多的Operator介面

  • LongUnaryOperator:long applyAsLong(long operand);: 對long型別做操作的一元運算元
  • IntUnaryOperator:int applyAsInt(int operand);: 對int型別做操作的一元運算元
  • DoubleUnaryOperator:double applyAsDouble(double operand);: 對double型別做操作的一元運算元
  • DoubleBinaryOperator:double applyAsDouble(double left, double right);: 對double型別做操作的二元運算元
  • IntBinaryOperator:int applyAsInt(int left, int right);: 對int型別做操作的二元運算元
  • LongBinaryOperator:long applyAsLong(long left, long right);: 對long型別做操作的二元運算元

7 - 其他函式式介面

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

四. 方法引用

1 - 概述

在學習了 Lambda 表示式之後,我們通常使用 Lambda 表示式來建立匿名方法。然而,有時候我們僅僅是呼叫了一個已存在的方法。如下:

Arrays.sort(strArray, (s1, s2) -> s1.compareToIgnoreCase(s2));

在 Java 8 中,我們可以直接通過方法引用來簡寫 Lambda 表示式中已經存在的方法。

Arrays.sort(strArray, String::compareToIgnoreCase);

這種特性就叫做方法引用(Method Reference)。

方法引用是用來直接訪問類或者例項的已經存在的方法或者構造方法。方法引用提供了一種引用而不執行方法的方式,它需要由相容的函式式介面構成的目標型別上下文。計算時,方法引用會建立函式式介面的一個例項。當 Lambda 表示式中只是執行一個方法呼叫時,不用 Lambda 表示式,直接通過方法引用的形式可讀性更高一些。方法引用是一種更簡潔易懂的 Lambda 表示式。

注意: 方法引用是一個 Lambda 表示式,其中方法引用的操作符是雙冒號::

2 - 分類

方法引用的標準形式是:類名::方法名。(注意:只需要寫方法名,不需要寫括號)

有以下四種形式的方法引用:

  • 引用靜態方法: ContainingClass::staticMethodName
  • 引用某個物件的例項方法: containingObject::instanceMethodName
  • 引用某個型別的任意物件的例項方法:ContainingType::methodName
  • 引用構造方法: ClassName::new

3 - 示例

使用示例如下:

public class Person {

    String name;

    LocalDate birthday;

    public Person(String name, LocalDate birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    @Override
    public String toString() {
        return this.name;
    }
}

測試類:

public class MethodReferenceTest {

    @Test
    public static void main() {
        Person[] pArr = new Person[] {
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))
        };

        // 使用匿名類
        Arrays.sort(pArr, new Comparator<Person>() {
            @Override
            public int compare(Person a, Person b) {
                return a.getBirthday().compareTo(b.getBirthday());
            }
        });

        //使用lambda表示式
        Arrays.sort(pArr, (Person a, Person b) -> {
            return a.getBirthday().compareTo(b.getBirthday());
        });

        //使用方法引用,引用的是類的靜態方法
        Arrays.sort(pArr, Person::compareByAge);
    }

}

五. Stream 流操作

流是 Java8 中 API 的新成員,它允許你以 宣告式 的方式處理資料集合(通過查詢語句來表達,而不是臨時編寫一個實現)。這有點兒像是我們運算元據庫一樣,例如我想要查詢出熱量較低的菜品名字我就可以像下面這樣:

COPYSELECT name FROM dishes WHERE calorie < 400;

您看,我們並沒有對菜品的什麼屬性進行篩選(比如像之前使用迭代器一樣每個做判斷),我們只是表達了我們想要什麼。那麼為什麼到了 Java 的集合中,這樣做就不行了呢?

另外一點,如果我們想要處理大量的資料又該怎麼辦?是否是考慮使用多執行緒進行併發處理呢?如果是,那麼可能編寫的關於併發的程式碼比使用迭代器本身更加的複雜,而且除錯起來也會變得麻煩。

基於以上的幾點考慮,Java 設計者在 Java 8 版本中 (真正把函數語言程式設計風格引入到 Java 中),引入了流的概念,來幫助您節約時間!並且有了 Lambda 的參與,流操作的使用將更加順暢!

1 - 流操作特點

特點一:內部迭代

就現在來說,您可以把它簡單的當成一種高階的迭代器(Iterator),或者是高階的 for 迴圈,區別在於,前面兩者都是屬於外部迭代,而流採用內部迭代。

上圖簡要說明了內部迭代與外部迭代的差異,我們再舉一個生活中實際的例子(引自《Java 8 實戰》),比如您想讓您兩歲的孩子索菲亞把她的玩具都收到盒子裡面去,你們之間可能會產生如下的對話:

  • 你:“索菲亞,我們把玩具收起來吧,地上還有玩具嗎?”
  • 索菲亞:“有,球。”
  • 你:“好,把球放進盒子裡面吧,還有嗎?”
  • 索菲亞:“有,那是我的娃娃。”
  • 你:“好,把娃娃也放進去吧,還有嗎?”
  • 索菲亞:“有,有我的書。”
  • 你:“好,把書也放進去,還有嗎?”
  • 索菲亞:“沒有了。”
  • 你:“好,我們收好啦。”

這正是你每天都要對 Java 集合做的事情。你外部迭代了一個集合,顯式地取出每個專案再加以處理,但是如果你只是跟索菲亞說:“把地上所有玩具都放進盒子裡”,那麼索菲亞就可以選擇一手拿娃娃一手拿球,或是選擇先拿離盒子最近的那個東西,再拿其他的東西。

採用內部迭代,專案可以透明地並行處理,或者用優化的順序進行處理,要是使用 Java 過去的外部迭代方法,這些優化都是很困難的。

這或許有點雞蛋裡挑骨頭,但這差不多就是 Java 8 引入流的原因了——Streams 庫的內部迭代可以自動選擇一種是和你硬體的資料表示和並行實現。

特點二:只能遍歷一次

請注意,和迭代器一樣,流只能遍歷一次。當流遍歷完之後,我們就說這個流已經被消費掉了,你可以從原始資料那裡重新獲得一條新的流,但是卻不允許消費已消費掉的流。例如下面程式碼就會丟擲一個異常,說流已被消費掉了:

List<String> title = Arrays.asList("Wmyskxz", "Is", "Learning", "Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
// 執行上面程式會報以下錯誤
/*
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at Test1.main(Tester.java:17)
*/

特點三:方便的並行處理

Java 8 中不僅提供了方便的一些流操作(比如過濾、排序之類的),更重要的是對於並行處理有很好的支援,只需要加上 .parallel() 就行了!例如我們使用下面程式來說明一下多執行緒流操作的方便和快捷,並且與單執行緒做了一下對比:

COPYpublic class StreamParallelDemo {

    /** 總數 */
    private static int total = 100_000_000;

    public static void main(String[] args) {
        System.out.println(String.format("本計算機的核數:%d", Runtime.getRuntime().availableProcessors()));

        // 產生1000w個隨機數(1 ~ 100),組成列表
        Random random = new Random();
        List<Integer> list = new ArrayList<>(total);

        for (int i = 0; i < total; i++) {
            list.add(random.nextInt(100));
        }

        long prevTime = getCurrentTime();
        list.stream().reduce((a, b) -> a + b).ifPresent(System.out::println);
        System.out.println(String.format("單執行緒計算耗時:%d", getCurrentTime() - prevTime));

        prevTime = getCurrentTime();
        // 只需要加上 .parallel() 就行了
        list.stream().parallel().reduce((a, b) -> a + b).ifPresent(System.out::println);
        System.out.println(String.format("多執行緒計算耗時:%d", getCurrentTime() - prevTime));

    }

    private static long getCurrentTime() {
        return System.currentTimeMillis();
    }
}

以上程式分別使用了單執行緒流和多執行緒流計算了一千萬個隨機數的和,輸出如下:

本計算機的核數:8
655028378
單執行緒計算耗時:4159
655028378
多執行緒計算耗時:540

並行流的內部使用了預設的 ForkJoinPool 分支/合併框架,它的預設執行緒數量就是你的處理器數量,這個值是由 Runtime.getRuntime().availableProcessors() 得到的(當然我們也可以全域性設定這個值)。我們也不再去過度的操心加鎖執行緒安全等一系列問題。

2 - 一些重要方法說明

  • stream: 返回資料流,集合作為其源
  • parallelStream: 返回並行資料流, 集合作為其源
  • filter: 方法用於過濾出滿足條件的元素
  • map: 方法用於對映每個元素對應的結果
  • forEach: 方法遍歷該流中的每個元素
  • limit: 方法用於減少流的大小
  • sorted: 方法用來對流中的元素進行排序
  • anyMatch: 是否存在任意一個元素滿足條件(返回布林值)
  • allMatch: 是否所有元素都滿足條件(返回布林值)
  • noneMatch: 是否所有元素都不滿足條件(返回布林值)
  • collect: 方法是終端操作,這是通常出現在管道傳輸操作結束標記流的結束

3 - 一些使用示例

Filter 過濾

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

Sort 排序

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

Map 對映

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

Match 匹配

boolean anyStartsWithA = stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA);      // true

boolean allStartsWithA = stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ = stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ);      // true

Count 計數

long startsWithB = stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();
System.out.println(startsWithB);    // 3

Reduce 歸約

這是一個最終操作,允許通過指定的函式來將 stream 中的多個元素規約為一個元素,規越後的結果是通過 Optional 介面表示的。程式碼如下:

Optional<String> reduced = stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);

想了解更多請參考:https://www.wmyskxz.com/2019/08/03/java8-liu-cao-zuo-ji-ben-shi-yong-xing-neng-ce-shi/

六. Optional

到目前為止,臭名昭著的空指標異常是導致 Java 應用程式失敗的最常見原因。以前,為了解決空指標異常,Google公司著名的 Guava 專案引入了 Optional 類,Guava 通過使用檢查空值的方式來防止程式碼汙染,它鼓勵程式設計師寫更乾淨的程式碼。受到 Google Guava 的啟發,Optional類已經成為 Java 8 類庫的一部分。

Optional 實際上是個容器:它可以儲存型別 T 的值,或者僅僅儲存 null。Optional 提供很多有用的方法,這樣我們就不用顯式進行空值檢測。

我們下面用兩個小例子來演示如何使用 Optional 類:一個允許為空值,一個不允許為空值。

Optional<String> fullName = Optional.ofNullable(null);
System.out.println("Full Name is set? " + fullName.isPresent());
System.out.println("Full Name: " + fullName.orElseGet(() -> "[none]"));
System.out.println(fullName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));

如果 Optional 類的例項為非空值的話,isPresent() 返回 true,否從返回 false。為了防止 Optional 為空值,orElseGet() 方法通過回撥函式來產生一個預設值。map() 函式對當前 Optional 的值進行轉化,然後返回一個新的 Optional 例項。orElse() 方法和 orElseGet() 方法類似,但是 orElse 接受一個預設值而不是一個回撥函式。下面是這個程式的輸出:

Full Name is set? false
Full Name: [none]
Hey Stranger!

讓我們來看看另一個例子:

Optional<String> firstName = Optional.of("Tom");
System.out.println("First Name is set? " + firstName.isPresent());
System.out.println("First Name: " + firstName.orElseGet(() -> "[none]"));
System.out.println(firstName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));
System.out.println();

下面是程式的輸出:

First Name is set? true
First Name: Tom
Hey Tom!

Lambda 配合 Optinal 優雅解決 null

這裡假設我們有一個 person object,以及一個 person object 的 Optional wrapper:

Optional<T> 如果不結合 Lambda 使用的話,並不能使原來繁瑣的 null check 變的簡單。

只有當 Optional<T> 結合 Lambda 一起使用的時候,才能發揮出其真正的威力!

我們現在就來對比一下下面四種常見的 null 處理中,Java 8 的 Lambda + Optional<T> 和傳統 Java 兩者之間對於 null 的處理差異。

情況一:存在則繼續

情況二:存在則返回,無則返回不存在

情況三:存在則返回,無則由函式產生

情況四:奪命連環 null 檢查

由上述四種情況可以清楚地看到,Optional<T> + Lambda 可以讓我們少寫很多 ifElse 塊。尤其是對於情況四那種奪命連環 null 檢查,傳統 Java 的寫法顯得冗長難懂,而新的 Optional<T> +Lambda 則清新脫俗,清楚簡潔。

七. Data/Time API

Java 8 在包 java.time 下包含了一組全新的時間日期API。新的日期API和開源的 Joda-Time 庫差不多,但又不完全一樣,下面的例子展示了這組新API裡最重要的一些部分:

1 - Clock 時鐘

Clock 類提供了訪問當前日期和時間的方法,Clock 是時區敏感的,可以用來取代 System.currentTimeMillis() 來獲取當前的微秒數。某一個特定的時間點也可以使用 Instant 類來表示,Instant 類也可以用來建立老的 java.util.Date 物件。程式碼如下:

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

2 - Timezones 時區

在新 AP I中時區使用 ZoneId 來表示。時區可以很方便的使用靜態方法 of 來獲取到。時區定義了到 UTS 時間的時間差,在 Instant 時間點物件到本地日期物件之間轉換的時候是極其重要的。程式碼如下:

System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

3 - LocalTime 本地時間

LocalTime 定義了一個沒有時區資訊的時間,例如 晚上 10 點,或者 17:30:15。下面的例子使用前面程式碼建立的時區建立了兩個本地時間。之後比較時間並以小時和分鐘為單位計算兩個時間的時間差。程式碼如下:

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2));  // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

LocalTime 提供了多種工廠方法來簡化物件的建立,包括解析時間字串。程式碼如下:

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59
DateTimeFormatter germanFormatter = DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

4 - LocalData 本地日期

LocalDate 表示了一個確切的日期,比如 2014-03-11。該物件值是不可變的,用起來和 LocalTime 基本一致。下面的例子展示瞭如何給 Date 物件加減天/月/年。另外要注意的是這些物件是不可變的,操作返回的總是一個新例項。程式碼如下:

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();

System.out.println(dayOfWeek);    // FRIDAY

從字串解析一個 LocalDate 型別和解析 LocalTime 一樣簡單。程式碼如下:

DateTimeFormatter germanFormatter = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

5 - LocalDateTime 本地日期時間

LocalDateTime同時表示了時間和日期,相當於前兩節內容合併到一個物件上了。LocalDateTimeLocalTime還有LocalDate一樣,都是不可變的。LocalDateTime提供了一些能訪問具體欄位的方法。程式碼如下:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

只要附加上時區資訊,就可以將其轉換為一個時間點Instant物件,Instant時間點物件可以很容易的轉換為老式的java.util.Date。程式碼如下:

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

格式化LocalDateTime和格式化時間和日期一樣的,除了使用預定義好的格式外,我們也可以自己定義格式。程式碼如下:

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

java.text.NumberFormat不一樣的是新版的DateTimeFormatter是不可變的,所以它是執行緒安全的。

八. 重複註解

自從 Java 5 引入了註解機制,這一特性就變得非常流行並且廣為使用。然而,使用註解的一個限制是相同的註解在同一位置只能宣告一次,不能宣告多次。Java 8 打破了這條規則,引入了重複註解機制,這樣相同的註解可以在同一地方宣告多次。

重複註解機制本身必須用 @Repeatable 註解。事實上,這並不是語言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個快速入門的例子:

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class RepeatingAnnotations {

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filters {
        Filter[] value();
    }

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Filters.class)
    public @interface Filter {
        String value();
    };

    @Filter("filter1")
    @Filter("filter2")
    public interface Filterable {
    }

    public static void main(String[] args) {
        for(Filter filter: Filterable.class.getAnnotationsByType(Filter.class)) {
            System.out.println(filter.value());
        }
    }

}

正如我們看到的,這裡有個使用 @Repeatable(Filters.class) 註解的註解類 FilterFilters 僅僅是 Filter 註解的陣列,但Java編譯器並不想讓程式設計師意識到 Filters 的存在。這樣,介面 Filterable 就擁有了兩次 Filter(並沒有提到Filter)註解。

同時,反射相關的API提供了新的函式getAnnotationsByType()來返回重複註解的型別(請注意Filterable.class.getAnnotation(Filters.class)`經編譯器處理後將會返回Filters的例項)。

九. 擴充套件註解的支援

Java 8 擴充套件了註解的上下文。現在幾乎可以為任何東西新增註解:區域性變數、泛型類、父類與介面的實現,就連方法的異常也能新增註解。下面演示幾個例子:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;

public class Annotations {

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
    public @interface NonEmpty {
    }

    public static class Holder<@NonEmpty T> extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {
        }
    }

    @SuppressWarnings("unused")
    public static void main(String[] args) {
        final Holder<String> holder = new @NonEmpty Holder<String>();
        @NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();
    }

}

十. Base64

在 Java 8 中,Base64 編碼已經成為 Java 類庫的標準。它的使用十分簡單,下面讓我們看一個例子:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64s {

    public static void main(String[] args) {
        final String text = "Base64 finally in Java 8!";

        final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
        System.out.println(encoded);

        final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
        System.out.println(decoded);
    }

}

程式在控制檯上輸出了編碼後的字元與解碼後的字元:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

Base64 類同時還提供了對 URL、MIME 友好的編碼器與解碼器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

十一. JavaFX

JavaFX是一個強大的圖形和多媒體處理工具包集合,它允許開發者來設計、建立、測試、除錯和部署富客戶端程式,並且和Java一樣跨平臺。從Java8開始,JavaFx已經內建到了JDK中。關於JavaFx更詳細的文件可參考JavaFX中文文件

十二. 其它

1. JDBC4.2規範

JDBC4.2主要有以下幾點改動:

  • 增加了對REF Cursor的支援
  • 修改返回值大小範圍(update count)
  • 增加了java.sql.DriverAction介面
  • 增加了java.sql.SQLType介面
  • 增加了java.sql.JDBCtype列舉
  • java.time包時間型別的支援

2. 更好的型別推測機制

Java 8 在型別推測方面有了很大的提高。在很多情況下,編譯器可以推測出確定的引數型別,這樣就能使程式碼更整潔。讓我們看一個例子:

public class Value<T> {

    public static<T> T defaultValue() {
        return null;
    }

    public T getOrDefault(T value, T defaultValue) {
        return (value != null) ? value : defaultValue;
    }

}

這裡是Value<String>型別的用法。

public class TypeInference {

    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault("22", Value.defaultValue());
    }

}

Value.defaultValue()的引數型別可以被推測出,所以就不必明確給出。在Java 7中,相同的例子將不會通過編譯,正確的書寫方式是Value.<String>defaultValue()

3. HashMap效能提升

Java 8 中,HashMap 內部實現又引入了紅黑樹,使得 HashMap 的總體效能相較於 Java 7 有比較明顯的提升。以下是對 Hash 均勻和不均勻的情況下的效能對比

Hash較均勻的情況

Hash較均勻時的效能對比

Hash極不均勻的情況

Hash極不均勻時的效能對比

想要了解更多 HashMap 的童鞋戳這裡吧:傳送門

4. IO/NIO 的改進

Java 8 對IO/NIO也做了一些改進。主要包括:改進了java.nio.charset.Charset的實現,使編碼和解碼的效率得以提升,也精簡了jre/lib/charsets.jar包;優化了String(byte[], *)構造方法和String.getBytes()方法的效能;還增加了一些新的IO/NIO方法,使用這些方法可以從檔案或者輸入流中獲取流(java.util.stream.Stream),通過對流的操作,可以簡化文字行處理、目錄遍歷和檔案查詢。

新增的 API 如下:

  • BufferedReader.line(): 返回文字行的流Stream<String>
  • File.lines(Path, Charset): 返回文字行的流Stream<String>
  • File.list(Path): 遍歷當前目錄下的檔案和目錄
  • File.walk(Path, int, FileVisitOption): 遍歷某一個目錄下的所有檔案和指定深度的子目錄
  • File.find(Path, int, BiPredicate, FileVisitOption...): 查詢相應的檔案

下面就是用流式操作列出當前目錄下的所有檔案和目錄:

Files.list(new File(".").toPath()).forEach(System.out::println);

5. JavaScript 引擎 Nashorn

Java 8 提供了一個新的Nashorn javascript引擎,它允許我們在 JVM 上執行特定的 javascript 應用。Nashorn javascript 引擎只是javax.script.ScriptEngine另一個實現,而且規則也一樣,允許Java和JavaScript互相操作。這裡有個小例子:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");

System.out.println(engine.getClass().getName());
System.out.println("Result:" + engine.eval("function f(){return 1;}; f() + 1;"));

輸出如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2

6. 併發(Concurrency)

在新增Stream機制與Lambda的基礎之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支援聚集操作。同時也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支援共有資源池(common pool)(請檢視我們關於Java 併發的免費課程)。

新增的java.util.concurrent.locks.StampedLock類提供一直基於容量的鎖,這種鎖有三個模型來控制讀寫操作(它被認為是不太有名的java.util.concurrent.locks.ReadWriteLock類的替代者)。

java.util.concurrent.atomic包中還增加了下面這些類:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

7. 類依賴分析器jdeps

Jdeps是一個功能強大的命令列工具,它可以幫我們顯示出包層級或者類層級java類檔案的依賴關係。它接受class檔案、目錄、jar檔案作為輸入,預設情況下,jdeps會輸出到控制檯。

作為例子,讓我們看看現在很流行的 Spring 框架的庫的依賴關係報告。為了讓報告短一些,我們只分析一個 jar: org.springframework.core-3.0.5.RELEASE.jar.

jdeps org.springframework.core-3.0.5.RELEASE.jar這個命令輸出內容很多,我們只看其中的一部分,這些依賴關係根絕包來分組,如果依賴關係在classpath裡找不到,就會顯示 not found。

C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io
      -> java.lang
      -> java.lang.annotation
      -> java.lang.ref
      -> java.lang.reflect
      -> java.util
      -> java.util.concurrent
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang
      -> java.lang.annotation
      -> java.lang.reflect
      -> java.util

8. JVM 的 PermGen 空間被移除

PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。JVM 選項-XX:PermSize-XX:MaxPermSize分別被-XX:MetaSpaceSize-XX:MaxMetaspaceSize所代替。

區別:

  1. 元空間並不在虛擬機器中,而是使用本地記憶體
  2. 預設情況下,元空間的大小僅受本地記憶體限制
  3. 也可以通過-XX:MetaspaceSize指定元空間大小

參考資料

  1. 「MoreThanJava」Day 7:介面詳解 - https://www.wmyskxz.com/2020/08/13/morethanjava-day-7-jie-kou-xiang-jie/
  2. 【知乎問題】Lambda 表示式 有何用處?如何使用? | @Mingqi - https://www.zhihu.com/question/20125256
  3. Java8新特性及使用(一) | 閃爍之狐 - http://blinkfox.com/2018/11/13/hou-duan/java/java8-xin-te-xing-ji-shi-yong-yi/#toc-heading-21
  4. Java8新特性及使用(二) | 閃爍之狐 - http://blinkfox.com/2018/11/14/hou-duan/java/java8-xin-te-xing-ji-shi-yong-er/

文章推薦

  1. 這都JDK15了,JDK7還不瞭解? - https://www.wmyskxz.com/2020/08/18/java7-ban-ben-te-xing-xiang-jie/
  2. 你記筆記嗎?關於最近知識管理工具革新潮心臟有話要說 - https://www.wmyskxz.com/2020/08/16/ni-ji-bi-ji-ma-guan-yu-zui-jin-zhi-shi-guan-li-gong-ju-ge-xin-chao-xin-zang-you-hua-yao-shuo/
  3. 黑莓OS手冊是如何詳細闡述底層的程式和執行緒模型的? - https://www.wmyskxz.com/2020/07/31/hao-wen-tui-jian-hei-mei-os-shou-ce-shi-ru-he-xiang-xi-chan-shu-di-ceng-de-jin-cheng-he-xian-cheng-mo-xing-de/
  4. 「MoreThanJava」系列文集 - https://www.wmyskxz.com/categories/MoreThanJava/
  • 本文已收錄至我的 Github 程式設計師成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名部落格:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裡,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見!

相關文章