選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
26 分鐘閱讀時間 (5175 個字)

深入淺出 Java 8 新語法特性

java

為什麼要寫這篇文章?

在開發公司產品時,原先使用的是 Java 7,隨著架構越來越龐大,程式碼也越來越冗長,有時候寫起一些邏輯,或是想設計一些封裝時,經常受限於 Java 7 的語法結構,因此最後決定將產品的 Java 版本提升至 Java 8,換成 Java 8 後,不但開發速度變快,Bug 變少了 ,連考試都考了一百分呢! 因此想藉著這篇文章推廣 Java 8 的語法特性,讓還沒轉換至 Java 8,或者是已經轉換至 Java 8 卻較少使用 Java 8 的開發人員,可以一起來使用 Java 8 強大、方便的新功能!

Java 小知識

JDK? OpenJDK?

Java 由 Sun 創造,為了讓對 Java 興趣的廠商、組織、開發者與使用者參與定義Java未來的功能與特性,Sun 公司於 1998 年組成了 JCP(Java Community Process),目的是讓 Java 演進由 Sun 非正式地主導,成為全世界數以百計代表成員公開監督的過程。

任何想要提議加入 Java 的功能或特性,必須以 JSR(Java Specification Requests) 正式文件的方式提交,JSR 必須經過 JCP 執行委員會投票通過後才可成為最終標準文件,有興趣的廠商或組織可以根據 JSR 實現產品。

若 JSR 成為最終文件後,必須根據 JSR 實作出免費且開發原始碼的參考實現,稱為 RI (Reference Implementation),並提供 TCK(Technology Compatibility Kit)作為技術相容測試工具箱,方便其它想根據 JSR 實現產品的廠商或組織參考與測試相容性。

在 JSR 所規範的標準之下,各廠商可以各自實作成品,所以同一份 JSR,可以有不同廠商的實作產品,以 Java SE 為例,對於身為開發人員,或使用 Java 開發產品的公司而言,只要是使用相容於標準的 JDK/JRE 開發產品,就可以執行於相容於標準的 JRE 上,而不用擔心跨平台的問題。

2006 年的 JavaOne 大會上,Sun 宣告對 Java 開放原始碼,從 JDK7 b10 開始有了OpenJDK,並於2009年4月15日正式發佈 OpenJDK。Oracle 時代發佈的 JDK7 正式版本,指定了OpenJDK7 為官方參考實作。

簡單來說,OpenJDK 是由 JDK 開源而來,JDK 常見的參考實作之一為 Oracle JDK,隨著 JDK 8 後 Oracle 的商業授權問題(Java依舊免費?)發生,陸續有其他廠商的 OpenJDK Builds 出現。


Java 版本沿革

Java 版本命名規則為:Java [特性版本號碼]u[修補版本號碼],其中特性版本號碼異動代表有JVM、語言、程式庫等各種更新,而修補版本號碼異動則代表有一些安全性修補、效能增強或工具更新等。

Java 8 及更早版本中,安全性修補的釋出會是基於5的倍數,遇偶數加一,例如8u5、8u11、8u25、8u31等,特性釋出版本會是20的倍數,像是8u20、8u40、8u60等,則會包含先前的安全性修補,以及不影響規格書的特性;自 Java 9 開始,安全性修補版本改為每三個月釋出一次,而特性版本改為每六個月釋出一次。

其中特性版本也有區分是否為長期支援(Long-Term-Support, LTS) 版本,如 Java 9、10 都是短期支援版本,在下個小版本釋出之後,上個版本就不再維護,使用者要趕快更換至新版本,而長期支援版本除了顧名思義支援的時間較久外,同時也會包含前幾個特性版本的相關安全性修復,因此對企業而言,通常會採行目前的長期支援版本進行相關的系統開發維運。 目前 Java 的長期版本有 Java 8(Oracle 預計支援至 2030/12) 及 Java 11(Oracle 預計支援至 2026/09)

Java 目前最新的版本為 Java 14,已引進許多新的語法特性,下一個長期版本則預計為 2021/09 發布的 Java 17。

Java版本歷史

Java 8 新語法特性

Optional

