【廖雪峯官方網站/Java教程】函數式編程

本博客是函數式編程這一節的學習筆記,網址:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943847278976
這一節課內容分爲3個主題:Lambda基礎、方法引用和試用Stream。
函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!
函數式編程最早是數學家阿隆佐·邱奇研究的一套函數變換邏輯,又稱Lambda Calculus(λ-Calculus),所以也經常把函數式編程稱爲Lambda計算。
Java平臺從Java 8開始,支持函數式編程。

1.Lambda基礎

關於Lambda的詳細用法介紹強烈推薦此博客:https://blog.csdn.net/bitcarmanlee/article/details/70195403。題外話,此博客作者是一大牛,java分類中的文章值得細看。

1.1.Lambda表達式

在Java程序中,我們經常遇到一大堆***單方法接口***,即一個接口只定義了一個方法:

  • Comparator
  • Runnable
  • Callable
    以Comparator爲例,我們想要調用Arrays.sort()時,可以傳入一個Comparator實例,以匿名類方式編寫如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

上述寫法非常繁瑣。從Java 8開始,我們可以用Lambda表達式替換單方法接口。改寫上述代碼如下:

// Lambda
import java.util.Arrays;
public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, (s1, s2) -> {
            return s1.compareTo(s2);
        });
        System.out.println(String.join(", ", array));
    }
}

觀察Lambda表達式的寫法,它只需要寫出方法定義:

(s1, s2) -> {
    return s1.compareTo(s2);
}

其中,參數是(s1, s2),參數類型可以省略,因爲編譯器可以自動推斷出String類型。-> { … }表示方法體,所有代碼寫在內部即可。Lambda表達式沒有class定義,因此寫法非常簡潔。
如果只有一行return xxx的代碼,完全可以用更簡單的寫法:

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

返回值的類型也是由編譯器自動推斷的,這裏推斷出的返回值是int,因此,只要返回int,編譯器就不會報錯。
有關Comparator用法,貼一個鏈接:
[1]https://blog.csdn.net/u012250875/article/details/55126531
下面是java中一些使用到Comparator接口的地方:

Arrays.sort(T[], Comparator<? super T> c);
Collections.sort(List<T> list, Comparator<? super T> c);

在利用Comparator接口時,需要重寫

int compare(T o1, T o2);

這個函數,關於此函數的比較順序,jdk官方默認是升序,是基於:
int compare(T o1, T o2)
Returns: a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.

  • o1 < o2: 返回負整數,標識o1和o2的順序不用交換
  • o1 = o2: 返回零,o1和o2的位置不用動
  • o1 > o2: 返回正整數,o1和o2的位置需要交換

若要實現降序排列,則只需要

  • o1 > o2: 返回負整數,標識o1和o2的順序不用交換
  • o1 = o2: 返回零,o1和o2的位置不用動
  • o1 < o2: 返回正整數,o1和o2的位置需要交換

有關compareTo的用法,貼兩個鏈接:
[1]https://www.runoob.com/java/number-compareto.html
[2]https://www.cnblogs.com/efforts-will-be-lucky/p/7052910.html
關於匿名內部類解析:
[1]https://blog.csdn.net/chengqiuming/article/details/91352913

1.2.FunctionalInterface

我們把只定義了單方法的接口稱之爲FunctionalInterface,用註解@FunctionalInterface標記。例如,Callable接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

再來看Comparator接口:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
    default Comparator<T> thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}

雖然Comparator接口有很多方法,但只有一個抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定義的方法,不算在接口方法內。因此,Comparator也是一個FunctionalInterface。

1.3.小結

  • 單方法接口被稱爲FunctionalInterface。
  • 接收FunctionalInterface作爲參數的時候,可以把實例化的匿名類改寫爲Lambda表達式,能大大簡化代碼。
  • Lambda表達式的參數和返回值均可由編譯器自動推斷。

2.方法引用

2.1.靜態方法引用/實例方法引用

使用Lambda表達式,我們就可以不必編寫FunctionalInterface接口的實現類,從而簡化代碼:

Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
});

實際上,除了Lambda表達式,我們還可以直接傳入方法引用。例如:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, Main::cmp);
        System.out.println(String.join(", ", array));
    }

    static int cmp(String s1, String s2) {
        return s1.compareTo(s2);
    }
}

