JDK8中Stream使用解析

口袋裡的貓發表於2021-06-06

JDK8中Stream使用解析

現在談及JDK8的新特新,已經說不上新了。本篇介紹的就是StreamLambda,說的Stream可不是JDK中的IO流,這裡的Stream指的是處理集合的抽象概念『像流一樣處理集合資料』。

瞭解Stream前先認識一下Lambda

函式式介面和Lambda

先看一組簡單的對比

傳統方式使用一個匿名內部類的寫法

new Thread(new Runnable() {
    @Override
    public void run() {
        // ...
    }
}).start();

換成Lambda的寫法

new Thread(() -> {
    // ...
}).start();

其實上面的寫法就是簡寫了函式式介面匿名實現類

配合Lambda,JDK8引入了一個新的定義叫做:函式式介面(Functional interfaces)

函式式介面

從概念上講,有且僅有一個需要實現方法的介面稱之為函式式介面

看一個JDK給的一個函式式介面的原始碼

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

可以看到介面上面有一個@FunctionalInterface註釋,功能大致和@Override類似

不寫@Override也能重寫父類方法,該方法確實沒有覆蓋或實現了在超型別中宣告的方法時編譯器就會報錯,主要是為了編譯器可以驗證識別程式碼編寫的正確性。

同樣@FunctionalInterface也是這樣,寫到一個不是函式式介面的介面上面就會報錯,即使不寫@FunctionalInterface註釋,編譯器也會將滿足函式式介面定義的任何介面視為函式式介面。

寫一個函式式介面加不加@FunctionalInterface註釋,下面的介面都是函式式介面

interface MyFunc {
  String show(Integer i);
}

Lambda表示式

Lambda表示式就是為了簡寫函式式介面

構成

看一下Lambda的構成

  1. 括號裡面的引數
  2. 箭頭 ->
  3. 然後是身體
    • 它可以是單個表示式或java程式碼塊。

整體表現為 (...引數) -> {程式碼塊}

簡寫

下面就是函式式介面的實現簡寫為Lambda的例子

  • 無參 - 無返回
interface MyFunc1 {
    void func();
}

// 空實現
MyFunc1 f11 = () -> { };
// 只有一行語句
MyFunc1 f12 = () -> {
    System.out.println(1);
    System.out.println(2);
};
// 只有一行語句
MyFunc1 f13 = () -> {
    System.out.println(1);
};
// 只有一行語句可以省略 { }
MyFunc1 f14 = () -> System.out.println(1);
  • 有參 - 無返回
interface MyFunc2 {
    void func(String str);
}

// 函式體空實現
MyFunc2 f21 = (str) -> { };
// 單個引數可以省略 () 多個不可以省略
MyFunc2 f22 = str -> System.out.println(str.length());
  • 無參 - 有返回
interface MyFunc3 {
    int func();
}

// 返回值
MyFunc3 f31 = () -> {return 1;};
// 如果只有一個return 語句時可以直接寫return 後面的表示式語句
MyFunc3 f32 = () -> 1;
  • 有參 - 有返回
interface MyFunc4 {
    int func(String str);
}

// 這裡單個引數簡寫了{}
MyFunc4 f41 = str -> {
    return str.length();
};
// 這裡又簡寫了return
MyFunc4 f42 = str -> str.length();
// 這裡直接使用了方法引用進行了簡寫 - 在文章後續章節有介紹到
MyFunc4 f43 = String::length;

這裡可以總結出來簡寫規則

上面寫的Lambda表示式中引數都沒有寫引數型別(可以寫引數型別的),so

  1. 小括號內引數的型別可以省略;
  2. 沒有引數時小括號不能省略,小括號中有且僅有一個引數時,不能預設括號
  3. 如果大括號內有且僅有一個語句,則無論是否有返回值,都可以省略大括號、return關鍵字及語句分號(三者省略都需要一起省略)。

看到這裡應該認識到了如何用Lambda簡寫函式式介面,那現在就進一步的認識一下JDK中Stream中對函式式介面的幾種大類

常用內建函式式介面

上節說明了Lambda表示式就是為了簡寫函式式介面,為使用方便,JDK8提供了一些常用的函式式介面。最具代表性的為Supplier、Function、Consumer、Perdicate,這些函式式介面都在java.util.function包下。

這些函式式介面都是泛型型別的,下面的原始碼都去除了default方法,只保留真正需要實現的方法。