Java 8 中新增了 java.util.Optional 類別,希望透過這個類別降低程式中發生 NullPointerException 的機率。

Optional 出現以前…

在下面的程式中,如果呼叫 getNickName() 時忘了檢查 null,那麼就會直接顯示 null,在這個簡單的例子中並不會怎樣,只是顯示結果令人困惑罷了,但如果後續的執行流程牽涉到至關重要的結果,程式快樂地繼續執行下去,錯誤可能到最後才會呈現發生。

public static void main(String[] args) {
    String nickName = getNickName("Duke");
    if (nickName == null) {
        nickName = "Openhome Reader";
    }
    out.println(nickName);
}

private static String getNickName(String name) {
    Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
    nickNames.put("Justin", "caterpillar");
    nickNames.put("Monica", "momor");
    nickNames.put("Irene", "hamimi");
    return nickNames.get(name); // 鍵不存在時會傳回null
}

在日常的開發過程中,我們有時候需要對呼叫其他 method 取得的回傳值進行 null 檢驗,確保回傳值不為 null,而且經常會忘記做這件事情,而導致後續的例外產生,但有時候回傳值又真的有可能為 null,在這些情境下,針對回傳值的檢查顯得複雜又不易維護。

Optional 出現以後…

Java 8 中新增的 Optional 類別出現後,這個問題變得簡單許多,上面的程式改寫為 Optional 後,會變成下面的寫法:

public static void main(String[] args) {
    Optional<String> nickNameOptional = getNickName("Duke");
    String nickName;
    if(nickNameOptional.isPresent()) {
        nickName = nickNameOptional.get();
    } else {
        nickName = "Openhome Reader";
    }

    System.out.println(nickName);
}

private static Optional<String> getNickName(String name) {
    Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
    nickNames.put("Justin", "caterpillar");
    nickNames.put("Monica", "momor");
    nickNames.put("Irene", "hamimi");

    String nickName = nickNames.get(name);

    if(nickName == null) {
        return Optional.empty();
    } else {
        return Optional.of(nickName);
    }
}

在 Java 8 Optional 的寫法中,getNickName 這個方法清楚定義了回傳的型別為 Optional,字面上的意思看起來像是「可選擇的字串」,顧名思義就是代表這個方法回傳的字串可能有值,也可能沒有值,因此後續 main 方法接到回傳值後,可以很清楚的根據回傳值是否存在做後續的處理。而在有使用 Optional 的情境中,如果 getNickName 回傳的型別為 String,就代表這個方法不應該會回傳 null,而是至少會回傳空字串,因此就不需要針對這個方法的回傳值進行 null check。

Optional 更進一步能怎麼寫…

除了上述的寫法外,Optional 其實也可以在寫得更精簡一些,如:

public static void main(String[] args) {
    Optional<String> nickNameOptional = getNickName("Duke");
    System.out.println(nickNameOptional.orElse("Openhome Reader"));
}

private static Optional<String> getNickName(String name) {
    Map<String, String> nickNames = new HashMap<>(); // 假裝的鍵值資料庫
    nickNames.put("Justin", "caterpillar");
    nickNames.put("Monica", "momor");
    nickNames.put("Irene", "hamimi");

    String nickName = nickNames.get(name);

    return Optional.ofNullable(nickName);
}

在 getNickName 方法中,使用 Optional.ofNullable() 包裹住 nickName,如果 nickName 不是 null 就會呼叫 of() 方法,如果 nickName 是 null,則會呼叫 empty() 方法,從上述的程式結果來看,此處會呼叫 empty() 回傳空的 Optional。

而在 main 方法中,則是使用了 Optional.orElse() ,意思為:如果 Optional 不為 empty 則回傳 Optional 中的值,如果 Optional 為 empty 則回傳 orElse 中的值,從上述的程式結果來看,因為從 getNickName 取得的 nickNameOptional 為 empty Optional,因此 nickNameOptional.orElse("Openhome Reader") 實際上會得到 "Openhome Reader"。


介面的靜態方法及預設方法

