五分鐘學習 Java 8 的流程式設計

WngShhng發表於2018-05-25

1、概述

  1. Java8中在Collection中增加了一個stream()方法,該方法返回一個Stream型別。我們就是用該Stream來進行流程式設計的;
  2. 流與集合不同,流是隻有在按需計算的,而集合是已經建立完畢並存在快取中的;
  3. 流與迭代器一樣都只能被遍歷一次,如果想要再遍歷一遍,則必須重新從資料來源獲取資料;
  4. 外部迭代就是指需要使用者去做迭代,內部迭代在庫內完成的,無需使用者實現;
  5. 可以連線起來的流操作稱為中間操作,關閉流的操作稱為終端操作(從形式上看,就是用.連起來的操作中,中間的那些叫中間操作,最終的那個操作叫終端操作)。

2、篩選

2.1 過濾

Stream<
T>
filter(Predicate<
? super T>
predicate);
複製程式碼

filter通過指定一個Predicate型別的行為引數對流中的元素進行過濾,最終還是會返回一個流,因為它是中間操作。中間操作返回的結果都是一個流,所以,如果我們想要得到一個集合或者其他的非流型別,就需要使用終端操作來獲取。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<
Integer>
filter = list.stream().filter(integer ->
integer >
3).collect(Collectors.toList());
// [4, 5, 5, 6, 7, 8, 9]複製程式碼

2.2 去重

Stream<
T>
distinct();
複製程式碼

上面就是去重的方法的定義,它會按照流中的元素的equal()和hashCode()方法進行去重。去重之後將繼續返回一個流,所以它也是中間操作。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<
Integer>
filter = list.stream().filter(integer ->
integer >
3).distinct().collect(Collectors.toList());
// [4, 5, 6, 7, 8, 9]複製程式碼

2.3 限制

Stream<
T>
limit(long maxSize);
複製程式碼

就像是SQL裡面的limit語句,在流中也有類似的limit()方法。它用於限制返回的結果的數量,將會從流的頭開始取固定數量的元素,也是中間操作,使用完之後仍然會返回一個流。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<
Integer>
filter = list.stream().filter(integer ->
integer >
3).limit(3).collect(Collectors.toList());
// [4, 5, 5]複製程式碼

2.4 跳過

Stream<
T>
skip(long n);
複製程式碼

這個方法的定義和limit()幾分相似。它也是中間操作,用於跳過從流的頭開始指定數量的元素,使用完之後仍然會返回一個流。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<
Integer>
filter = list.stream().filter(integer ->
integer >
3).skip(3).collect(Collectors.toList());
// [6, 7, 8, 9]複製程式碼

3、對映

<
R>
Stream<
R>
map(Function<
? super T, ? extends R>
mapper);
複製程式碼

還記得Function函式介面的方法嗎?它允許你把輸入的型別轉換成另一種型別。上面就是它在map()方法中的應用。在流操作中使用了該方法之後,流就會嘗試將當前流中所有的元素轉換成另一種型別。當你呼叫終端操作collect()的時候,自然也就得到了另一種型別的集合。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<
String>
filter = list.stream().map((integer ->
String.valueOf(integer) + "-")).collect(Collectors.toList());
// 結果:[1-, 1-, 2-, 3-, 4-, 5-, 5-, 6-, 7-, 8-, 9-]複製程式碼

4、查詢

Optional<
T>
findFirst();
Optional<
T>
findAny();
複製程式碼

在指定的流中查詢元素的時候可以用這兩個方法,它們是Stream介面中的方法,返回的已經不再是Stream型別了,這可以說明它們是終端操作。所以,通常也是用來放在終端,繼續操作的話就要使用Optional介面的方法了。

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
Optional<
Integer>
optionalInteger = list.stream().filter(integer ->
integer >
10).findAny();
Optional<
Integer>
optionalInteger = list.stream().filter(integer ->
integer >
10).findFirst();
複製程式碼

