Java-stream(1) Stream基本概念 & Stream介面

小肖愛吃肉發表於2020-11-25

Java8 集合中的 Stream 相當於高階版的 Iterator,他可以通過 Lambda 表示式對集合進行各種非常便利、高效的聚合操作(Aggregate Operation),或者大批量資料操作 (Bulk Data Operation)
Stream 就如同一個迭代器(Iterator),單向,不可往復,資料只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返
函式式的解決方案解開了程式碼細節和業務邏輯的耦合,類似於sql語句,表達的是"要做什麼"而不是"如何去做",使程式設計師可以更加專注於業務邏輯,寫出易於理解和維護的程式碼

一、基本概念

1.1 Stream 操作分類

在這裡插入圖片描述
(圖片來源:極客時間-《java效能調優實戰》第6講)

我們通常還會將中間操作稱為懶操作,也正是由這種懶操作結合終結操作、資料來源構成的處理管道(Pipeline),實現了 Stream 的高效

1.2 Stream 主要架構類關係

在這裡插入圖片描述

二、Stream介面

2.1 概述(介面文件)

什麼是stream?

是一個支援序列/並行的聚合操作的元素佇列。為了進行計算,組建了一個 stream管道(pipeline),一個流管道由一個源(可能是陣列、集合、建構函式、I/O流等)、0到多箇中間操作(intermediate operations)和終結操作(terminal operation)組成。

stream有哪些特點?

-》流是惰性的,只有在啟動終結操作時才對源資料執行計算,只執行中間操作的時候是不會觸發流計算的,並且僅在需要時才使用源元素。

-》集合和stream雖然在表面有一些相似之處,但是他兩的側重點是不一樣的。集合主要是對元素的管理和訪問。相比之下,stream不提供直接訪問或者操作元素的方法,而是關注於宣告性地描述元素的聚合和計算操作,然而,如果所提供的流操作不提供所需的功能,則可以使用這些操作來執行受控遍歷。

-》如果stram popeline在操作過程中對源資料進行了修改操作會導致不可預知的報錯

-》引數會去呼叫函式介面(Function)或者通常使用lambda表示式引用,所以這些引數必須“非空”

-》在stream中資料從源到終結只會被執行一次,不可以重複運算元據,如果有重複使用的情況會丟擲 IllegalStateException 異常(由於一些流操作可能返回其接收器而不是新的流物件,因此可能不可能在所有情況下檢測重用)

-》stream有一個close()方法,還有實現了AutoCloseable介面(BaseStream繼承),但是大多數stream例項在使用完後不需要關閉,通常,只有源為IO通道的流才需要關閉。大多數流由集合、陣列或生成器組成,這些函式不需要特殊的資源管理

-》流管道可以序列執行資料,可以並行執行,通過paralle()方法去選擇不同的執行方式

2.2 一些常用方法

filter()

作用:中間-無狀態操作,對流資料進行過濾,返回滿足入參條件的資料
入參:Predicate<? super T> predicate 資料的過濾條件
出參:Stream:返回一個新的符合條件的Stream 流資料
舉例:過濾出 小於3 的元素

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).filter(p -> p < 3).collect(Collectors.toList());
  System.out.println(list);
}

輸出:[1, 2]

map()

作用:中間-無狀態操作,內部通過函式的形式對流資料進行一系列操作,返回函式結果集
入參:Function<? super T, ? extends R> mapper:操作函式
出參:Stream :返回一個新的函式處理結果集的流資料
舉例:將每個元素 +1

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).map(p -> p + 1).collect(Collectors.toList());
  System.out.println(list);
}

輸出:[2, 3, 4, 5]

distinct()

作用:
中間-有狀態操作,對當前流資料進行元素去重操作(通過equals()方法判斷),返回一個由不同元素組成的流資料,對於有序流,不同元素的選擇是穩定的(對於重複的元素,保留遇到的第一個出現的元素),對於無序流,不提供穩定性保證。如果是在並行管道(paralle pipeline)中,保持流資料的排序穩定性是比較昂貴的(要求當前操作充當一個完整的屏障,具有大量的緩衝開銷)。

一般情況下業務程式碼中是不需要保持排序穩定性的,是有無需流資料來源(generate())或者通過unordered()方法刪除排序約束可以更好的提高在並行管道中的執行效率。如果必須要做到排序的穩定性,最好切換到順序執行來提高效能。

入參:無入參,對當前流資料進行去重操作(this)
出參:Stream:去重後的一個新的流資料
舉例:將元素去重

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 4, 4).distinct().collect(Collectors.toList());
  System.out.println(list);
}
sorted()

作用:中間-有狀態操作,對當前流資料進行自然排序(按照compareTo()規則排序),如果當前流資料沒有實現Comparable介面的話會丟擲 ClassCastException 異常。同樣,對於有序流,排序是穩定的。對於無序流,不提供穩定性保證。
入參:無入參,對當前流資料進行排序操作(this)
出參:Stream:排序後的一個新的流資料
備註:該方法還提供了過載方法,入參為Comparator介面物件,這個方法根據Comparator提供的規則來進行排序。
舉例:對元素進行排序

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 3, 4, 2).sorted().collect(Collectors.toList());
  System.out.println(list);
}

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

peek()

作用:
中間-無狀態操作,對流資料進行一些操作,但是它只是對Stream中的元素進行某些操作(比如輸出之類),但是操作之後的資料並不返回到Stream中,所以返回的還是原來的元素,不會像map()一樣返回一個新的型別的流資料,通常會作為debug列印中間資料使用。

這裡要注意的是,如果流資料是個實體物件的話,peek()可以通過呼叫實體物件的setter方法對其屬性值進行改變——也就是說peek()可會流資料進行修改操作,其他方法是不具備的(會建立一個新的物件作為返回結果)。

入參:Consumer<? super T> action:對當前流資料的操作函式
出參:Stream:當前流資料
備註:map()和peek()的區別詳解移步:https://www.cnblogs.com/flydean/p/java-8-stream-peek.html
舉例:輸出每個元素值

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).peek(p -> System.out.println("輸出:p = " + p)).collect(Collectors.toList());
  System.out.println(list);
}

輸出:

輸出:p = 1

輸出:p = 2

輸出:p = 3

輸出:p = 4

[1, 2, 3, 4]

此外stream介面還提供了例如limit(限定流資料的長度),min(返回小元素),max(返回最大元素)等方法,這裡就不一一展開說明了,其原理都是相似的。

三、擴充套件

擴充套件一:Predicate介面

Predicate是個斷言式介面,其引數是<T,boolean>,也就是給一個引數T,返回boolean型別的結果。跟Function一樣,Predicate的具體實現也是根據傳入的lambda表示式來決定的。

boolean test(T t);

擴充套件二:排序穩定性

假定在一個待排序的序列中,存在多個相同的元素,若經過排序操作,這些元素的相對次序不變,則成為這種演算法是穩定的,否則就是不穩定的。

eg. 在原序列中,node1==node2 && node1在node2之前
排序後:如node1在node2之前並且無論執行多少次都是這樣,則認為這種演算法是穩定的,否則是不穩定的。

寫在最後 歡迎關注微信公眾號【小肖愛吃肉】和你一起記錄生活的小美好
在這裡插入圖片描述