Java 8 在介面上也引入了一些新的語言特性:

  • 靜態方法 (Static Methods),靜態方法允許介面提供靜態方法,讓呼叫端可以靜態呼叫
  • 預設方法 (Default Methods),預設方法讓我們可以在既有的介面上新增方法,並能確保與實作舊版本介面編譯成的類別檔相容

預設方法是什麼?

一般常見 java 的介面應該只會有方法的介面,或者是提供靜態方法讓呼叫端以靜態的方式呼叫,而當有類別實作某個介面時,就必須同時也實作該介面的所有方法介面,如:

public interface InterfaceA {
    void foo();

    static void bar() {
        System.out.println("bar");
    }
}

public class ClassA implements InterfaceA {

    @Override
    void foo() {
        System.out.println("foo");
    }
}

public class classB {

    public void method1() {
        // 一般呼叫
        InterfaceA a = new ClassA();
        a.foo();

        // 靜態呼叫
        InterfaceA.bar();
    }
}

預設方法則允許在介面上直接新增方法的實作,實作該介面的類別可以不實作預設方法,也可以覆寫預設方法,呼叫端呼叫預設方法時就如同一般呼叫一樣簡單,如:

public interface InterfaceA {
    void foo();

    static void bar() {
        System.out.println("bar");
    }

    default void baz() {
        System.out.println("baz");
    }
}

public class ClassA implements InterfaceA {

    @Override
    void foo() {
        System.out.println("foo");
    }

    // 也可覆寫預設方法
    // @Override
    // void baz() {
    //     System.out.println("override baz");
    // }
}

public class classB {

    public void method1() {
        // 一般呼叫
        InterfaceA a = new ClassA();
        a.foo();

        // 靜態呼叫
        InterfaceA.bar();

        // 呼叫預設方法
        a.baz();
    }
}

為什麼介面需要提供靜態方法和預設方法?

在 Java 8 預設方法出現之前,介面與其實作類別之間的耦合度很高,當需要為一個介面新增方法時,所有的實作類別都必須隨之修改新增實作。預設方法可以為介面新增新的方法,而無須修改既有的介面的相關實作,因此不只解決了這個問題,同時也讓既有的舊介面保持了向後相容的彈性。
舉例來說,在 Java 8 中,Collection 或 List 繼承的 Iterable 介面原本沒有forEach() 方法,Java 8 為了增加 Lambda 語法的可用性,所以加入了 forEach() 方法。如果沒有預設方法,則實作此介面的類別將都必須實作forEach()方法,使得在擴展舊介面變得相對困難。
而介面提供靜態方法也是基於同一個目的 - 讓既有的舊介面保持了向後相容的彈性,透過介面新增靜態方法可以讓介面額外的方法,同時又不影響既有的實作類別,與預設方法不同的是,預設方法允許實作類別修改其行為,但靜態方法則不允許實作類別修改其行為。
參考 Java 8 的 Comparator Interface 片段:

public interface Comparator<T> {
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }
}

其中 reversed() 方法為預設方法,而 naturalOrder() 為靜態方法,呼叫端呼叫 reversed() 時,預期的是原先的排序會反轉,但有可能受到實作類別的不同而有不同的排序方式,因此這個方法做成預設方法,允許實作類別覆寫。而 naturalOrder 代表的是自然排序,應該不管是任何類別實作時都是預設使用 Object 的自然排序進行排序,不能讓實作類別覆寫,因此做成靜態方法。

預設方法 vs. 多重實作

在 Java 的世界中,允許多重實作,意即一個類別可以實作多個介面,如:

public interface InterfaceA {
    void foo();
}

public interface InterfaceB {
    void bar();
}

public class ClassC implements InterfaceA, InterfaceB {

    @Override
    public void foo() {
        System.out.println("foo");
    }

    @Override
    public void bar() {
        System.out.println("bar");
    }
}

那如果類別實作的多個介面中,恰巧存在 method signature 相同的預設方法呢?

public interface InterfaceA {
    void foo();

    default void baz() {
        System.out.println("baz A");
    }
}

public interface InterfaceB {
    void bar();

    default void baz() {
        System.out.println("baz B");
    }
}

當有類別同時實作具有相同預設方法的介面時,該類別必須一定要覆寫該預設方法,並可以如繼承般呼叫實作介面的預設方法,如:

public class ClassC implements InterfaceA, InterfaceB {

    @Override
    public void foo() {
        System.out.println("foo");
    }

    @Override
    public void bar() {
        System.out.println("bar");
    }

    @Override
    public void baz() {
        InterfaceA.super.baz(); // 呼叫 InterfaceA 的 baz 方法
        InterfaceB.super.baz(); // 呼叫 InterfaceB 的 baz 方法
        System.out.println("ClassC baz"); // 做其他的事
    }
}

Lambda

Lambda 是什麼?

在不同領域,對於 Lambda 的定義可能不太相同,但概念大致相同:Lambda 為一個函數,可以根據輸入的值,決定輸出的值,但 Lambda 與一般函數不同的是,Lambda 具有匿名性,意即我們不需要為 Lambda 函數命名,僅須關注 Lambda 的 input / logic / output 為何即可。

舉例來說,原先 Java 中的 Runnable 可以這麼實作:

Runnable myRunnable = new Runnable() {

    @Override
    public void run() {
        System.out.println("run me!");
    }
};

其中 Runnable 是個介面,並有唯一一個方法介面:void run(),實作此介面時需要一併實作此方法介面。

從 Lambda 的精神來看,我們僅須關注 Runnable 的 input / logic / output:

  • input: 沒有 input
  • logic: 執行的邏輯
  • output: 沒有 output (void)
    因此在 Lambda 語法中,可以輕易地將實作 Runnable 的寫法改為下方寫法,不但語法精簡許多,同時也能將焦點關注在方法實際的邏輯上。
Runnable runnbale = () -> System.out.println("run me!");

為什麼要寫 Lambda?

  • 語法精簡
    在 Java 中,許多只有一個方法的介面,如果要使用這些介面,往往需要寫個幾行程式碼才能完成,改用 Lambda 後只須短短一兩行
  • 效能提昇
    有別於一般 new 出物件實體後呼叫的行為,Lambda 在執行時不會 new 出一個物件實體,而是直接將 Lambda 的 body 程式碼存放在記憶體後直接呼叫,ByteCode 長度因此減少,效率提昇
    如下列程式中,分別將 Runnable new 出來及以 Lambda 使用 Runnable 來看:
    package com.kuma.demo;

    public class LambdaDemo {

        public static void main(String[] args) {

            Runnable myRunnable = new Runnable() {

                @Override
                public void run() {
                    System.out.println("run me!");

                }
            };

            Runnable myLambdaRunnable = () -> System.out.println("run me!");
            
            myRunnable.run();
            myLambdaRunnable.run();
        }
    }

經過編譯後得到的 ByteCode 如下:

public class com.kuma.demo.LambdaDemo {
      public com.kuma.demo.LambdaDemo();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return

      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class com/kuma/demo/LambdaDemo$1
           3: dup
           4: invokespecial #3                  // Method com/kuma/demo/LambdaDemo$1."<init>":()V
           7: astore_1
           8: invokedynamic #4,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
          13: astore_2
          14: aload_1
          15: invokeinterface #5,  1            // InterfaceMethod java/lang/Runnable.run:()V
          20: aload_2
          21: invokeinterface #5,  1            // InterfaceMethod java/lang/Runnable.run:()V
          26: return
    }

可以看到不使用 Lambda 時產生的 ByteCode 為:

0: new           #2                  // class com/kuma/demo/LambdaDemo$1
    3: dup
    4: invokespecial #3                  // Method com/kuma/demo/LambdaDemo$1."<init>":()V
    ```
    而使用 Lambda 時產生的 ByteCode 則僅僅只有
    ```
    8: invokedynamic #4,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

Functional Interface

文章前面提到的「只有一個方法的介面」一般被稱為 Single Abstract Method type(SAM),在 Java 8 以前就存在許多這樣的介面,在 Java 8 後,各式各樣的 SAM 更如雨後春筍般冒出,而 SAM 在 Java 8 有了一個更確切的名字,叫做「Functional Interface(函式介面)」。函式介面本質上就是介面,但這個介面要求僅具單一抽象方法,在 Java 8 中常見的 Functional Interface 如下:

public interface Consumer<T> {
    void accept(T t);
}