上述代碼在Arrays.sort()中直接傳入了靜態方法cmp的引用,用Main::cmp表示。
因此,所謂方法引用,是指如果某個方法簽名和接口恰好一致,就可以直接傳入方法引用。
因爲Comparator< String >接口定義的方法是int compare(String, String),和靜態方法int cmp(String, String)相比,除了方法名外,方法參數一致,返回類型相同,因此,我們說兩者的方法簽名一致,可以直接把方法名作爲Lambda表達式傳入:

Arrays.sort(array, Main::cmp);

注意:在這裏,方法簽名只看參數類型和返回類型,不看方法名稱,也不看類的繼承關係。
我們再看看如何引用實例方法。如果我們把代碼改寫如下:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, String::compareTo);
        System.out.println(String.join(", ", array));
    }
}

不但可以編譯通過,而且運行結果也是一樣的,這說明String.compareTo()方法也符合Lambda定義。
觀察String.compareTo()的方法定義:

public final class String {
    public int compareTo(String o) {
        ...
    }
}

這個方法的簽名只有一個參數,爲什麼和int Comparator< String >.compare(String, String)能匹配呢?
因爲實例方法有一個隱含的this參數,String類的compareTo()方法在實際調用的時候,第一個隱含參數總是傳入this,相當於靜態方法:

public static int compareTo(this, String o);

所以,String.compareTo()方法也可作爲方法引用傳入。
關於靜態方法和實例方法的說明參考:
[1]https://www.cnblogs.com/gemuxiaoshe/p/10641670.html

2.2.構造方法引用

除了可以引用靜態方法和實例方法,我們還可以引用構造方法。
我們來看一個例子:如果要把一個List< String >轉換爲List< Person >,應該怎麼辦?

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = ???

傳統的做法是先定義一個ArrayList< Person >,然後用for循環填充這個List:

List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
    persons.add(new Person(name));
}

要更簡單地實現String到Person的轉換,我們可以引用Person的構造方法:

// 引用構造方法
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("Bob", "Alice", "Tim");
        List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
        System.out.println(persons);
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "Person:" + this.name;
    }
}

後面我們會講到Stream的map()方法。現在我們看到,這裏的map()需要傳入的FunctionalInterface的定義是:

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

把泛型對應上就是方法簽名Person apply(String),即傳入參數String,返回類型Person。而Person類的構造方法恰好滿足這個條件,因爲構造方法的參數是String,而構造方法雖然沒有return語句,但它會隱式地返回this實例,類型就是Person,因此,此處可以引用構造方法。構造方法的引用寫法是類名::new,因此,此處傳入Person::new。

2.3.小結

FunctionalInterface允許傳入:

  1. 接口的實現類(傳統寫法,代碼較繁瑣);
  2. Lambda表達式(只需列出參數名,由編譯器推斷類型);
  3. 符合方法簽名的靜態方法;
  4. 符合方法簽名的實例方法(實例類型被看做第一個參數類型);
  5. 符合方法簽名的構造方法(實例類型被看做返回類型)。

FunctionalInterface不強制繼承關係,不需要方法名稱相同,只要求方法參數(類型和數量)與方法返回類型相同,即認爲方法簽名相同。

3.使用Stream

3.1.背景介紹

Java從8開始,不但引入了Lambda表達式,還引入了一個全新的流式API:Stream API。它位於java.util.stream包中。
劃重點:這個Stream不同於java.io的InputStream和OutputStream,它代表的是任意Java對象的序列。兩者對比如下:
在這裏插入圖片描述
有同學會問:一個順序輸出的Java對象序列,不就是一個List容器嗎?
再次劃重點:這個Stream和List也不一樣,List存儲的每個元素都是已經存儲在內存中的某個Java對象,而Stream輸出的元素可能並沒有預先存儲在內存中,而是實時計算出來的。
換句話說,List的用途是操作一組已存在的Java對象,而Stream實現的是惰性計算,兩者對比如下:
在這裏插入圖片描述
我們總結一下Stream的特點:它可以“存儲”有限個或無限個元素。這裏的存儲打了個引號,是因爲元素有可能已經全部存儲在內存中,也有可能是根據需要實時計算出來的。
Stream的另一個特點是,一個Stream可以輕易地轉換爲另一個Stream,而不是修改原Stream本身。
最後,真正的計算通常發生在最後結果的獲取,也就是惰性計算。

Stream<BigInteger> naturals = createNaturalStream(); // 不計算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不計算
Stream<BigInteger> s3 = s2.limit(100); // 不計算
s3.forEach(System.out::println); // 計算