Function介面

這是一個轉換的介面。介面有引數、有返回值,傳入T型別的資料,經過處理後,返回R型別的資料。『T和R都是泛型型別』可以簡單的理解為這是一個加工工廠。

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

使用例項:定義一個轉換函式『將字串轉為數字,再平方』

// 將字串轉為數字,再平方
Function<String, Integer> strConvertToIntAndSquareFun = (str) -> {
    Integer value = Integer.valueOf(str);
    return value * value;
};
Integer result = strConvertToIntAndSquareFun.apply("4");
System.out.println(result); // 16

Supplier介面

這是一個對外供給的介面。此介面無需引數,即可返回結果

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

使用例項:定義一個函式返回“Tom”字串

// 供給介面,呼叫一次返回一個 ”tom“ 字串
Supplier<String> tomFun = () -> "tom";
String tom = tomFun.get();
System.out.println(tom); // tom

Consumer介面

這是一個消費的介面。此介面有引數,但是沒有返回值

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

使用例項:定義一個函式傳入數字,列印一行相應數量的A

// 重複列印
Consumer<Integer> printA = (n)->{
    for (int i = 0; i < n; i++) {
        System.out.print("A");
    }
    System.out.println();
};
printA.accept(5); // AAAAA

Predicate介面

這是一個斷言的介面。此介面對輸入的引數進行一系列的判斷,返回一個Boolean值。

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

使用例項:定義一個函式傳入一個字串,判斷是否為A字母開頭且Z字母結尾

// 判斷是否為`A`字母開頭且`Z`字母結尾
Predicate<String> strAStartAndZEnd = (str) -> {
    return str.startsWith("A") && str.endsWith("Z");
};
System.out.println(strAStartAndZEnd.test("AaaaZ")); // true 
System.out.println(strAStartAndZEnd.test("Aaaaa")); // false
System.out.println(strAStartAndZEnd.test("aaaaZ")); // false
System.out.println(strAStartAndZEnd.test("aaaaa")); // false

Supplier介面外Function、Consumer、Perdicate還有其他一堆預設方法可以用,比如Predicate介面包含了多種預設方法,用於處理複雜的判斷邏輯(and, or);

上面的使用方式都是正常簡單的使用函式式介面,當函式式介面遇見了方法引用才真正發揮他的作用。

方法引用

方法引用的唯一存在的意義就是為了簡寫Lambda表示式。

方法引用通過方法的名字來指向一個方法,可以使語言的構造更緊湊簡潔,減少冗餘程式碼。

比如上面章節使用的

MyFunc4 f43 = String::length; // 這個地方就用到了方法引用

方法引用使用一對冒號 ::

相當於將String類的例項方法length賦給MyFunc4介面

public int length() {
    return value.length;
}
interface MyFunc4 {
    int func(String str);
}

這裡可能有點問題:方法 int length()的返回值和int func(String str)相同,但是方法引數不同為什麼也能正常賦值給MyFunc4

可以理解為Java例項方法有一個隱藏的引數第一個引數this(型別為當前類)

public class Student {
    public void show() {
        // ...
    }
    public void print(int a) {
        // ...
    }
}

例項方法show()print(int a)相當於

public void show(String this);
public void print(String this, int a);

這樣解釋的通為什麼MyFunc4 f43 = String::length;可以正常賦值。

String::length;
public int length() {
    return value.length;
}

// 相當於
public int length(String str) {
    return str.length();
}
// 這樣看length就和函式式介面MyFunc4的傳參和返回值就相同了

不只這一種方法引用詳細分類如下

方法引用分類

型別 引用寫法 Lambda表示式
靜態方法引用 ClassName::staticMethod (args) -> ClassName.staticMethod(args)
物件方法引用 ClassName::instanceMethod (instance, args) -> instance.instanceMethod(args)
例項方法引用 instance::instanceMethod (args) -> instance.instanceMethod(args)
構建方法引用 ClassName::new (args) -> new ClassName(args)

上面的方法就屬於物件方法引用

記住這個表格,不用刻意去記,使用Stream時會經常遇到

有幾種比較特殊的方法引用,一般來說原生型別如int不能做泛型型別,但是int[]可以

IntFunction<int[]> arrFun = int[]::new;
int[] arr = arrFun.apply(10); // 生成一個長度為10的陣列

這節結束算是把函式式介面,Lambda表示式,方法引用等概念串起來了。