Consumer 的行為是接受一個引數 T,處理後不回傳值

public interface Supplier<T> {
    T get();
}

Supplier 的行為是沒有引數,處理後回傳值 T

public interface Function<T, R> {
    R apply(T t);
}

Function 的行為是接受一個引數 T,處理後回傳值 R

public interface Predicate<T> {
    boolean test(T t);
}

Predicate 的行為與 Function 類似,都是接受一個引數 T,處理後回傳值,差別在於 Predicate 的回傳型別為 boolean

除了上述提到 Function 的函式介面外,Java 8 還提供許多與基本型別操作有關的 Function 函式介面,如:

  • IntFunction
  • LongFunction
  • DoubleFunction
  • IntToDoubleFunction
  • IntToLongFunction
  • LongToDoubleFunction
  • LongToIntFunction
  • DoubleToIntFunction
  • DoubleToLongFunction
    這些介面名稱本身就足以解釋其行為,在這邊就不一一贅述了。

同時也針對會使用到引述的函式介面(Consumer, Function, Predicate) 等,提供了以 Bi 開頭的相關函式介面(BiConsumer, BiFunction, BiPredicate),讓原先的引數可以變為兩個,內容如下:

public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

BiConsumer 的行為是接受兩個引數 T, U,處理後不回傳值

public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

BiFunction 的行為是接受兩個引數 T, U,處理後回傳值 R

public interface BiPredicate<T, U> {
    boolean apply(T t, U u);
}

BiPredicate 的行為是接受兩個引數 T, U,處理後回傳 boolean

原則上在一般日常的使用情境中,上述的函式介面應該以充分夠用,如果現在有一個需求是需要提供一個傳入六個引數,回傳一個值的 Function 呢?
在這之前應該要先想想,是要傳入六個引數,還是重構一下將六個引數封裝後,改為使用 Function。

如果有這種需求,其實只需要建立一個函式介面如下即可:

@FunctionalInterface
public interface SixParametersFunction<A, B, C, D, E, F, R> {

    R apply(A a, B b, C c, D d, E e, F f);

}

Lambda 語法結構

Lambda 的語法結構,簡而言之可以表示為:

input -> body

其中 input 和 body 都各有多種寫法,列舉如下:

  • input
    • (): 沒有引數
    • (X x): 一個 X 型別的引數
    • (x): 一個引數,省略型別
    • (X x, Y y): 兩個引數,分別為 X 和 Y 型別
    • (x, y): 兩個引數,省略型別
  • body
    • {}: 什麼都不做
    • {System.out.println("foo")}: 單行不回傳值
    • x + y: 單行回傳值
    • 多行不回傳值
      { System.out.println("NO"); System.out.println("NO2"); }
    • 多行回傳值
      { x++; y++; return x + y; }
      將前述的函式介面以 Lambda 實作後,可以發現程式碼十分簡潔:
Supplier<String> emptyStringSupplier = () -> "";
Consumer<String> printingConsumer = x -> System.out.println(x);
Function<Integer, Integer> incrementFunction = x -> x++;
Predicate<Integer> evenPredicate = x -> x % 2 == 0;
BiConsumer<String, String> printingAllConsumer = (x,y) -> {
    System.out.println(x);
    System.out.println(y);
};
BiFunction<Integer, Integer, Integer> sumFunction = (x, y) -> x + y;
BiPredicate<Integer, Integer> allEvenPredicate = (x, y) -> x % 2 == 0 && y % 2 == 0;

而六個引數的函式介面可以用 Lambda 實作如下:

SixParametersFunction<Integer, Integer, Integer, Integer, Integer, Integer, Integer> sumSixFunction = (a,
                b, c, d, e, f) -> a + b + c + d + e + f;

Method Reference

在 Java 8 加入了 Lambda 的語法特性後,除了原先 Lambda 的寫法外,還可以進一步改用 Method Reference(方法參考/方法引用)的寫法。
如使用 Lambda 的寫法可以依下列例子改為 MethodReference 的寫法:

List<String> strList = Arrays.asList("A","B","C");

// Lambda
strList.forEach(s -> System.out.print(s));