惰性計算的特點是:一個Stream轉換爲另一個Stream時,實際上只存儲了轉換規則,並沒有任何計算髮生。
例如,創建一個全體自然數的Stream,不會進行計算,把它轉換爲上述s2這個Stream,也不會進行計算。再把s2這個無限Stream轉換爲s3這個有限的Stream,也不會進行計算。只有最後,調用forEach確實需要Stream輸出的元素時,才進行計算。我們通常把Stream的操作寫成鏈式操作,代碼更簡潔:

createNaturalStream()
    .map(BigInteger::multiply)
    .limit(100)
    .forEach(System.out::println);

因此,Stream API的基本用法就是:創建一個Stream,然後做若干次轉換,最後調用一個求值方法獲取真正計算的結果:

int result = createNaturalStream() // 創建Stream
             .filter(n -> n % 2 == 0) // 任意個轉換
             .map(n -> n * n) // 任意個轉換
             .limit(100) // 任意個轉換
             .sum(); // 最終計算結果

小結
Stream API的特點是:

  1. Stream API提供了一套新的流式處理的抽象序列;
  2. Stream API支持函數式編程和鏈式操作;
  3. Stream可以表示無限序列,並且大多數情況下是惰性求值的。

3.2.創建Stream

要使用Stream,就必須現創建它。創建Stream有很多種方法,我們來一一介紹。

3.2.1.Stream.of()

創建Stream最簡單的方式是直接用Stream.of()靜態方法,傳入可變參數即創建了一個能輸出確定元素的Stream:

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        // forEach()方法相當於內部循環調用,
        // 可傳入符合Consumer接口的void accept(T t)的方法引用:
        stream.forEach(System.out::println);
    }
}

雖然這種方式基本上沒啥實質性用途,但測試的時候很方便。

3.2.2.基於數組或Collection

第二種創建Stream的方法是基於一個數組或者Collection,這樣該Stream輸出的元素就是數組或者Collection持有的元素:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
        Stream<String> stream2 = List.of("X", "Y", "Z").stream();
        stream1.forEach(System.out::println);
        stream2.forEach(System.out::println);
    }
}
  1. 把數組變成Stream使用Arrays.strem()方法;
  2. 對於Collection(List、Set、Queue等),直接調用stream()方法就可以獲得Stream;
  3. 上述創建Stream的方法都是把一個現有的序列變爲Stream,它的元素是固定的。

3.2.3.基於Supplier

具體請參考網址:https://www.liaoxuefeng.com/wiki/1252599548343744/1322655160467490

3.2.4.其他方法

創建Stream的第三種方法是通過一些API提供的接口,直接獲得Stream。
例如,Files類的lines()方法可以把一個文件變成一個Stream,每個元素代表文件的一行內容:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    ...
}

此方法對於按行遍歷文本文件十分有用。
另外,正則表達式的Pattern對象有一個splitAsStream()方法,可以直接把一個長字符串分割成Stream序列而不是數組:

Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);

3.2.5.基本類型

因爲Java的範型不支持基本類型,所以我們無法用Stream< int >這樣的類型,會發生編譯錯誤。爲了保存int,只能使用String< Integer >,但這樣會產生頻繁的裝箱、拆箱操作。爲了提高效率,Java標準庫提供了IntStream、LongStream和DoubleStream這三種使用基本類型的Stream,它們的使用方法和範型Stream沒有大的區別,設計這三個Stream的目的是提高運行效率:

// 將int[]數組變爲IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 將Stream<String>轉換爲LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);

3.2.6.小結

創建Stream的方法有 :

  1. 通過指定元素、指定數組、指定Collection創建Stream;
  2. 通過Supplier創建Stream,可以是無限序列;
  3. 通過其他類的相關方法創建。

基本類型的Stream有IntStream、LongStream和DoubleStream。

3.3.使用map

Stream.map()是Stream最常用的一個轉換方法,它把一個Stream轉換爲另一個Stream。
所謂map操作,就是把一種操作運算,映射到一個序列的每一個元素上。例如,對x計算它的平方,可以使用函數f(x) = x * x。我們把這個函數映射到一個序列1,2,3,4,5上,就得到了另一個序列1,4,9,16,25:
在這裏插入圖片描述
可見,map操作,把一個Stream的每個元素一一對應到應用了目標函數的結果上。

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

如果我們查看Stream的源碼,會發現map()方法接收的對象是Function接口對象,它定義了一個apply()方法,負責把一個T類型轉換成R類型:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

其中,Function的定義是:

@FunctionalInterface
public interface Function<T, R> {
    // 將T類型轉換爲R:
    R apply(T t);
}

利用map(),不但能完成數學計算,對於字符串操作,以及任何Java對象都是非常有用的。例如:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
                .stream()
                .map(String::trim) // 去空格
                .map(String::toLowerCase) // 變小寫
                .forEach(System.out::println); // 打印
    }
}

小結:
map()方法用於將一個Stream的每個元素映射成另一個元素並轉換成一個新的Stream;
可以將一種元素類型轉換成另一種元素類型。

3.4.使用filter

Stream.filter()是Stream的另一個常用轉換方法。
所謂filter()操作,就是對一個Stream的所有元素一一進行測試,不滿足條件的就被“濾掉”了,剩下的滿足條件的元素就構成了一個新的Stream。
例如,我們對1,2,3,4,5這個Stream調用filter(),傳入的測試函數f(x) = x % 2 != 0用來判斷元素是否是奇數,這樣就過濾掉偶數,只剩下奇數,因此我們得到了另一個序列1,3,5:
在這裏插入圖片描述
用IntStream寫出上述邏輯,代碼如下:

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 != 0)
                .forEach(System.out::println);
    }
}

從結果可知,經過filter()後生成的Stream元素可能變少。
filter()方法接收的對象是Predicate接口對象,它定義了一個test()方法,負責判斷元素是否符合條件:

@FunctionalInterface
public interface Predicate<T> {
    // 判斷元素t是否符合條件:
    boolean test(T t);
}

filter()除了常用於數值外,也可應用於任何Java對象。例如,從一組給定的LocalDate中過濾掉工作日,以便得到休息日:

import java.time.*;
import java.util.function.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream.generate(new LocalDateSupplier())
                .limit(31)
                .filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
                .forEach(System.out::println);
    }
}

class LocalDateSupplier implements Supplier<LocalDate> {
    LocalDate start = LocalDate.of(2020, 1, 1);
    int n = -1;
    public LocalDate get() {
        n++;
        return start.plusDays(n);
    }
}

小結:
使用filter()方法可以對一個Stream的每個元素進行測試,通過測試的元素被過濾後生成一個新的Stream。

3.5.使用reduce

map()和filter()都是Stream的轉換方法,而Stream.reduce()則是Stream的一個聚合方法,它可以把一個Stream的所有元素按照聚合函數聚合成一個結果。
我們來看一個簡單的聚合方法:

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
        System.out.println(sum); // 45
    }
}

reduce()方法傳入的對象是BinaryOperator接口,它定義了一個apply()方法,負責把上次累加的結果和本次的元素 進行運算,並返回累加的結果:

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:兩個輸入,一個輸出
    T apply(T t, T u);
}

上述代碼看上去不好理解,但我們用for循環改寫一下,就容易理解了:

Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
    sum = (sum, n) -> sum + n;
}

可見,reduce()操作首先初始化結果爲指定值(這裏是0),緊接着,reduce()對每個元素依次調用(acc, n) -> acc + n,其中,acc是上次計算的結果:

// 計算過程:
acc = 0 // 初始化爲指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9

因此,實際上這個reduce()操作是一個求和。
如果去掉初始值,我們會得到一個Optional< Integer >:

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent) {
    System.out.println(opt.get());
}

這是因爲Stream的元素有可能是0個,這樣就沒法調用reduce()的聚合函數了,因此返回Optional對象,需要進一步判斷結果是否存在。
利用reduce(),我們可以把求和改成求積,代碼也十分簡單:

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
        System.out.println(s); // 362880
    }
}

注意:計算求積時,初始值必須設置爲1。
除了可以對數值進行累積計算外,靈活運用reduce()也可以對Java對象進行操作。下面的代碼演示瞭如何將配置文件的每一行配置通過map()和reduce()操作聚合成一個Map< String, String >:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 按行讀取配置文件:
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v轉換爲Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    return Map.of(ss[0], ss[1]);
                })
                // 把所有Map聚合到一個Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                    m.putAll(kv);
                    return m;
                });
        // 打印結果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });
    }
}

關於Map.putAll()方法,該方法用來追加另一個Map對象到當前Map集合對象,它會把另一個Map集合對象中的所有內容添加到當前Map集合對象。

putAll(Map<? extends K,? extends V> m) 

小結:

  1. reduce()方法將一個Stream的每個元素依次作用於BinaryOperator,並將結果合併;
  2. reduce()是聚合方法,聚合方法會立刻對Stream進行計算。