Optional工具

Optional工具是一個容器物件,最主要的用途就是為了規避 NPE(空指標) 異常。構造方法是私有的,不能通過new來建立容器。是一個不可變物件,具體原理沒什麼可以介紹的,容器原始碼整個類沒500行,本章節主要介紹使用。

  • 構造方法
private Optional(T value) {
    // 傳 null 會報空指標異常
    this.value = Objects.requireNonNull(value);
}
  • 建立Optional的方法

empyt返回一個包含null值的Optional容器

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

of返回一個不包含null值的Optional容器,傳null值報空指標異常

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

ofNullable返回一個可能包含null值的Optional容器

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}
  • 可以使用的Optional的方法

ifPresent方法,引數是一個Consumer,當容器內的值不為null是執行Consumer

Optional<Integer> opt = Optional.of(123);
opt.ifPresent((x) -> {
	System.out.println(opt);
});
// out: 123

get方法,獲取容器值,可能返回空

orElse方法,當容器中值為null時,返回orElse方法的入參值

public T orElse(T other) {
    return value != null ? value : other;
}

orElseGet方法,當容器中值為null時,執行入參Supplier並返回值

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
  • 常見用法
// 當param為null時 返回空集合
Optional.ofNullable(param).orElse(Collections.emptyList());
Optional.ofNullable(param).orElseGet(() -> Collections.emptyList());

orElseorElseGet的區別,orElseGet算是一個惰性求值的寫法,當容器內的值不為null時Supplier不會執行。

平常工作開發中,也是經常通過 orElse 來規避 NPE 異常。

這方面不是很困難難主要是後續Stream有些方法需要會返回一個Optional一個容器物件。

Stream

Stream可以看作是一個高階版的迭代器。增強了Collection的,極大的簡化了對集合的處理。

想要使用Stream首先需要建立一個

建立Stream流的方式

// 方式1,陣列轉Stream
Arrays.stream(arr);
// 方式2,陣列轉Stream,看原始碼of就是方法1的包裝
Stream.of(arr);
// 方式3,呼叫Collection介面的stream()方法
List<String> list = new ArrayList<>();
list.stream();

有了Stream自然就少不了操作流

常用Stream流方法

大致可以把對Stream的操作大致分為兩種型別中間操作終端操作

  • 中間操作是一個屬於惰式的操作,也就是不會立即執行,每一次呼叫中間操作只會生成一個標記了新的Stream
  • 終端操作會觸發實際計算,當終端操作執行時會把之前所有中間操作以管道的形式順序執行,Stream是一次性的計算完會失效

操作Stream會大量的使用Lambda表示式,也可以說它就是為函數語言程式設計而生

先提前認識一個終端操作forEach對流中每個元素執行一個操作,實現一個列印的效果

// 列印流中的每一個元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi").forEach(str -> {
    System.out.println(str);
});

forEach的引數是一個Consumer可以用方法引用優化(靜態方法引用),優化後的結果為

Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .forEach(System.out::println);

有這一個終端操作就可以向下介紹大量的中間操作了

  • 中間操作

中間操作filter:過濾元素

fileter方法引數是一個Predicate介面,表示式傳入的引數是元素,返回true保留元素,false過濾掉元素

過濾長度小於3的字串,僅保留長度大於4的字串

Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    // 過濾
    .filter(str -> str.length() > 3)
    .forEach(System.out::println);
/*
輸出:
jerry
lisa
moli
Demi
*/

中間操作limit:截斷元素

限制集合長度不能超過指定大小

Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .limit(2)
    .forEach(System.out::println);
/*
輸出:
jerry
lisa
*/

中間操作skip:跳過元素(丟棄流的前n元素)

// 丟棄前2個元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .skip(2)
    .forEach(System.out::println);
/*
輸出:
moli
tom
Demi
*/

中間操作map:轉換元素

map傳入的函式會被應用到每個元素上將其對映成一個新的元素

// 為每一個元素加上 一個字首 "name: "
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
    .map(str -> "name: " + str)
    .forEach(System.out::println);
/*
輸出:
name: jerry
name: lisa
name: moli
name: tom
name: Demi
*/

中間操作peek:檢視元素

peek方法的存在主要是為了支援除錯,方便檢視元素流經管道中的某個點時的情況

下面是一個JDK原始碼中給出的例子

