1. 程式人生 > >Java8————Stream API

Java8————Stream API

引言

Java8 加入了java.util.stream包,這個包中的相關API將極大的增強容器物件對元素的操作能力。

它專注於對集合物件進行各種便利、高效的聚合操作,或大批量資料處理。

Stream API藉助於同樣新出現的Lambda表示式,極大的提高了程式設計效率和程式信噪比。

它提供了序列和並行兩種模式進行匯聚操作。並行模式底層採用 Fork / Join 框架來拆分任務和加速處理過程。

什麼是流?

一、流的概念

1、流不是資料結構

它沒有內部儲存,它只是用操作管道從source(資料結構、陣列、IO channel)抓取資料。

2、不修改源資料

例如Stream 的 filter操作會產生一個不包含被過濾元素的新的Stream,而不是從source中刪除那些元素。

3、流的操作引數

所有的Stream操作必須以Lambda表示式作為引數。

4、不支援索引訪問

Stream操作實際上是 增強For迴圈 的函式程式設計變式,它沒有元素下標的訪問方式。

5、流可以轉換成陣列或者List

6、惰性化

Intermediate操作永遠是惰性化的

7、並行能力

當一個集合不要求元素的順序時,我們可以通過Stream的並行化特性來充分利用多核資源,不需要再寫多執行緒程式碼,所有對它的操作會自動並行進行。

8、可以是無限的

集合有固定大小,Stream則不必,limit(n)、findFirst()這類short-circuiting操作可以對無限的Stream進行運算並很快完成。

二、流的操作分類

流的操作型別被分為三種:Intermediate、Terminal、short-circuiting

Intermediate :代表流的中間操作,這種操作的目的主要是開啟流,做出某種程度的對映或過濾,然後返回一個新的流,交給下一個操作使用。這類操作是惰性的,也就是說,僅僅呼叫到這類方法,並沒有真正開始流的遍歷。

Terminal :一個流只能有一個Terminal操作。所以這必定是流的最後一個操作。而Terminal操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個副作用。

short-circuiting :對於Intermediate 操作,如果接收的是一個無限大的Stream,則返回一個有限的新Stream;對於Terminal

操作,如果它接收的是一個無限大的Stream,但能在有限的時間計算出結果。

三、惰性化(lazy)

我們說Intermediate操作都是惰性化的,這如何理解?在對於一個Stream進行多次轉換操作(Intermediate操作),每次操作都對Stream中的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是 N(轉換次數)個for迴圈裡所有操作都執行完的總和嗎?其實不是這樣的!

我們說轉換操作是Lazy的,多個轉換操作只會在Terminal 操作的時候融合進來,一次迴圈完成。

我們可以這樣簡單的類比,在Java 8 未引進Stream API的時候,使用命令式進行for迴圈,並對每個元素進行諸如 if-else 、賦值、計算、獲取、新增等操作,而這些操作你可以理解為Stream中的Intermediate操作,只有在for迴圈真正執行的時候才會執行它們,這就是惰性化的語義,即提前安排好篩選、計算等Intermediate操作,當迴圈時再執行它們

常見用法歸納

一、建立流

我們可以通過一個“集合”物件來建立流,這個集合物件並不限於Collection介面,還包含那些能容納多個物件的容器。

建立流的方式大致可以分為三種:Arrays.stream()、Stream.of()、集合.stream(),下面舉例來說明:

1、陣列生成流

// 基本型別陣列
int[] numx = new int[] { 1, 2, 2, 3, 5 };
// 方法一:
IntStream stream1 = Arrays.stream(numx);

// 方法二:
IntStream stream2 = IntStream.of(numx);

// 引用型別陣列
Integer[] nums = new Integer[] { 1, 2, 2, 3, 5 };

// 方法一:
Stream<Integer> stream3 = Arrays.stream(nums);

// 方法二:
Stream<Integer> stream4 = Stream.of(nums);

需要注意的是,對於基本數值型,目前有三種對應的包裝型別 Stream:

IntStream、LongStream、DoubleStream。當然我們也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 會很耗時,所以特別為這三種基本數值型提供了對應的 Stream。 Java 8 中還沒有提供其它數值型 Stream,因為這將導致擴增的內容較多。而常規的數值型聚合運算可以通過上面三種 Stream 進行。

 2、集合生成流

List<Integer> numsList = Arrays.asList( 1, 2, 2, 3, 5 );
// 使用parallelStream會將List進行分段並行處理,因此處理的順序是不固定的。
Stream<Integer> parallelStream = numsList.parallelStream();

二、流轉化為容器(Terminal)

Stream<String> names = Arrays.asList("Tom", "Jerry", "Tim", "Morty").stream();

1、Stream轉Array

String[] namesArr = names.toArray(String[]::new);

2、 Stream轉Collection

List<String> list1 = names.collect(Collectors.toList());
// 或
List<String> list2 = names.collect(Collectors.toCollection(ArrayList::new));
Set<String> set = names.collect(Collectors.toSet());
Stack<String> stack = names.collect(Collectors.toCollection(Stack::new));

3、Stream轉String

String str = names.collect(Collectors.joining());// joining()有過載

 三、對映操作(Intermediate)

map將input stream中的每一個元素,對映成output  stream中的另外一個元素(一對一對映)

List<String> output = names.map(String::toUpperCase).collect(Collectors.toList());

