淺談java8中的流的使用

邊緣煩惱發表於2019-04-18

我們在開發的過程中會大量的使用集合,集合可以將資料進行分組,處理,好多的處理資料的業務邏輯類似於資料庫的操作,比如說對一系列的實體根據它其中的某個屬性來分組,篩選,像這樣的操作,資料庫是允許你宣告式的指定這些操作的。比如說:

SELECT name FROM apple WHERE weight < 400;
複製程式碼

這樣的業務邏輯,我們之前的程式碼實現都是for迴圈裡面,填上一大堆的if判斷,新建的臨時變數,佔用的程式碼空間很大,而且可讀性也不好。

        List<Apple> appleList = new ArrayList<>();
        List<Apple> wantedAppleList = new ArrayList<>();
        for (Apple app : appleList) {
            if (app.getWeight() < 400) {   //篩選
                wantedAppleList.add(app);
            }
        }

        Collections.sort(wantedAppleList, new Comparator<Apple>() {
            public int compare(Apple d1, Apple d2) {  //排序
                return Long.compare(d1.getWeight(), d2.getWeight());
            }
        });

        List<Long> appleIdList = new ArrayList<>();
        for (Apple d : wantedAppleList) {
            appleIdList.add(d.getId());   //獲取實體id
        }
複製程式碼

看上面的程式碼,佔用的空間很大,而且還會產生和使用垃圾變數,比如程式碼中的appleIdList,它起到的作用只是一個一次性的中間容器。 在java8之後,這樣的語句可以不讓它出現了,你不需要擔心怎麼去顯式的實現如何篩選,你只需要說明你想要什麼就行了。 如果要處理大量的元素,提高效能,你需要並行處理,利用多核架構,但是寫並行程式碼更復雜,而除錯起來也比較難受。比如說,使用synchronized來編寫程式碼,這個程式碼是迫使程式碼順序執行,也就違背了並行執行的初衷,這個在多核cpu上執行所需的成本會更大,多核的cpu的每個處理器核心都有自己獨立快取記憶體,加鎖需要把這些同步快取同步進行,需要在核心間進行緩慢的快取一致性協議通訊。 痛點說完了,接下來我們說下java8中的流是怎麼使用的。

java8中的流

java8中的集合支援一個新的Stream方法,它會返回一個流,到底什麼是流呢? 流:從支援資料處理的源生成的元素序列。讓我們來咬文嚼字的來分別解釋下,

  • 元素序列:和集合一樣,流也提供了一個介面,可以訪問特定元素型別的一組有序值,集合是一種資料結構,它的目的是儲存和訪問,但是流的目的是表達計算。
  • 源:當然就是提供資料的源頭,大部分是集合、陣列,由有序列表的產生的流順序也是一致的。
  • 資料處理操作:流的資料處理功能非常類似於資料庫的操作,就是表達出來你要怎麼處理。

流也有兩個重要的特點:

  • 流水線:Stream 中的很多操作會返回一個流,這樣操作就可以連結起來,形成一個大的流水線。
  • 內部迭代:與集合本身的顯式迭代不同,Stream流的操作都是在背後進行的。 針對開頭的程式碼,如果是java8的方式我們應該怎麼寫呢?
//如果是多核架構的話,可以將stream()換成parallelStream()
List<Long> appleIdList = appleList
                .stream()
            //  .parallelStream()   並行處理
                .filter(apple -> apple.getWeight() < 400)
                .sorted(Comparator.comparing(Apple::getWeight))
                .map(Apple::getId)
                .collect(Collectors.toList());
複製程式碼

經過對比,思考,我們可以發現,後者的程式碼是以宣告的方式寫的,就是陳述了你想要做什麼,而不是一大堆的if、for的去實現。這樣我們再遇到別的需求的時候,不用再去複製程式碼了,你只要再按照這樣的方式去陳述下你想要的就可以了,你可以把幾個簡單操作連結起來,來形成一個流水線,就表達複雜的資料處理。

集合和流的內在區別