Stream.of("one", "two", "three", "four")
    // 第1次檢視
    .peek(e -> System.out.println("第1次 value: " + e))
    // 過濾掉長度小於3的字串
    .filter(e -> e.length() > 3)
    // 第2次檢視
    .peek(e -> System.out.println("第2次 value: " + e))
    // 將流中剩下的字串轉為大寫
    .map(String::toUpperCase)
    // 第3次檢視
    .peek(e -> System.out.println("第3次 value: " + e))
    // 收集為List
    .collect(Collectors.toList());

/*
輸出:
第1次 value: one
第1次 value: two
第1次 value: three
第2次 value: three
第3次 value: THREE
第1次 value: four
第2次 value: four
第3次 value: FOUR
*/

mappeek有點相似,不同的是peek接收一個Consumer,而map接收一個Function

當然了你非要採用peek修改資料也沒人能限制的了

public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
            "name='" + name + '\'' +
            '}';
    }
}

Stream.of(new User("tom"), new User("jerry"))
    .peek(e -> {
        e.name = "US:" + e.name;
    })
    .forEach(System.out::println);
/*
輸出:
User{name='US:tom'}
User{name='US:jerry'}
*/

中間操作sorted:排序資料

// 排序資料
Stream.of(4, 2, 1, 3)
    // 預設是升序
    .sorted()
    .forEach(System.out::println);
/*
輸出:
1
2
3
4
*/

逆序排序

// 排序資料
Stream.of(4, 2, 1, 3)
    // 逆序
    .sorted(Comparator.reverseOrder())
    .forEach(System.out::println
/*
輸出:
4
3
2
1
*/

如果是物件如何排序,自定義Comparator,切記不要違反自反性,對稱性,傳遞性原則

public class User {
    public String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
            "name='" + name + '\'' +
            '}';
    }
}

// 名稱長的排前面
Stream.of(new User("tom"), new User("jerry"))
    .sorted((e1, e2) -> {
        return e2.name.length() - e1.name.length();
    })
    .forEach(System.out::println);
/*
輸出:
User{name='US:jerry'}
User{name='US:tom'}
*/	

中間操作distinct:去重

注意:必須重寫對應泛型的hashCode()和equals()方法


Stream.of(2, 2, 4, 4, 3, 3, 100)
    .distinct()
    .forEach(System.out::println);
/*
輸出:
2
4
3
100
*/	

中間操作flatMap:平鋪流

返回一個流,該流由通過將提供的對映函式(flatMap傳入的引數)應用於每個元素而生成的對映流的內容替換此流的每個元素,通俗易懂就是將原來的Stream中的所有元素都展開組成一個新的Stream


List<Integer[]> arrList = new ArrayList<>();
arrList.add(arr1);
arrList.add(arr2);
// 未使用
arrList.stream()
    .forEach(e -> {
        System.out.println(Arrays.toString(e));
    });

/*
輸出:
[1, 2]
[3, 4]
*/	

// 平鋪後
arrList.stream()
    .flatMap(arr -> Stream.of(arr))
    .forEach(e -> {
        System.out.println(e);
    });

/*
輸出:
1
2
3
4
*/	 	

終端操作max,min,count:統計

// 最大值
Optional<Integer> maxOpt = Stream.of(2, 4, 3, 100)
    .max(Comparator.comparing(e -> e));
System.out.println(maxOpt.get()); // 100

// 最小值
Optional<Integer> minOpt = Stream.of(2, 4, 3, 100)
    .min(Comparator.comparing(Function.identity()));
System.out.println(minOpt.get()); // 2

// 數量
long count = Stream.of("one", "two", "three", "four")
    .count();
System.out.println(count); // 4

上面例子中有一個點需要注意一下Function.identity()相當於 e -> e

看原始碼就可以看出來

static <T> Function<T, T> identity() {
    return t -> t;
}

終端操作findAny:返回任意一個元素

Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
    .findAny();
System.out.println(anyOpt.orElse(""));
/*
輸出:
one
*/	

終端操作findFirst:返回第一個元素

Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
    .findFirst();

System.out.println(firstOpt.orElse(""));
/*
輸出:
one
*/	

返回的Optional容器在上面介紹過了,一般配置orElse使用,原因就在於findAnyfindFirst可能返回空空容器,呼叫get可能會拋空指標異常

終端操作allMatch,anyMatch:匹配

// 是否全部為 one 字串
boolean allIsOne = Stream.of("one", "two", "three", "four")
    .allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // false