四、多集合對映操作(Intermediate)

flatMap(功能和map相同,只不過對映是一對多),flatMap 把 input Stream 中的層級結構扁平化,就是將最底層元素抽出來放到一起。

List<String> names1 = Arrays.asList("Tom", "Jerry", "Tim", "Morty");
List<String> names2 = Arrays.asList("Tony", "Jack", "Tina", "Marry");
List<String> collect = Stream.of(names1, names2)
                .flatMap(ns -> ns.stream().map(String::toLowerCase))
                .collect(Collectors.toList());

五、篩選操作(Intermediate)

filter對原始 Stream 進行某項測試,通過測試的元素被留下來生成一個新 Stream。

Integer[] sixNums = { 1, 2, 3, 4, 5, 6 };
Integer[] evens = Stream.of(sixNums).filter(n -> n % 2 == 0).toArray(Integer[]::new);

六、迴圈操作(Terminal)

forEach() 方法接收一個 Lambda 表示式,然後在 Stream 的每一個元素上執行該表示式。

但一般認為,forEach 和常規 for 迴圈的差異不涉及到效能,它們僅僅是函式式風格與傳統 Java 風格的差別.

注意:forEach 不能修改自己包含的本地變數值,也不能用 break/return 之類的關鍵字提前結束迴圈。

當需要為多核系統優化時,可以 parallelStream().forEach()。另外一點需要注意,forEach 是 terminal 操作。具有相似功能的 intermediate 操作 peek 可以達到上述目的。

Stream.of("one", "two", "three", "four")
                .filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList());

七、第一個元素(Terminal)

findFirst是一個 termimal 兼 short-circuiting 操作,它總是返回 Stream 的第一個元素,或者空。

注意,它的返回值型別:Optional。使用Optional的目的是儘可能避免 NullPointerException。它提供的是編譯時檢查,能極大的降低 NPE 這種 Runtime Exception 對程式的影響。

Optional<String> firstName = names2.stream().findFirst();

八、聚合操作(Terminal)

reduce方法的主要作用是把 Stream 元素組合起來。它提供一個起始值(種子),然後依照運算規則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字串拼接、數值的 sum、min、max、average 都是特殊的 reduce。

下面程式碼例如第一個示例的 reduce(),第一個引數(空白字元)即為起始值,第二個引數(String::concat)為 BinaryOperator。這類有起始值的 reduce() 都返回具體的物件。而對於第四個示例沒有起始值的 reduce(),由於可能沒有足夠的元素,返回的是 Optional,請留意這個區別。

// 字串連線,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 無起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 過濾,字串連線,concat = "ace"
String s = Stream.of("a", "B", "c", "D", "e", "F")
                .filter(x -> x.compareTo("Z") > 0)
                .reduce("", String::concat);

九、limit/skip (Short-circuiting)

limit 返回 Stream 的前面 n 個元素;skip 則是扔掉前 n 個元素(它是由一個叫 subStream 的方法改名而來)。

List<String> persons = new ArrayList<>();
for (int i = 1; i <= 10000; i++) {
    persons.add(new String("name" + i));
}
List<String> personNameList = persons.stream()
                .map(String::toUpperCase)
                .limit(10)
                .skip(3)
                .collect(Collectors.toList());

 上述程式碼是一個有 10000 個元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的toUpperCase()方法的執行次數為 limit 所限定的 10 次,而最終返回結果再跳過前 3 個元素後只有後面 7 個返回。

執行結果:

注意,有一種情況 limit/skip 無法達到 short-circuiting 目的,就是把它們放在 Stream 的排序操作後,原因跟 sorted這個 intermediate 操作有關:此時系統並不知道 Stream 排序後的次序如何,所以 sorted 中的操作看上去就像完全沒有被 limit 或者 skip 一樣。

十、排序操作(Intermediate)

對 Stream 的排序通過 sorted 進行,它比陣列的排序更強之處在於你可以首先對 Stream 進行各類 map、filter、limit、skip 甚至 distinct 來減少元素數量後,再排序,這能幫助程式明顯縮短執行時間。

List<String> pList = persons.stream().limit(2).sorted((p1, p2) -> p1.toString().compareTo(p2.toString()))
                .collect(Collectors.toList());

 十一、最大/最小值、去重操作(Intermediate)

min 和 max 的功能也可以通過對 Stream 元素先排序,再 findFirst 來實現,但前者的效能會更好,為 O(n),而 sorted 的成本是 O(n log n)。同時它們作為特殊的 reduce 方法被獨立出來也是因為求最大最小值是很常見的操作。

BufferedReader br = new BufferedReader(new FileReader("c:\\noThisFile.txt"));
int longest = br.lines()
                .mapToInt(String::length)
                .max().getAsInt();
        
br.close();
System.out.println(longest);

十二、匹配操作(Terminal)

Stream 有三個 match 方法,從語義上說:

allMatch:Stream 中全部元素符合傳入的 predicate,返回 true 

anyMatch:Stream中只要有一個元素符合傳入的 predicate,返回 true

noneMatch:Stream 中沒有一個元素符合傳入的predicate,返回 true

它們都不是要遍歷全部元素才能返回結果。例如 allMatch 只要一個元素不滿足條件,就 skip 剩下的所有元素,返回 false。

List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream()
                .allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream()
                .anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);

鳴謝