那麼集合和流的內在區別是什麼呢?

  1. 比較粗略的說,兩者的主要區別就是在於什麼時間進行計算。 集合是一個記憶體中的資料結構,它儲存包含著所有的值,每一個元素都是存放在記憶體裡的,元素的計算結果才能成為集合的一部分。 而流呢,是概念上的固定的資料結構,元素都是按需計算的。從另一個角度來說,流就是延遲建立的集合,只要在需要的時候才會計算值,得到結果。套用管理學上的話:需求驅動,實時創造。 舉一個例子:用瀏覽器進行搜尋,當你輸入一個關鍵字的時候,Google不會在所有的匹配結果都出來,所有的圖片和都下載好之後才返回給你,而是首先給你10個或是20個,當你點選下一頁的時候再給你接下來的10個,20個。這也就是隻有在需要的時候才會去計算,好像有點類似於懶載入的意思。 有一點流和迭代器比較類似,就是流只能遍歷一次,如果你還想在處理一遍,就只能從原始的資料來源那裡重新生成一個流來遍歷(當然了,這裡說的集合,不是I/O流)。

  2. 另一個關鍵的區別就是兩者的遍歷資料的方式。 Collection介面需要使用者去做迭代,for-each,就是外部迭代,去顯式的取出每個元素,去處理。而Stream使用的是內部迭代它把迭代已經做了,還把得到的流儲存起來。內部迭代的時候,專案可以透明的並行處理,或者是用更好的順序去處理,Stream庫的內部迭代可以自己去選擇一種適合你硬體的資料表示和並行實現。

淺談java8中的流的使用

流的操作:我們再來看下上面的程式碼:filter、sorted、map流水線式的稱為中間操作。collect觸發流水線操作的是終端操作。

淺談java8中的流的使用

流的使用

流的使用包括三件事:

  • 資料來源,集合
  • 中間操作,流水線
  • 終端操作,執行流水線,生成結果

其實流水線的背後理念類似於構建器模式,構建器模式就是用來設定一套配置,也就是這裡的中間操作,接著呼叫built方法,也就是這裡的終端操作。關於設計模式,這裡就不細說了,以後也會專門的說下各個設計模式,各位小夥伴不要捉急。

篩選 filter:篩選出符合條件的 distinct:去除重複 limit:返回一個不超過給定長度的流,截短 skip:跳過給定長度,如果超過總量,返回空

List<Apple> red = appleList.stream().filter(apple -> apple.getColor().equals("red")).distinct().limit(3).collect(Collectors.toList());
複製程式碼

map:對流的元素應用函式,接受一個函式作為引數,並且會把這個函式應用到每一個元素上,並對映到一個新的元素。

List<String> appleNameList = appleList.stream().map(Apple::getName).collect(Collectors.toList());
複製程式碼

有的時候也會有這樣情況,在使用map操作之後,會產生一個集合或者是陣列,而你需要把所有的集合合併為一個集合,這被叫做流的扁平化,接著上程式碼:

List<String> collect = appleList.stream().map(Apple::getName).map(word -> word.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
複製程式碼

flatMap()方法讓你把流中的每一個值都換成另一個流,然後把所有的流連線起來,成為一個流。

查詢和匹配 anyMatch:流中是否有一個元素符合 allMatch:流中元素是否全部符合 noneMatch:流中無元素符合條件 findAny:查詢當前流中的任意元素

Optional<String> any = appleList.stream().map(Apple::getName).map(word -> word.split("")).flatMap(Arrays::stream).distinct().findAny();
複製程式碼

Optional是一個容器,代表一個值存在值或不存在,這樣就避免出現null了。可以用isPresent()方法判斷這個容器是否有值。

歸約:

//原始碼中的reduce
T reduce(T identity, BinaryOperator<T> accumulator);

Integer allSum = numbers.stream().reduce(0, (a, b) -> a + b);
複製程式碼

reduce()方法有兩個引數,總和變數的初始值,上面的0就是,後面的Lambda是加和的操作。你也可以操作相乘,Lambda反覆的結合每個元素,一直到流被規約成一個值。 java8中Integer類現在有了一個靜態的sum方法來求和,你還可以這麼寫:

Integer allInteger = numbers.stream().reduce(0, Integer::sum);
複製程式碼

關於reduce,它還有一個過載的變體,看下面,沒有初始值,返回一個Optional物件,你們知道為什麼會是Optional嗎?因為沒有初始值,加和操作可能不會得到值。

Optional<Integer> result = numbers.stream().reduce(Integer::sum);
Optional<Integer> maxResult = numbers.stream().reduce(Integer::max);
Optional<Integer> minResult = numbers.stream().reduce(Integer::min);
複製程式碼

最後

如果對本文有任何異議或者說有什麼好的建議,可以加我好友(公眾號後臺聯絡作者),也可以在下面留言區留言。希望這篇文章能幫助大家披荊斬棘,乘風破浪。

這樣的分享我會一直持續,你的關注、轉發和好看是對我最大的支援,感謝。

淺談java8中的流的使用

相關文章