allIsOne = Stream.of("one", "one", "one", "one")
    .allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // true

// 是否包含 one 字串
boolean hasOne = Stream.of("one", "two", "three", "four")
    .anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // true

hasOne = Stream.of("two", "three", "four")
    .anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // false

上面僅僅介紹了一個forEach終端操作,但是業務開發中更多的是對處理的資料進行收集起來,如下面的一個例子將元素收集為一個List集合

終端操作collect:收集元素到集合

collect高階使用方法很複雜,常用的用法使用Collectors工具類

  • 收整合List
List<String> list = Stream.of("one", "two", "three", "four")
    .collect(Collectors.toList());
System.out.println(list);
/*
輸出:
[one, two, three, four]
*/	
  • 收整合Set『收集後有去除的效果,結果集亂序』
Set<String> set = Stream.of("one", "one", "two", "three", "four")
    .collect(Collectors.toSet());
System.out.println(set);
/*
輸出:
[four, one, two, three]
*/	
  • 字串拼接
String str1 = Stream.of("one", "two", "three", "four")
    .collect(Collectors.joining());
System.out.println(str1); // onetwothreefour
String str2 = Stream.of("one", "two", "three", "four")
    .collect(Collectors.joining(", "));
System.out.println(str2); // one, two, three, four
  • 收整合Map
// 使用Lombok外掛
@Data
@AllArgsConstructor
public class User {
    public Integer id;
    public String name;
}

Map<Integer, User> map = Stream.of(new User(1, "tom"), new User(2, "jerry"))
    .collect(Collectors.toMap(User::getId, Function.identity(), (k1, k2) -> k1));
System.out.println(map);
/*
輸出:
{
    1=User(id=1, name=tom), 
    2=User(id=2, name=jerry)
}
*/	

toMap常用的方法簽名

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
/*
keyMapper:Key 的對映函式
valueMapper:Value 的對映函式
mergeFunction:當 Key 衝突時,呼叫的合併方法
*/
  • 資料分組
@Data
@AllArgsConstructor
class User {
    public Integer id;
    public String name;
}
Map<String, List<User>> map = Stream.of(
    new User(1, "tom"), new User(2, "jerry"),
    new User(3, "moli"), new User(4, "lisa")
).collect(Collectors.groupingBy(u -> {
    if (u.id % 2 == 0) {
        return "奇";
    }
    return "偶";
}));
System.out.println(map);
/*
輸出:
{
    偶=[User(id=1, name=tom), User(id=3, name=moli)], 
    奇=[User(id=2, name=jerry), User(id=4, name=lisa)]
}
*/	

分組後value 是一個集合,groupingBy分組還有一個引數可以指定下級收集器,後續例子中有使用到

Steam例

下面例子用到的基礎資料,如有例子特例會在例子中單獨補充

List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "tom",    19, "男", "軟工"));
studentList.add(new Student(2, "lisa",   15, "女", "軟工"));
studentList.add(new Student(3, "Ada",    16, "女", "軟工"));
studentList.add(new Student(4, "Dora",   14, "女", "計科"));
studentList.add(new Student(5, "Bob",    20, "男", "軟工"));
studentList.add(new Student(6, "Farrah", 15, "女", "計科"));
studentList.add(new Student(7, "Helen",  13, "女", "軟工"));
studentList.add(new Student(8, "jerry",  12, "男", "計科"));
studentList.add(new Student(9, "Adam",   20, "男", "計科"));

例1:封裝一個分頁函式

/**
* 分頁方法
*
* @param list     要分頁的資料
* @param pageNo   當前頁
* @param pageSize 頁大小
*/
public static <T> List<T> page(Collection<T> list, long pageNo, long pageSize) {
    if (Objects.isNull(list) || list.isEmpty()) {
        return Collections.emptyList();
    }
    return list.stream()
        .skip((pageNo - 1) * pageSize)
        .limit(pageSize)
        .collect(Collectors.toList());
}

List<Student> pageData = page(studentList, 1, 3);
System.out.println(pageData);
/*
輸出:
[
  Student(id=1, name=tom, age=19, sex=男, className=軟工), 
  Student(id=2, name=lisa, age=15, sex=女, className=軟工), 
  Student(id=3, name=Ada, age=16, sex=女, className=軟工)
]
*/

例2:獲取軟工班全部的人員id