// MethodReference
strList.forEach(System.out::print);

使用 Method Reference 的好處

使用 Method Reference 最大的好處在於語法精簡很多,原先在 Lambda 中的寫法雖然已經比起以往精簡許多,但是 Method Reference 還能在這個基礎上更加精簡,讓開發人員在開發時更能聚焦在商業邏輯而非語法上。
如:

// Lambda
Arrays.sort(stringArray, (o1, o2) -> o1.compareTo(o2));

// MethodReference
Arrays.sort(stringArray, String::compareTo);

使用 Method Reference 的限制

  • Lambda 中只有呼叫一個方法
  • Lambda 中提供的引數與呼叫方法的參數一致
    這種限制其實與 Functional Interface 有點類似,其實都是 Java 8 提供的語法糖衣,透過這些規則,讓語法可以更為精簡。

常見的 Method Reference 寫法

  • Static Method
List<String> messages = Arrays.asList("hello", "baeldung", "readers!");

// Lambda
messages.forEach(word -> StringUtils.capitalize(word));

// MethodReference
messages.forEach(StringUtils::capitalize);


  • 使用其他物件的 Method
BicycleComparator bikeFrameSizeComparator = new BicycleComparator();

// Lambda
createBicyclesList().stream()
  .sorted((a, b) -> bikeFrameSizeComparator.compare(a, b));


// MethodReference
createBicyclesList().stream()
  .sorted(bikeFrameSizeComparator::compare);


  • Constructor
public Bicycle(String brand) {
    this.brand = brand;
    this.frameSize = 0;
}

List<String> bikeBrands = Arrays.asList("Giant", "Scott", "Trek", "GT");

// Lambda
bikeBrands.stream()
  .map(brand -> new Bicycle(brand));


// MethodReference
bikeBrands.stream()
  .map(Bicycle::new);
createBicyclesList().stream()
  .sorted(bikeFrameSizeComparator::compare);

Stream

Java 8 新增了一個新的 Stream package 專門用來處理集合(collection),搭配 Lambda 語法,在處理集合時能讓邏輯變得更清楚,效率更好。
在過去沒有 Stream 時,我們操作一個集合時可能會這樣寫:

Set<Integer> numbers = Sets.newHashSet(1, 2, 3, 4);
Set<Integer> oddNumbers = findOddNumbers(numbers);

pubilc Set<Integer> findOddNumbers(Set<Integer> numbers) {
    Set<Integer> oddNumbers = Sets.newHashSet();
    for(Integer number : numbers) {
        if(number % 2 != 0) {
            oddNumbers.add(number);
        }
    }
}

雖然這種寫法已行之有年,但這樣寫應該會有一些問題:

  • 為了取得一個 oddNumbers 的集合,得先額外準備一個容器後,再將符合的元素裝入容器中
  • for-loop 中同時處理 過濾數字加入數字到新容器中 這兩件事
  • 此段邏輯不獨立抽成 Method 會導致流程複雜,抽成 Method 又顯得有些多餘

Stream 出現後,大幅改善了這些問題,上面那段程式可以用 Stream 語法改寫如下:

Set<Integer> numbers = Sets.newHashSet(1, 2, 3, 4);
Set<Integer> oddNumbers = findOddNumbers(numbers);

pubilc Set<Integer> findOddNumbers(Set<Integer> numbers) {
    return numbers.stream()
        .filter(number -> number % 2 != 0)
        .collect(Collectors.toSet());
}

上面的語法用口語來說可以解釋成:
把一個 Collection 的數字轉為 Stream 後,過濾數字留下奇數,並將奇數收集為一個 Set

使用 Stream 的好處

以往在使用迴圈時,開發人員經常混合使用 if 判斷、型別轉換,甚至是很多 continue、break、return 等,由於邏輯複雜,容易使迴圈的內容快速膨脹,造成後續維護上很大的負擔,不但迴圈的內容很難理解,更怕改壞。
改成用 Stream 語法後,最大的好處在於迴圈中的邏輯容易獨立,每個操作彼此獨立,不用擔心改壞其他部份,整體而言語法簡潔許多,可讀性也提高許多。

Stream 語法