3.6.輸出集合

3.6.1.轉換操作/聚合操作

我們介紹了Stream的幾個常見操作:map()、filter()、reduce()。這些操作對Stream來說可以分爲兩類,一類是轉換操作,即把一個Stream轉換爲另一個Stream,例如map()和filter(),另一類是聚合操作,即對Stream的每個元素進行計算,得到一個確定的結果,例如reduce()。
區分這兩種操作是非常重要的,因爲對於Stream來說,對其進行轉換操作並不會觸發任何計算!我們可以做個實驗:

import java.util.function.Supplier; 
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args)     {
        Stream<Long> s1 = Stream.generate(new NatualSupplier());
        Stream<Long> s2 = s1.map(n -> n * n);
        Stream<Long> s3 = s2.map(n -> n - 1);
        System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842
    }
}

class NatualSupplier implements Supplier<Long> {
    long n = 0;
    public Long get() {
        n++;
        return n;
    }
}

因爲s1是一個Long類型的序列,它的元素高達922億個,但執行上述代碼,既不會有任何內存增長,也不會有任何計算,因爲轉換操作只是保存了轉換規則,無論我們對一個Stream轉換多少次,都不會有任何實際計算髮生。

而聚合操作則不一樣,聚合操作會立刻促使Stream輸出它的每一個元素,並依次納入計算,以獲得最終結果。所以,對一個Stream進行聚合操作,會觸發一系列連鎖反應:

Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);

我們對s4進行reduce()聚合計算,會不斷請求s4輸出它的每一個元素。因爲s4的上游是s3,它又會向s3請求元素,導致s3向s2請求元素,s2向s1請求元素,最終,s1從Supplier實例中請求到真正的元素,並經過一系列轉換,最終被reduce()聚合出結果。
可見,聚合操作是真正需要從Stream請求數據的,對一個Stream做聚合計算後,結果就不是一個Stream,而是一個其他的Java對象。

3.6.2.輸出爲List/Set

reduce()只是一種聚合操作,如果我們希望把Stream的元素保存到集合,例如List,因爲List的元素是確定的Java對象,因此,把Stream變爲List不是一個轉換操作,而是一個聚合操作,它會強制Stream輸出每個元素。
下面的代碼演示瞭如何將一組String先過濾到空字符串,然後把非空字符串保存到List中:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
        List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
        System.out.println(list);
    }
}

把Stream的每個元素收集到List的方法是調用collect()並傳入Collectors.toList()對象,它實際上是一個Collector實例,通過類似reduce()的操作,把每個元素添加到一個收集器中(實際上是ArrayList)。
類似的,collect(Collectors.toSet())可以把Stream的每個元素收集到Set中。

3.6.3.輸出爲數組

把Stream的元素輸出爲數組和輸出爲List類似,我們只需要調用toArray()方法,並傳入數組的“構造方法”:

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

注意到傳入的“構造方法”是String[]::new,它的簽名實際上是IntFunction<String[]>定義的String[] apply(int),即傳入int參數,獲得String[]數組的返回值。

3.6.4.輸出爲Map

如果我們要把Stream的元素收集到Map中,就稍微麻煩一點。因爲對於每個元素,添加到Map時需要key和value,因此,我們要指定兩個映射函數,分別把元素映射爲key和value:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        // 把元素s映射爲key:
                        s -> s.substring(0, s.indexOf(':')),
                        // 把元素s映射爲value:
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}

3.6.5.分組輸出

Stream還有一個強大的分組功能,可以按組輸出。我們看下面的例子:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

分組輸出使用Collectors.groupingBy(),它需要提供兩個函數:一個是分組的key,這裏使用s -> s.substring(0, 1),表示只要首字母相同的String分到一組,第二個是分組的value,這裏直接使用Collectors.toList(),表示輸出爲List,上述代碼運行結果如下:

{
    A=[Apple, Avocado, Apricots],
    B=[Banana, Blackberry],
    C=[Coconut, Cherry]
}

可見,結果一共有3組,按"A",“B”,"C"分組,每一組都是一個List。
假設有這樣一個Student類,包含學生姓名、班級和成績:

class Student {
    int gradeId; // 年級
    int classId; // 班級
    String name; // 名字
    int score; // 分數
}

如果我們有一個Stream,利用分組輸出,可以非常簡單地按年級或班級把Student歸類。

3.6.6.小結

  1. Stream可以輸出爲集合:
  2. Stream通過collect()方法可以方便地輸出爲List、Set、Map,還可以分組輸出。