List<Integer> idList = studentList.stream()
    .filter(e -> Objects.equals(e.getClassName(), "軟工"))
    .map(Student::getId)
    .collect(Collectors.toList());
System.out.println(idList);
/*
輸出:
[1, 2, 3, 5, 7]
*/

例3:收集每個班級中的人員名稱列表

Map<String, List<String>> map = studentList.stream()
        .collect(Collectors.groupingBy(
                Student::getClassName,
                Collectors.mapping(Student::getName, Collectors.toList())
        ));
System.out.println(map);
/*
輸出:
{
  計科=[Dora, Farrah, jerry, Adam], 
  軟工=[tom, lisa, Ada, Bob, Helen]
}
*/

例4:統計每個班級中的人員個數

Map<String, Long> map = studentList.stream()
    .collect(Collectors.groupingBy(
        Student::getClassName,
        Collectors.mapping(Function.identity(), Collectors.counting())
    ));
System.out.println(map);
/*
輸出:
{
  計科=4, 
  軟工=5
}
*/

例5:獲取全部女生的名稱

List<String> allFemaleNameList = studentList.stream()
    .filter(stu -> Objects.equals("女", stu.getSex()))
    .map(Student::getName)
    .collect(Collectors.toList());
System.out.println(allFemaleNameList);
/*
輸出:
[lisa, Ada, Dora, Farrah, Helen]
*/

例6:依照年齡排序

// 年齡升序排序
List<Student> stuList1 = studentList.stream()
    // 升序
    .sorted(Comparator.comparingInt(Student::getAge))
    .collect(Collectors.toList());
System.out.println(stuList1);
/*
輸出:
[
Student(id=8, name=jerry, age=12, sex=男, className=計科), 
Student(id=7, name=Helen, age=13, sex=女, className=軟工), 
Student(id=4, name=Dora, age=14, sex=女, className=計科), 
Student(id=2, name=lisa, age=15, sex=女, className=軟工), 
Student(id=6, name=Farrah, age=15, sex=女, className=計科), 
Student(id=3, name=Ada, age=16, sex=女, className=軟工), 
Student(id=1, name=tom, age=19, sex=男, className=軟工), 
Student(id=5, name=Bob, age=20, sex=男, className=軟工), 
Student(id=9, name=Adam, age=20, sex=男, className=計科)
]
*/

// 年齡降序排序
List<Student> stuList2 = studentList.stream()
    // 降序
    .sorted(Comparator.comparingInt(Student::getAge).reversed())
    .collect(Collectors.toList());
System.out.println(stuList2);
/*
輸出:
[
Student(id=5, name=Bob, age=20, sex=男, className=軟工), 
Student(id=9, name=Adam, age=20, sex=男, className=計科), 
Student(id=1, name=tom, age=19, sex=男, className=軟工), 
Student(id=3, name=Ada, age=16, sex=女, className=軟工), 
Student(id=2, name=lisa, age=15, sex=女, className=軟工), 
Student(id=6, name=Farrah, age=15, sex=女, className=計科), 
Student(id=4, name=Dora, age=14, sex=女, className=計科), 
Student(id=7, name=Helen, age=13, sex=女, className=軟工), 
Student(id=8, name=jerry, age=12, sex=男, className=計科)
]
*/

例7:分班級依照年齡排序

該例中和例3類似的處理,都使用到了downstream下游 - 收集器

Map<String, List<Student>> map = studentList.stream()
        .collect(
                Collectors.groupingBy(
                        Student::getClassName,
                        Collectors.collectingAndThen(Collectors.toList(), arr -> {
                            return arr.stream()
                                    .sorted(Comparator.comparingInt(Student::getAge))
                                    .collect(Collectors.toList());
                        })
                )
        );
/*
輸出:
{
  計科 =[
    Student(id = 8, name = jerry, age = 12, sex = 男, className = 計科), 
    Student(id = 4, name = Dora, age = 14, sex = 女, className = 計科), 
    Student(id = 6, name = Farrah, age = 15, sex = 女, className = 計科), 
    Student(id = 9, name = Adam, age = 20, sex = 男, className = 計科)
  ],
  軟工 =[
    Student(id = 7, name = Helen, age = 13, sex = 女, className = 軟工), 
    Student(id = 2, name = lisa, age = 15, sex = 女, className = 軟工), 
    Student(id = 3, name = Ada, age = 16, sex = 女, className = 軟工), 
    Student(id = 1, name = tom, age = 19, sex = 男, className = 軟工), 
    Student(id = 5, name = Bob, age = 20, sex = 男, className = 軟工)
  ]
}
*/

