《Java8實戰》-讀書筆記第一章(02)

雷俠發表於2018-07-31

《Java8實戰》-讀書筆記第一章(02)

從方法傳遞到Lambda

接著上次的Predicate,繼續來了解一下,如果繼續簡化程式碼。

把方法作為值來傳遞雖然很有用,但是要是有很多類似與isHeavyApple和isGreenApple這種可能只用一兩次的方法定義一堆確實有點煩人。為了解決這個問題,Java8它引入了一套新記法(匿名函式或Lambda),然你可以這樣寫:

List<Apple> isRedApples = filterApples(FilteringApples.apples, apple -> "red".equals(apple.getColor()));
複製程式碼

或者是:

List<Apple> appleList = filterApples(FilteringApples.apples, apple -> apple.getWeight() < 120
                && "red".equals(apple.getColor()));
複製程式碼

甚至,你都可以不需要使用filterApples這個方法了,直接使用Stream中的filter方法就可以解決了:

List<Apple> isGreenApple = apples.stream().filter(apple -> "green".equals(apple.getColor()))
                .collect(Collectors.toList());
複製程式碼

酷,看起來很不錯。所以,你甚至都不需要為只用一次的方法寫定義;這樣的程式碼看起來更簡潔、更清晰,因為你用不著去找自己到底傳遞了什麼程式碼。

在剛剛篩選蘋果的過程中,就有使用到Stream(流)其中的一個方法,這個Stream和InputStream、OutputStream是兩個完全不同的東西。Stream它是Java8中的一個核心新特,它是一套新的用來處理集合的API,有很多類似與filter這樣的方法而且使用起來非常的簡單和簡潔,可以簡化大部分程式碼並且在並行的情況下利用多核CPU,能很有效的提升對集合處理的效能。

本章只是簡單的介紹了一下流的使用方式,至於流的詳細用法後面的章節會提到的。

現在,有一串字串,需要進行篩選並且轉為大寫以進行排序,在Java8之前是我們是這麼幹的:

List<String> stringList = Arrays.asList("a1", "a2", "b1", "c1", "c2", "c4", "c3");

List<String> cList = new ArrayList<>();
for (String s : stringList) {
    // 篩選出以c開頭的字串
    if (s.startsWith("c")) {
        // 將以c開頭的字串轉為大寫,新增到集合
        cList.add(s.toUpperCase());
    }
}

// 排序
Collections.sort(cList);

// 遍歷列印
for (String s : cList) {
    System.out.println(s);
}
複製程式碼

這樣的程式碼看起來很頭疼,需要寫這麼長一段的程式碼,在Java8中可以使用Stream進行優化:

List<String> stringList = Arrays.asList("a1", "a2", "b1", "c1", "c2", "c4", "c3");

stringList.stream()
        // 篩選出以c開頭的字串
        .filter(s -> s.startsWith("c"))
        // 將剛剛以c開頭的字串轉為大寫
        .map(String::toUpperCase)
        // 排序
        .sorted()
        // 迴圈遍歷
        .forEach(System.out::println);
複製程式碼

太棒了,只需要短短的一行程式碼就可以完成!但是,使用Stream它也是有缺點的,它的效能不如foreach的效率高為了解決這個問題,Stream支援並。使用並行能極大的利用多核CPU的優勢,例如說:這些程式碼原本只是用單核進行處理,現在有一臺8核的CPU電腦,那麼它的處理速度就會是單核的八倍。

我們來進行比較一下,生成一個0-100的數字並寫入到檔案中,循序流VS並行流誰的效率更高.

循序流:

long startTime = System.currentTimeMillis();
OutputStream out = new FileOutputStream(new File("D:/integer1.txt"));

IntStream.rangeClosed(0, 100)
        .forEach(i -> {
            try {
                Thread.sleep(100L);
                out.write(i);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        });

long endTime = System.currentTimeMillis();
System.out.println("循序流:" + (endTime - startTime));
複製程式碼

並行流:

long startTime = System.currentTimeMillis();
OutputStream out = new FileOutputStream(new File("D:/integer2.txt"));

IntStream.rangeClosed(0, 100)
        .parallel().forEach(i -> {
    try {
        Thread.sleep(100L);
        out.write(i);
    } catch (IOException | InterruptedException e) {
        e.printStackTrace();
    }
});

long endTime = System.currentTimeMillis();
System.out.println("並行流:" + (endTime - startTime));
複製程式碼

執行結果(I5-6200U的筆記本上執行結果):

循序流:10251
並行流:2620
複製程式碼

效率明顯要比循序流快很多嘛!但是,並行流並不是萬能的,如果把sleep去掉後並且數字加到100萬,你會發現執行的時間比循序流還要長。

去掉sleep並且生成的數字是0-100萬,所消耗的時間:

循序流:2775
並行流:3346
複製程式碼

至於為什麼有時候並行流效率比循序流還低,這個以後的文章會解釋。

預設方法

預設方法是Java8中的一個新特性,它的出現使得介面的升級變得平滑了,因為子類不是必須再去顯式的實現介面中的方法了。

例如:在Java8中,你可以直接呼叫List介面中的sort方法、它是用Java8 List介面中如下所示的預設方法實現的:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}
複製程式碼

這意味著List的任何實體類都不需要顯式的實現sort,而在以前的Java版本中,除非提供了sort的實現,否則這些實體類都無法編譯通過。但是,預設方法也存在著一些問題,一個類可以實現多個介面,那麼好幾個介面多有同樣的預設方法,那麼這是否意味著Java中有了某種形式的多繼承?如果是多繼承,那麼會不會出現像C++中菱形繼承的問題?這些問題以後的文章中都會有解釋和解決方案。

第一章總結:

  1. 瞭解了Java8中的一些核心新特性,例如:Lambda表示式、Stream、預設方法。
  2. 瞭解了Lambda表示式和Stream為程式碼帶來的簡潔性。
  3. 並行流帶來的好處。
  4. Java8中的預設方法帶來的好處。

程式碼案例:

chap1

相關文章