1. 程式人生 > >JDK8----集合之流式(Streams)操作

JDK8----集合之流式(Streams)操作

JDK8 —- 集合之流式(Streams)操作

為什麼需要 Stream

  Stream 作為 Java 8 的一大亮點,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不同的概念。它也不同於 StAX 對 XML 解析的 Stream,也不是 Amazon Kinesis 對大資料實時處理的 Stream。Java 8 中的 Stream 是對集合(Collection)物件功能的增強,它專注於對集合物件進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量資料操作 (bulk data operation)。Stream API 藉助於同樣新出現的 Lambda表示式,極大的提高程式設計效率和程式可讀性。同時它提供序列和並行

兩種模式進行匯聚操作,併發模式能夠充分利用多核處理器的優勢,使用 fork/join 並行方式來拆分任務和加速處理過程。通常編寫並行程式碼很難而且容易出錯, 但使用 Stream API 無需編寫一行多執行緒的程式碼,就可以很方便地寫出高效能的併發程式。所以說,Java 8 中首次出現的 java.util.stream 是一個函式式語言+多核時代綜合影響的產物

什麼是聚合操作
  在傳統的 J2EE 應用中,Java 程式碼經常不得不依賴於關係型資料庫的聚合操作來完成諸如:
  * 客戶每月平均消費金額
  * 最昂貴的在售商品
  * 本週完成的有效訂單(排除了無效的)
  * 取十個資料樣本作為首頁推薦
這類的操作。
  但在當今這個資料大爆炸的時代,在資料來源多樣化、資料海量化的今天,很多時候不得不脫離 RDBMS,或者以底層返回的資料為基礎進行更上層的資料統計。而 Java 的集合 API 中,僅僅有極少量的輔助型方法,更多的時候是程式設計師需要用 Iterator 來遍歷集合,完成相關的聚合應用邏輯。這是一種遠不夠高效、笨拙的方法。在 Java 7 中,如果要發現 type 為 grocery 的所有交易,然後返回以交易值降序排序好的交易 ID 集合,我們需要這樣寫:

        List<Transaction> groceryTransactions = new ArrayList<>();
        for (Transaction t : transactions) {
            if (t.getType() == Transaction.GROCERY) {
                groceryTransactions.add(t);
            }
        }
        Collections.sort(groceryTransactions, new Comparator() {
            public
int compare(Transaction t1, Transaction t2) { return t2.getValue().compareTo(t1.getValue()); } }); List<Integer> transactionIds = new ArrayList<>(); for (Transaction t : groceryTransactions) { transactionIds.add(t.getId()); }

  而在 Java 8 使用 Stream,程式碼更加簡潔易讀;而且使用併發模式,程式執行速度更快。

        List<Integer> transactionsIds = transactions.parallelStream().
                filter(t -> t.getType() == Transaction.GROCERY).
                sorted(comparing(Transaction::getValue).reversed()).
                map(Transaction::getId).
                collect(toList());

集合之流式操作
  Java 8 引入了流式操作(Stream),通過該操作可以實現對集合(Collection)的並行處理和函式式操作。根據操作返回的結果不同,流式操作分為中間操作和最終操作兩種。最終操作返回一特定型別的結果,而中間操作返回流本身,這樣就可以將多個操作依次串聯起來。根據流的併發性,流又可以分為序列和並行兩種。流式操作實現了集合的過濾、排序、對映等功能。
Stream 和 Collection 集合的區別:Collection 是一種靜態的記憶體資料結構,而 Stream 是有關計算的。前者是主要面向記憶體,儲存在記憶體中,後者主要是面向 CPU,通過 CPU 實現計算。

序列和並行的流
  流有序列和並行兩種,序列流上的操作是在一個執行緒中依次完成,而並行流則是在多個執行緒上同時執行。並行與序列的流可以相互切換:通過 stream.sequential() 返回序列的流,通過 stream.parallel() 返回並行的流。相比較序列的流,並行的流可以很大程度上提高程式的執行效率。

Stream 總覽

什麼是流
  Stream 不是集合元素,它不是資料結構並不儲存資料,它是有關演算法和計算的,它更像一個高階版本的 Iterator。原始版本的 Iterator,使用者只能顯式地一個一個遍歷元素並對其執行某些操作;高階版本的 Stream,使用者只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字串”、“獲取每個字串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的資料轉換。
  Stream 就如同一個迭代器(Iterator),單向,不可往復,資料只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。
  而和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、序列化操作。顧名思義,當使用序列方式去遍歷時,每個 item 讀完後再讀下一個 item。而使用並行去遍歷時,資料會被分成多個段,其中每一個都在不同的執行緒中處理,然後將結果一起輸出。Stream 的並行操作依賴於 Java7 中引入的 Fork/Join 框架(JSR166y)來拆分任務和加速處理過程。Java 的並行 API 演變歷程基本如下:
  1.0-1.4 中的 java.lang.Thread
  5.0 中的 java.util.concurrent
  6.0 中的 Phasers 等
  7.0 中的 Fork/Join 框架
  8.0 中的 Lambda
Stream 的另外一大特點是,資料來源本身可以是無限的

流的構成
  當我們使用一個流的時候,通常包括三個基本步驟:
  獲取一個數據源(source)→ 資料轉換→執行操作獲取想要的結果,每次轉換原有 Stream 物件不改變,返回一個新的 Stream 物件(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道,如下圖所示。
圖 1. 流管道 (Stream Pipeline) 的構成

流的操作型別分為兩種:
  Intermediate:一個流可以後面跟隨零個或多個 intermediate 操作。其目的主要是開啟流,做出某種程度的資料對映/過濾,然後返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅呼叫到這類方法,並沒有真正開始流的遍歷。
  Terminal:一個流只能有一個 terminal 操作,當這個操作執行後,流就被使用“光”了,無法再被操作。所以這必定是流的最後一個操作。Terminal 操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個 side effect。
  
  在對於一個 Stream 進行多次轉換操作 (Intermediate 操作),每次都對 Stream 的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是 N(轉換次數)個 for 迴圈裡把所有操作都做掉的總和嗎?其實不是這樣的,轉換操作都是 lazy 的,多個轉換操作只會在 Terminal 操作的時候融合起來,一次迴圈完成。我們可以這樣簡單的理解,Stream 裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在 Terminal 操作的時候迴圈 Stream 對應的集合,然後對每個元素執行所有的函式。

還有一種操作被稱為 short-circuiting。用以指:
  對於一個 intermediate 操作,如果它接受的是一個無限大(infinite/unbounded)的 Stream,但返回一個有限的新 Stream。
  對於一個 terminal 操作,如果它接受的是一個無限大的 Stream,但能在有限的時間計算出結果。
當操作一個無限大的 Stream,而又希望在有限時間內完成操作,則在管道內擁有一個 short-circuiting 操作是必要非充分條件。

流的使用詳解
  簡單說,對 Stream 的使用就是實現一個 filter-map-reduce 過程,產生一個最終結果,或者導致一個副作用(side effect)。
  
流的構造與轉換
  下面提供最常見的幾種構造 Stream 的樣例。

        // 1. Individual values
        Stream stream = Stream.of("a", "b", "c");

        // 2. Arrays
        String[] strArray = new String[]{"a", "b", "c"};
        stream = Stream.of(strArray);
        stream = Arrays.stream(strArray);

        // 3. Collections
        List<String> list = Arrays.asList(strArray);
        stream = list.stream();

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