一般而言,Stream 的語法如下:
Collection[.Stream Operation][.Intermediate Operation][.Intermediate Operation]...[.Terminal Operation]
將 Collection Stream 化後,可以進行任意個中間操作(Intermediate Operation),最後可以執行最終操作(Terminal Operation)。
需注意的是,一旦 Stream 進行過最終操作後,Stream 就會被關閉而無法重新使用。

List<Integer> numbers = Lists.newArrayList(1, 2, 3, 4);

Stream<Integer> oddNumberStream = numbers.stream().filter(number -> number % 2 != 0);

long count = oddNumberStream.count();
List<Integer> oddNumbers = oddNumberStream.collect(Collectors.toList());

如上述程式,在 count Stream 後,如果又使用原本的 Stream 進行操作,則會拋出 java.lang.IllegalStateException: stream has already been operated upon or closed 的例外。

  • Stream Operation
    • stream()
      • 將 Collection 轉為循序處理的 Stream
    • parallelStream()
      • 將 Collection 轉為平行化處理的 Stream
  • Intermediate Operation
    • parallel()
      • 將 Stream 轉為平行化處理的 Stream
    • sequential()
      • 將 Stream 轉為循序處理的 Stream
    • distinct()
      • 過濾 Stream 中重複的項目
    • filter(Predicate)
      • 將 Stream 中的元素根據 Predicate 過濾
    • flatMap(Function>)
      • 將 Stream 中的元素攤平轉換,從 Stream> 轉為 Stream
      • 類似操作的行為有 flatMapToDouble()、flatMapToInt()、flatMapToLong()
    • skip(long)
      • 過濾 Stream,只留下特定順序後的元素
    • limit(long)
      • 過濾 Stream,只留下特定順序前的元素
    • map()
      • 將 Stream 中的元素轉換,從 Stream 轉為 Stream
      • 類似操作的行為有 mapToDouble()、mapToInt()、mapToLong()
    • sorted(Comparator)
      • 不帶 Compatator 時根據物件預設的排序方式排序 Stream
      • 有帶 Comparator 時根據 Comparator 排序 Stream
    • unOrdered()
      • 將 Stream 的排序變為無序性
    • onClose(Runnable)
      • 在 Stream 執行 Terminal Operation 後執行給定的 Runnable
  • Terminal Operation
    • allMatch(Predicate)
      • 判斷 Stream 中的元素是否全部符合 Predicate
    • anyMatch(Predicate)
      • 判斷 Stream 中的元素是否有元素符合 Predicate
    • noneMatch(Predicate)
      • 判斷 Stream 中的元素是否全部不符合 Predicate
    • collect(Collector)
      • 操作如其名,將 Stream 根據 Collector 收集成對應的集合
    • collect(Supplier, BiConsumer, BiConsumer)
      • 第一個參數定義 Stream 要被 Collect 成哪種集合
      • 第二個參數定義 Stream 裡的元素要怎麼被放進集合中
      • 第三個參數定義當 Stream 的元素分別被放入不同集合中後,不同的集合要怎麼合併成同一個集合
    • count()
      • 計算在 Stream 中元素的個數
    • findAny()
      • 取得 Stream 中符合條件的任意一個元素
      • 在有序性的 Stream 中,會回傳遞一個符合條件的元素,在無序性 / 平行的 Stream 中,則每次都有可能取到不同的元素
    • findFirst()
      • 不論在何種 Stream 中,取得 Stream 中符合條件的第一個元素
    • iterator()
      • 將 Stream 轉成 Iterator
    • max(Comparator)
      • 根據 Compatator 取得 Stream 裡最大的元素
    • min(Comparator)
      • 根據 Compatator 取得 Stream 裡最小的元素
    • reduce(BinaryOperator)
      • 將 Stream 根據 BinaryOperator 合併成單一元素
    • forEach(Consumer)
      • 針對每個 Stream 中的元素執行 Consumer 的動作
    • forEachOrdered(Consumer)
      • 針對每個 Stream 中的元素依序執行 Consumer 的動作

References

.Net Core 使用 FluentMigrator 遷移資料庫
Izpack 基本應用

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/05/07, 週二

Captcha 圖像