本例中使用到的downstream的方式更為通用,可以實現絕大多數的功能,例3中的方法JDK提供的簡寫方式

下面是用collectingAndThen的方式實現和例3相同的功能

Map<String, Long> map = studentList.stream()
        .collect(
                Collectors.groupingBy(
                        Student::getClassName,
                        Collectors.collectingAndThen(Collectors.toList(), arr -> {
                            return (long) arr.size();
                        })
                )
        );
/*
輸出:
{
  計科=4, 
  軟工=5
}
*/

例8:將資料轉為ID和Name對應的資料結構Map

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map);
/*
輸出:
{
  1=tom, 
  2=lisa, 
  3=Ada, 
  4=Dora, 
  5=Bob, 
  6=Farrah, 
  7=Helen, 
  8=jerry, 
  9=Adam
}
*/
  • 情況1

上面程式碼,在現有的資料下正常執行,當新增多新增一條資料

studentList.add(new Student(9, "Adam - 2", 20, "男", "計科"));

這個時候id為9的資料有兩條了,這時候再執行上面的程式碼就會出現Duplicate key Adam

也就是說呼叫toMap時,假設其中存在重複的key,如果不做任何處理,會拋異常

解決異常就要引入toMap方法的第3個引數mergeFunction,函式式介面方法簽名如下

R apply(T t, U u);

程式碼修改後如下

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName, (v1, v2) -> {
        System.out.println("value1: " + v1);
        System.out.println("value2: " + v2);
        return v1;
    }));
/*
輸出:
value1: Adam
value2: Adam - 2
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam}
*/

可以看出來mergeFunction 引數v1為原值,v2為新值

日常開發中是必須要考慮第3引數的mergeFunction,一般採用策略如下

// 引數意義: o 為原值(old),n 為新值(new)
studentList.stream()
    // 保留策略
    .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> o));


studentList.stream()
    // 覆蓋策略
    .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> n));
  • 情況2

在原有的資料下增加一條特殊資料,這條特殊資料的namenull

studentList.add(new Student(10, null, 20, "男", "計科"));

此時原始程式碼情況1的程式碼都會出現空指標異常

解決方式就是toMap的第二引數valueMapper返回值不能為null,下面是解決的方式

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(
        Student::getId,
        e -> Optional.ofNullable(e.getName()).orElse(""),
        (o, n) -> o
     ));
System.out.println(map);
/*
輸出:
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam, 10=}
*/
// 此時沒有空指標異常了

還有一種寫法(參考寫法,不用idea工具編寫程式碼,這種寫法沒有意義)

public final class Func {

    /**
     * 當 func 執行結果為 null 時, 返回 defaultValue
     *
     * @param func         轉換函式
     * @param defaultValue 預設值
     * @return
     */
    public static <T, R> Function<T, R> defaultValue(@NonNull Function<T, R> func, @NonNull R defaultValue) {
        Objects.requireNonNull(func, "func不能為null");
        Objects.requireNonNull(defaultValue, "defaultValue不能為null");
        return t -> Optional.ofNullable(func.apply(t)).orElse(defaultValue);
    }

}

Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(
        Student::getId,
        Func.defaultValue(Student::getName, null),
        (o, n) -> o
    ));
System.out.println(map);

這樣寫是為了使用像idea這樣的工具時,Func.defaultValue(Student::getName, null)呼叫第二個引數傳null會有一個告警的標識『不關閉idea的檢查就會有warning提示』。

綜上就是toMap的使用注意點,

key對映的id有不能重複的限制,value對映的name也有不能有null,解決方式也在下面有提及

例9:封裝一下關於Stream的工具類

工作中使用Stream最多的操作都是對於集合來的,有時Stream使用就是一個簡單的過濾filter或者對映map操作,這樣就出現了大量的.collect(Collectors.toMap(..., ..., ...)).collect(Collectors.toList()),有時還要再呼叫之前檢測集合是否為null,下面就是對Stream的單個方法進行封裝

public final class CollUtils {

