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

  • 「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);

想了解更多請參考://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:接口詳解 – //www.wmyskxz.com/2020/08/13/morethanjava-day-7-jie-kou-xiang-jie/
  2. 【知乎問題】Lambda 表達式 有何用處?如何使用? | @Mingqi – //www.zhihu.com/question/20125256
  3. Java8新特性及使用(一) | 閃爍之狐 – //blinkfox.com/2018/11/13/hou-duan/java/java8-xin-te-xing-ji-shi-yong-yi/#toc-heading-21
  4. Java8新特性及使用(二) | 閃爍之狐 – //blinkfox.com/2018/11/14/hou-duan/java/java8-xin-te-xing-ji-shi-yong-er/

文章推薦

  1. 這都JDK15了,JDK7還不了解? – //www.wmyskxz.com/2020/08/18/java7-ban-ben-te-xing-xiang-jie/
  2. 你記筆記嗎?關於最近知識管理工具革新潮心臟有話要說 – //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手冊是如何詳細闡述底層的進程和線程模型的? – //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」系列文集 – //www.wmyskxz.com/categories/MoreThanJava/
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

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

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