3.7.其他操作

我們把Stream提供的操作分爲兩類:轉換操作和聚合操作。除了前面介紹的常用操作外,Stream還提供了一系列非常有用的方法。

3.7.1.排序

對Stream的元素進行排序十分簡單,只需調用sorted()方法:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Orange", "apple", "Banana")
            .stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(list);
    }
}

此方法要求Stream的每個元素必須實現Comparable接口。如果要自定義排序,傳入指定的Comparator即可:

List<String> list = List.of("Orange", "apple", "Banana")
    .stream()
    .sorted(String::compareToIgnoreCase)
    .collect(Collectors.toList());

關於sort()用法的一個例子

package lambda;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class lambdaSortTest {

    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        Dog dog1 = new Dog(13, "black");
        Dog dog2 = new Dog(13, "red");
        Dog dog3 = new Dog(8, "yellow");

        dogList.add(dog1);
        dogList.add(dog2);
        dogList.add(dog3);

        dogList.stream().sorted(Comparator
                .comparing(Dog::getPrice) // 先按照價格升序排列
                .thenComparing(Dog::getColor)) // 再按照顏色升序排列
                .forEach(System.out::println);
    }
}

class Dog {
    private int price;
    private String color;

    public Dog(int price, String color) {
        this.price = price;
        this.color = color;
    }

    public int getPrice() {
        return price;
    }

    public String getColor() {
        return color;
    }

    @Override
    public String toString() {
        return "Dog, price = "
                + price + "; color = "
                + color + ".";
    }
}

輸出如下:

Dog, price = 8; color = yellow.
Dog, price = 13; color = black.
Dog, price = 13; color = red.

注意sorted()只是一個轉換操作,它會返回一個新的Stream。

3.7.2.去重

對一個Stream的元素進行去重,沒必要先轉換爲Set,可以直接用distinct():

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]

3.7.3.截取

截取操作常用於把一個無限的Stream轉換成有限的Stream,skip()用於跳過當前Stream的前N個元素,limit()用於截取當前Stream最多前N個元素:

List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳過A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]

截取操作也是一個轉換操作,將返回新的Stream。

3.7.4.合併

將兩個Stream合併爲一個Stream可以使用Stream的靜態方法concat():

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合併:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]

3.7.5.flatMap

如果Stream的元素是集合:

Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));

而我們希望把上述Stream轉換爲Stream< Integer >,就可以使用flatMap():

Stream<Integer> i = s.flatMap(list -> list.stream());

因此,所謂flatMap(),是指把Stream的每個元素(這裏是List)映射爲Stream,然後合併成一個新的Stream:
在這裏插入圖片描述

3.7.6.並行

通常情況下,對Stream的元素進行處理是單線程的,即一個一個元素進行處理。但是很多時候,我們希望可以並行處理Stream的元素,因爲在元素數量非常大的情況,並行處理可以大大加快處理速度。
把一個普通Stream轉換爲可以並行處理的Stream非常簡單,只需要用parallel()進行轉換:

Stream<String> s = ...
String[] result = s.parallel() // 變成一個可以並行處理的Stream
                   .sorted() // 可以進行並行排序
                   .toArray(String[]::new);

經過parallel()轉換後的Stream只要可能,就會對後續操作進行並行處理。我們不需要編寫任何多線程代碼就可以享受到並行處理帶來的執行效率的提升。

3.7.7.其他聚合方法

除了reduce()和collect()外,Stream還有一些常用的聚合方法:

  • count():用於返回元素個數;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

針對IntStream、LongStream和DoubleStream,還額外提供了以下聚合方法:

  • sum():對所有元素求和;
  • average():對所有元素求平均數。

3.7.8.其他方法

還有一些方法,用來測試Stream的元素是否滿足以下條件:

  • boolean allMatch(Predicate<? super T>):測試是否所有元素均滿足測試條件; boolean
  • anyMatch(Predicate<? super T>):測試是否至少有一個元素滿足測試條件。

最後一個常用的方法是forEach(),它可以循環處理Stream的每個元素,我們經常傳入System.out::println來打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {
    System.out.println("Hello, " + str);
});

3.7.9.小結

Stream提供的常用操作有:

  • 轉換操作:map(),filter(),sorted(),distinct(),skip(),limit();
  • 合併操作:concat(),flatMap();
  • 並行處理:parallel();
  • 聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();
  • 其他操作:allMatch(), anyMatch(), forEach()。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章