    /**
     * 過濾資料集合
     *
     * @param collection 資料集合
     * @param filter     過濾函式
     * @return
     */
    public static <T> List<T> filter(Collection<T> collection, Predicate<T> filter) {
        if (isEmpty(collection)) {
            return Collections.emptyList();
        }
        return collection.stream()
                .filter(filter)
                .collect(Collectors.toList());
    }

    /**
     * 獲取指定集合中的某個屬性
     *
     * @param collection 資料集合
     * @param attrFunc   屬性對映函式
     * @return
     */
    public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc) {
        return attrs(collection, attrFunc, true);
    }

    /**
     * 獲取指定集合中的某個屬性
     *
     * @param collection  資料集合
     * @param attrFunc    屬性對映函式
     * @param filterEmpty 是否過濾空值 包括("", null, [])
     * @return
     */
    public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc, boolean filterEmpty) {
        if (isEmpty(collection)) {
            return Collections.emptyList();
        }
        Stream<R> rStream = collection.stream().map(attrFunc);
        if (!filterEmpty) {
            return rStream.collect(Collectors.toList());
        }
        return rStream.filter(e -> {
            if (Objects.isNull(e)) {
                return false;
            }
            if (e instanceof Collection) {
                return !isEmpty((Collection<?>) e);
            }
            if (e instanceof String) {
                return ((String) e).length() > 0;
            }
            return true;
        }).collect(Collectors.toList());
    }


    /**
     * 轉換為map, 有重複key時, 使用第一個值
     *
     * @param collection  資料集合
     * @param keyMapper   key對映函式
     * @param valueMapper value對映函式
     * @return
     */
    public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
                                            Function<T, K> keyMapper,
                                            Function<T, V> valueMapper) {
        if (isEmpty(collection)) {
            return Collections.emptyMap();
        }
        return collection.stream()
                .collect(Collectors.toMap(keyMapper, valueMapper, (k1, k2) -> k1));
    }

    /**
     * 判讀集合為空
     *
     * @param collection 資料集合
     * @return
     */
    public static boolean isEmpty(Collection<?> collection) {
        return Objects.isNull(collection) || collection.isEmpty();
    }
}

如果單次使用Stream都在一個函式中可能出現大量的冗餘程式碼,如下

// 獲取id集合
List<Integer> idList = studentList.stream()
    .map(Student::getId)
    .collect(Collectors.toList());
// 獲取id和name對應的map
Map<Integer, String> map = studentList.stream()
    .collect(Collectors.toMap(Student::getId, Student::getName, (k1, k2) -> k1));
// 過濾出 軟工 班級的人員
List<Student> list = studentList.stream()
    .filter(e -> Objects.equals(e.getClassName(), "軟工"))
    .collect(Collectors.toList());

使用工具類

// 獲取id集合
List<Integer> idList = CollUtils.attrs(studentList, Student::getId);
// 獲取id和name對應的map
Map<Integer, String> map = CollUtils.toMap(studentList, Student::getId, Student::getName);
// 過濾出 軟工 班級的人員
List<Student> list = CollUtils.filter(studentList, e -> Objects.equals(e.getClassName(), "軟工"));

工具類旨在減少單次使用Stream時出現的冗餘程式碼,如toMaptoList,同時也進行了為null判斷

總結

本篇介紹了函式式介面LambdaOptional方法引用Stream等一系列知識點

也是工作中經過長時間積累終結下來的,比如例5中每一個操作都換一行,這樣不完全是為了格式化好看

List<String> allFemaleNameList = studentList.stream()
    .filter(stu -> Objects.equals("女", stu.getSex()))
    .map(Student::getName)
    .collect(Collectors.toList());
System.out.println(allFemaleNameList);
// 這樣寫 .filter 和 .map 的函式表示式中報錯可以看出來是那一行

如果像下面這樣寫,報錯是就會指示到一行上不能直接看出來是.filter還是.map報的錯,並且這樣寫也顯得擁擠

List<String> allFemaleNameList = studentList.stream().filter(stu -> Objects.equals("女", stu.getSex())).map(Student::getName).collect(Collectors.toList());
System.out.println(allFemaleNameList);

Stream的使用遠遠不止本篇文章介紹到的,比如一些同類的IntStreamLongStreamDoubleStream都是大同小異,只要把Lambda搞熟其他用法都一樣

學習Stream流一定要結合場景來,同時也要注意Stream需要規避的一些風險,如toMap的注意點(例8有詳細介紹)。

還有一些高階用法downstream下游 - 收集器等(例4,例7)。

相關文章