上面是使用的兩個示例,這裡返回的結果是Optional型別的。Optional的設計借鑑了Guava中的Optional。使用它的好處是你不需要像以前一樣將返回的結果與null進行判斷,並在結果為null的時候通過=賦值一個預設值了。使用Optional中的方法,你可以更優雅地完成相同的操作。下面我們列出Optional中的一些常用的方法:

編號 方法 說明
1 isPresent() 判斷值是否存在,存在的話就返回true,否則返回false
2 isPresent(Consumer block) 在值存在的時候執行給定的程式碼
3 T get() 如果值存在,那麼返回該值;否則,丟擲NoSuchElement異常
4 T orElse(T other) 如果值存在,那麼返回該值;否則,則返回other

5、匹配

boolean allMatch(Predicate<
? super T>
predicate);
boolean noneMatch(Predicate<
? super T>
predicate);
boolean anyMatch(Predicate<
? super T>
predicate);
複製程式碼

從定義上面來看,上面的三個方法也是終端操作。它們分別用來判斷:流中的資料是否全部匹配指定的條件,流中的資料是否全部不匹配指定的條件,流中的資料是否存在一些匹配指定的條件。下面是一些示例:

List<
Integer>
list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
boolean allMatch = list.stream().allMatch(integer ->
integer <
10);
boolean anyMatch = list.stream().anyMatch(integer ->
integer >
3);
boolean noneMatch = list.stream().noneMatch(integer ->
integer >
100);
複製程式碼

6、歸約

Optional<
T>
reduce(BinaryOperator<
T>
accumulator);
T reduce(T identity, BinaryOperator<
T>
accumulator);
複製程式碼

Stream介面中的reduce方法共有三個過載版本,上面我們給出常用的兩個的定義。它們基本是類似的,只是第二個方法引數列表中多了個初始值,而沒有初始值的那個,返回了Optinoal型別;所以,區別不大,我們只要搞明白它的行為就可以了。下面是歸約的例子:

List<
String>
list = Arrays.asList("a", "b", "c", "d", "e", "f");
String ret = list.stream().reduce("-", (a, b) ->
a + b);
複製程式碼

它的輸出結果是-abcdef,顯然它的效果就是:假如,$是某種操作,List是某個”數列”,那麼歸約的意義就是初始值$n[0]$n[1]$n[2]$...$n[n-1]

7、數值流

同樣是因為裝箱的效能原因,Java8中為數值型別專門提供了數值流:IntStream DoubleStream和LongStream。Stream介面提供了三個中間方法來完成從任意流對映到數值流的操作:

IntStream mapToInt(ToIntFunction<
? super T>
mapper);
LongStream mapToLong(ToLongFunction<
? super T>
mapper);
DoubleStream mapToDouble(ToDoubleFunction<
? super T>
mapper);
複製程式碼

所以你可以用上面三個方法從任意流中獲取數值流。然後,再利用數值流的方法來完成其他的操作。上面三個數值流和Stream介面都繼承子BaseStream,所以它們包含的方法還是有區別的,但總體上來說大同小異。Stream比較具有一般性,上面三個數值流更有針對性,後者也提供了許多便利的方法。如果想要從數值流中獲取物件流,你可以呼叫它們的boxed()方法,來獲取裝箱之後的流。

這裡稍提及一下,對於Optional,Java8也為我們提供了對應的數值型別:OptionalInt OptionalDouble OptionalLong。

在上面的三種數值流中還有幾個靜態方法用於獲取指定數值範圍的流:

public static LongStream range(long startInclusive, final long endExclusive)public static LongStream rangeClosed(long startInclusive, final long endInclusive)複製程式碼

上面是用於獲取指定範圍的LongStream的方法,一個對應於數學中的開區間,一個對應於數學中的閉區間的概念。

8、構建流

上面我們在獲取流的時候,實際上都是從Collection的預設方法stream()中獲取的流,這有些笨拙。實際上,Java8為我們提供了一些建立流的方法。這裡,我們列舉一下這些方法:

public static<
T>
Builder<
T>
builder() // 1public static<
T>
Stream<
T>
empty() // 2public static<
T>
Stream<
T>
of(T t) // 3public static<
T>
Stream<
T>
of(T... values) // 4public static<
T>
Stream<
T>
iterate(final T seed, final UnaryOperator<
T>
f) // 5public static<
T>
Stream<
T>
generate(Supplier<
T>
s) // 6 public static <
T>
Stream<
T>
concat(Stream<
? extends T>
a, Stream<
? extends T>
b) // 7複製程式碼

上面的方法都是Stream介面中的靜態方法,我們可以用這些方法來獲取到流。下面我們對每個方法做一些簡要的說明:

  1. 從名稱上就可以看出這裡使用了構建者模式,你可以每次呼叫Builder的add()方法插入一個元素來建立流;
  2. 用來建立一個空的流
  3. 建立一個只包含一個元素的流
  4. 使用不定引數建立一個包含指定元素的流
  5. 弄清楚它的原理關鍵是要搞明白後面的UnaryOperator的含義,這是一個函式式介面,並且繼承自Function,不同之處在於它的入參和回參型別相同。這個方法的原理是從某個種子值開始,按照後面的函式的規則進行計算,每次是在之前的值的基礎上執行某個函式的。所以Stream.iterate(2, n ->
    n * n).limit(3)
    將返回由2 4 16構成的流。
  6. 這裡的Supplier也是一個函式介面,它只有一個get()方法,無參,只接受指定型別的返回值。所以,這個方法需要你提供一個用於生成數值的函式(或者說規則),比如Math.random()等等。
  7. 這個比較容易理解,就是通過將兩個流合併來得到一個新的流。

9、收集器

上面我們已經見識過了流的規約操作,但是那些操作還比較幼稚。Java8的收集器為我們提供了更加強大的規約功能。

說起收集器,肯定繞不過兩個類Collector和Collectors,它倆有啥關係呢?其實Collector只是一個介面;Collectors是一個類, 其中的靜態內部類CollectorImpl實現了該介面,並且被Collectors用來提供一些功能。Collectors中有許多的靜態方法用於獲取Collector的例項,使用這些例項我們可以完成複雜的功能。當然,我們也可以通過實現Collector介面來定義自己的收集器。

Stream的collect()方法有3個過載的版本。我們就是通過其中的一個來使用收集器的,這是它的定義:

<
R, A>
R collect(Collector<
? super T, A, R>
collector);
複製程式碼

我們注意一下這個方法的引數和返回型別. 從上面我們可以看出傳入的Collector有3個泛型,其中的最後一個泛型別R與返回的型別是一致的. 這很重要——可以預防你呼叫了某個方法卻不知道最終返回的是什麼型別。

我們先來看一些簡單的例子,這裡的stream是由Student物件構成的流:

Optional<
Student>
student = stream.collect(Collectors.maxBy(comparator)) // 需要傳入一個比較器到maxBy()方法中long count = stream.collect(Collectors.counting())複製程式碼

上面的兩種方式比較雞肋,因為你可以使用count()和max()方法來替代它們。下面我們再看一些收集器的其他例子,注意在這些例子中,我並沒有使用lambda簡化函式式介面,是因為想要你更清楚地看到它的泛型別和方法定義。這可能有助於你理解這些方法的作用機理。

9.1 計算平均值和總數

下面的語句用於計算平均值,類似的還有summingInt()用於計算總數。它們的用法是相似的。

Double d = stream.collect(Collectors.averagingInt(new ToIntFunction<
Student>
() {
@Override public int applyAsInt(Student value) {
return value.getGrade();

}
}));
複製程式碼

從上面我們看出,呼叫averagingInt()方法的時候需要傳入一個ToIntFunction函式式介面,用於根據指定的型別返回一個整數值。

9.2 連線字串

joining()工廠方法是專門用來連線字串的,它要求流是字串流,所以在對Student流進行拼接之前,需要先將其對映成字串流:

String members = stream.map(new Function<
Student, String>
() {
@Override public String apply(Student student) {
return student.getName();

}
}).collect(Collectors.joining(", "));
// 使用','將字串拼接起來複製程式碼

9.3 廣義的規約彙總

Optional<
Student>
optional = stream.collect(Collectors.reducing(new BinaryOperator<
Student>
() {
@Override public Student apply(Student student, Student student2) {
return student.getGrade() >
student2.getGrade() ? student : student2;

}
}));
複製程式碼

上面的就是用來規約的函式。我們用了reducing工廠方法,並向其中傳入一個BinaryOperator型別。這裡我們指定最終的返回型別是Student。所以,上面的程式碼的效果是獲取成績最大的學生。

9.4 分組

Collectors中的分組還是比較有意思的。我們先看groupingBy方法的定義:

Collector<
T, ?, Map<
K, D>
>
groupingBy(Function<
? super T, ? extends K>
classifier)Collector<
T, ?, Map<
K, D>
>
groupingBy(Function<
? super T, ? extends K>
classifier, Collector<
? super T, A, D>
downstream)複製程式碼

groupingBy方法有3個過載的版本,這裡我們給出其中常用的兩個。第一個方法是通過指定規則對流進行分組的,而第二個方法先通過classifier指定的規則對流進行分組,然後用downstream的規則對分組後的流進行後續的操作。注意第二個引數仍然是Collector型別,這說明我們仍然可以對分組後的流再次收集,比如再分組、求最大值等等。

Map<
Integer, List<
Student>
>
map = stream.collect(Collectors.groupingBy(new Function<
Student, Integer>
() {
@Override public Integer apply(Student student) {
return student.getClazz();

}
}));
複製程式碼

以上是groupingBy()方法的第一個例子。注意這裡我們是通過將Student通過’班級欄位’對映成一個整數來進行分組的。下面是一個二次分組的例子。這裡的用了上面的第二個groupingBy()方法,並在downstream中指定了另一個分組操作。

Map<
Integer, Map<
Integer, List<
Student>
>
>
map = stream.collect(Collectors.groupingBy(new Function<
Student, Integer>
() {
@Override public Integer apply(Student student) {
return student.getClazz();

}
}, Collectors.groupingBy(new Function<
Student, Integer>
() {
@Override public Integer apply(Student student) {
return student.getGrade() == 100 ? 1 : student.getGrade() >
90 ? 2 : student.getGrade() >
80 ? 3 : 4;

}
})));
複製程式碼

9.5 分割槽

與分組類似的還有一個分割槽的操作,分割槽只是分組的一種特例。它們的使用方式也基本一致,它的方法簽名與上面的groupingBy方法類似。我們直接看它的一個使用的方式好了:

Map<
Boolean, List<
Student>
>
map = stream.collect(Collectors.partitioningBy(new Predicate<
Student>
() {
@Override public boolean test(Student student) {
return student.getGrade() >
90;

}
}));
複製程式碼

這就是分割槽的使用方式。它通過一個指定的函式式介面,將指定的型別對映到一個布林型別。所以,它類似與分組,只不過它分組的結果只有兩種,要麼true,要麼false。當然,類似於分組,你也可以在partitioningBy()方法的第二個引數中再指定一個收集器,這樣就可以對分割槽後的流進行後續的操作了。

總結:

以上就是Java8中的流的常見的用法,這裡只是列舉了一些常見的、Java8 API中提供的一些類和方法。重點仍然是搞清楚其中的設計的原理,不要盲目記憶。學習的時候結合JDK原始碼進行,看到方法的定義就大致瞭解了它的設計原理。最後,不得不說的是,使用流程式設計確實很簡潔和優雅。

相關程式碼:Github

來源:https://juejin.im/post/5b07f4536fb9a07ac90da4e5

相關文章