就從Java8開(kāi)始吧(三)說(shuō)一說(shuō)Stream

前兩講我們聊了一聊Java8的lambda表達(dá)式,有的同學(xué)一定會(huì)問(wèn),lambda表達(dá)式僅僅是一種語(yǔ)法糖,僅僅起到了美化代碼的作用么?

答案是也不是。說(shuō)是是因?yàn)樗牡拇_確只是一種語(yǔ)法糖,換句話(huà)說(shuō),Java8中使用lambda表達(dá)式能實(shí)現(xiàn)的東西,在Java7及之前的版本中幾乎一定可以實(shí)現(xiàn)。說(shuō)不是是因?yàn)橛辛薼ambda表達(dá)式,API的設(shè)計(jì)得到了更充分的發(fā)揮空間,極大地提升了編程的效率和可讀性,這樣各種逆天的API才能如雨后春筍般出現(xiàn)。

說(shuō)起逆天的API,就不得不提起jdk8中新加入的Stream機(jī)制,它可以說(shuō)是Java API中的魔術(shù)師,將lambda表達(dá)式的價(jià)值發(fā)揮得淋漓盡致。Stream稱(chēng)為流式計(jì)算,它不同于文件讀寫(xiě)流(InputStream、OutputStream)或是xml解析流,它是對(duì)集合(Collection)對(duì)象功能的增強(qiáng),它專(zhuān)注于對(duì)集合對(duì)象進(jìn)行各種非常便利、高效的聚合操作(aggregate operation),或者大批量數(shù)據(jù)操作 (bulk data operation)。

在進(jìn)一步了解Stream之前,我們首先需要了解一下什么是聚合操作。簡(jiǎn)單點(diǎn)說(shuō),聚合操作就是把一堆數(shù)通過(guò)某種統(tǒng)計(jì)計(jì)算變成一個(gè)數(shù)。比如求一組數(shù)據(jù)的個(gè)數(shù)、平均值、最小值、最大值、和、標(biāo)準(zhǔn)差之類(lèi)的操作。放到具體業(yè)務(wù)中,可能是計(jì)算某個(gè)班級(jí)的平均分、某個(gè)商場(chǎng)中最便宜的商品、某個(gè)公司最年長(zhǎng)的員工等等。在Stream出現(xiàn)之前,Java對(duì)于數(shù)據(jù)聚合操作的能力是很弱的,要么要求程序員自己編寫(xiě)大量的代碼進(jìn)行數(shù)據(jù)處理,要么依賴(lài)于關(guān)系型數(shù)據(jù)庫(kù)的聚合函數(shù)進(jìn)行操作。Stream出現(xiàn)之后,Java自帶的API終于也能高效處理數(shù)據(jù)了。

那么究竟什么是流呢?首先我們看一下流的概念。流不是數(shù)據(jù)結(jié)構(gòu),不保存數(shù)據(jù),它是一種特殊的迭代工具,它是關(guān)于算法和計(jì)算的,它可以對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換、映射、過(guò)濾和篩選,并通過(guò)一系列操作獲取到想要的結(jié)果。流是怎么高效處理數(shù)據(jù)的呢?據(jù)說(shuō)一個(gè)成功的魔術(shù)有三個(gè)步驟:以虛代實(shí)、偷天換日、化腐朽為神奇(引自《致命魔術(shù)》)。Stream也是通過(guò)三個(gè)類(lèi)似的步驟完成了對(duì)于數(shù)據(jù)的高效處理。

一、以虛代實(shí)————數(shù)據(jù)源轉(zhuǎn)換成流
這一步是關(guān)于流的創(chuàng)建的。我們拿到的數(shù)據(jù)源通常是一個(gè)可迭代的數(shù)據(jù)結(jié)構(gòu),如集合或數(shù)組。如果數(shù)據(jù)源是集合,我們可以利用集合的stream()方法進(jìn)行流的創(chuàng)建,如:

List<Integer> list = Lists.newArrayList(1, 3, 5, 7, 9);
Stream<Integer> stream = list.stream();

如果數(shù)據(jù)源是數(shù)組,我們可以利用Stream類(lèi)提供的工具方法進(jìn)行流的創(chuàng)建,如:

String[] array = {"1", "3", "5", "7", "9"};
Stream<String> stream = Stream.of(array);

也可以使用Arrays工具類(lèi)中的stream方法進(jìn)行創(chuàng)建,如:

String[] array = {"1", "3", "5", "7", "9"};
Stream<String> stream = Arrays.stream(array);

注意,Stream包含一個(gè)泛型參數(shù),表明流處理的是哪種類(lèi)型的數(shù)據(jù),Stream的泛型參數(shù)應(yīng)該與數(shù)據(jù)源的類(lèi)型一致。

二、偷天換日————數(shù)據(jù)的轉(zhuǎn)換
這一步是從一個(gè)流轉(zhuǎn)換成另一個(gè)流的過(guò)程,由于轉(zhuǎn)換結(jié)果還是流,因此這一步可以多次進(jìn)行,這也是流式計(jì)算理念的核心。
流的數(shù)據(jù)轉(zhuǎn)換操作(也叫中間操作,intermediate)主要包括map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered等。
這里介紹幾種常見(jiàn)的操作,其他操作各位讀者可以自行翻閱API文檔。

  • map
    這里的map不是地圖,有點(diǎn)類(lèi)似于集合框架里的map,是映射的意思,可以將流中的元素按照某種規(guī)則統(tǒng)一映射成另一種元素,例如將字符串列表中所有字符串變?yōu)榇髮?xiě):
List<String> list = Lists.newArrayList("abc", "def", "ghi");
// 初始生成的流
Stream<String> initial = list.stream();
// 轉(zhuǎn)化為所有元素均為大寫(xiě)字母的流
Stream<String> mapped = initial.map(s -> s.toUpperCase());
// 使用方法引用來(lái)表達(dá)
// Stream<String> mapped = initial.map(String::toUpperCase);

這里映射的方案是通過(guò)功能型接口(Function,見(jiàn)上一篇)來(lái)表達(dá)的,可以使用lambda表達(dá)式或者方法引用來(lái)實(shí)現(xiàn)功能型接口。通過(guò)這個(gè)例子我們也可以看出lambda表達(dá)式(以及方法引用)對(duì)于代碼簡(jiǎn)潔度和可讀性的重要提升。如果不使用lambda表達(dá)式,我們就要通過(guò)匿名內(nèi)部類(lèi)這種繁瑣的代碼來(lái)實(shí)現(xiàn)功能性接口了。

  • filter
    filter顧名思義,就是對(duì)流里的元素進(jìn)行篩選。例如篩選所有大于10的數(shù)字:
List<Integer> list = Lists.newArrayList(1, 100, 30, 5, 18, 9, 6);
// 初始生成的流
Stream<Integer> initial = list.stream();
// 只保留大于10的元素,這時(shí)流里的元素包括100, 30, 18
Stream<Integer> filtered = initial.filter(i -> i > 10); 

filter方法中過(guò)濾的方案是通過(guò)斷言型接口(Predicate)來(lái)表達(dá)的,它接收一個(gè)參數(shù)并返回一個(gè)boolean值。如果返回結(jié)果為true,表明元素符合要求,應(yīng)該保留;反之元素被過(guò)濾掉。

  • sorted
    將流中的元素進(jìn)行排序,有一個(gè)無(wú)參方法和一個(gè)接受Comparator類(lèi)型參數(shù)的重載方法。無(wú)參方法按照自然排序(即實(shí)現(xiàn)Comparable接口的類(lèi)的默認(rèn)排序)進(jìn)行排序,如果元素所屬的類(lèi)型沒(méi)有實(shí)現(xiàn)Comparable接口,則會(huì)拋出ClassCastException(類(lèi)型轉(zhuǎn)換異常)。有參數(shù)的重載方法則可以按照Comparator提供的策略進(jìn)行排序,Comparator類(lèi)型同樣可以使用lambda表達(dá)式進(jìn)行實(shí)現(xiàn)。例如:
List<Integer> list = Lists.newArrayList(1, 100, 30, 5, 18, 9, 6);
// 初始生成的流
Stream<Integer> initial = list.stream();
// 無(wú)參方法實(shí)現(xiàn)自然排序
// Stream<Integer> sorted = initial.sorted();
// 有參方法實(shí)現(xiàn)自定義排序
Stream<Integer> sorted = initial.sorted((o1, o2) -> o2 - o1);
  • 并行操作 parallel、 sequential、 unordered
    Stream的一個(gè)強(qiáng)大之處在于支持并行操作,Stream通過(guò)并行流支持并行操作,并行流能夠借助多核處理器并行執(zhí)行代碼,這樣可以顯著提高性能。并行流的API簡(jiǎn)單可靠,在一定程序上規(guī)避了多線程并發(fā)編程的復(fù)雜性。
List<Integer> list = Lists.newArrayList(1, 100, 30, 5, 18, 9, 6);
// 初始生成的流
Stream<Integer> initial = list.stream();
// 將流轉(zhuǎn)換為并行流
Stream<Integer> paralleled = inital.parallel();
// 判斷流是否為并行流
boolean b = paralleled.isParallel();
System.out.println(b);

關(guān)于怎么使用并行流進(jìn)行編程,限于篇幅這里就先不展開(kāi)了,回頭有機(jī)會(huì)我們單獨(dú)開(kāi)辟一個(gè)章節(jié)來(lái)介紹。

三、化腐朽為神奇————數(shù)據(jù)的聚合

作為一個(gè)成功的魔術(shù)師,光有前兩個(gè)步驟是不夠的,最重要的一個(gè)步驟就是“化腐朽為神奇”——把流轉(zhuǎn)換成我們想要的結(jié)果。在流的操作中,這一個(gè)步驟被稱(chēng)為數(shù)據(jù)的聚合,也叫流的中止操作(termination)。常見(jiàn)的流中止操作包括forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator。注意,流做了中止操作后,就不能再進(jìn)行其他操作了,否則會(huì)報(bào)IllegalStateException異常(java.lang.IllegalStateException: stream has already been operated upon or closed)。

  • reduce
    reduce是最著名的流中止操作,它和map并稱(chēng)和map-reduce(因作為谷歌搜索引擎的核心算法而出名)。它的主要作用是對(duì)Stream元素進(jìn)行依次聚合。它提供一個(gè)種子(或者把第一個(gè)元素作為種子),然后將種子和第一個(gè)元素按某種規(guī)則(比如加減乘除)進(jìn)行聚合,得到的結(jié)果再與第二個(gè)元素按照相同規(guī)則進(jìn)行聚合。從某種意義上講,上面提到的mix、max、sum、average等都是特殊的reduce。例如:
List<Integer> list = Lists.newArrayList(1, 100, 30, 5, 18, 9, 6);
// 初始生成的流
Stream<Integer> initial = list.stream();
// 利用reduce進(jìn)行求和操作
Integer sum = initial.reduce(0, (a, b)->(a + b));
  • forEach
    forEach是java8提供的新版本遍歷,與Collection類(lèi)中的forEach用法相同,即對(duì)流中的每一個(gè)元素進(jìn)行某種操作。注意在并行流中,遍歷操作不能夠保證執(zhí)行的順序。
List<Integer> list = Lists.newArrayList(1, 100, 30, 5, 18, 9, 6);
// 自然排序后取前5個(gè)元素
Stream<Integer> stream = list.stream().sorted().limit(5);
// 打印流中某個(gè)元素
stream.forEach(System.out::println);
  • collect
    collect意為收集操作,它和其他聚合操作略有不同,它不是將流中所有元素聚合成一個(gè)值,而是收集為可以查看的結(jié)果。例如將流重新轉(zhuǎn)換為L(zhǎng)ist。collect有兩個(gè)常用的重載方法,一個(gè)是基礎(chǔ)方法:
<R> R collect(Supplier<R> supplier,
            BiConsumer<R, ? super T> accumulator,
            BiConsumer<R, R> combiner);

它需要傳遞三個(gè)參數(shù):一個(gè)是供給者,用于產(chǎn)生最后結(jié)果的容器,如生成一個(gè)新的ArrayList;一個(gè)是累加器,用于將Stream中的元素累加到一起,一個(gè)是組合器,用于將累加后的結(jié)果進(jìn)行組合添加到容器中。例如:

    // 取大于5的元素
    Stream stream = Stream.of(1, 100, 30, 5, 18, 9, 6).filter(p -> p > 5);
    // 供給者用于提供ArrayList,累加器用于將Stream中的item累加到list中,組合器用于將累加的結(jié)果組合到一起
    List result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));

基礎(chǔ)方法用起來(lái)比較麻煩,如果只是想將Stream轉(zhuǎn)換為L(zhǎng)ist的話(huà),可以使用collect的重載方法

<R, A> R collect(Collector<? super T, A, R> collector);

可以使用Collectors工具類(lèi)中的靜態(tài)方法去構(gòu)造collector:

     // 取大于5的元素
    Stream stream = Stream.of(1, 100, 30, 5, 18, 9, 6).filter(p -> p > 5);
    // 使用Collectors的靜態(tài)方法構(gòu)造collector
    List result = stream.collect(Collectors.toList());

其他像min、max、count等方法的使用較為簡(jiǎn)單,這里不再詳細(xì)展開(kāi)了。
總結(jié)一下,Stream具有以下特性(敲黑板):

  • Stream不是數(shù)據(jù)結(jié)構(gòu),也不會(huì)修改源數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)
  • Stream操作的參數(shù)均為函數(shù)式接口(無(wú)參數(shù)的除外)
  • Stream的三個(gè)步驟:生成流、轉(zhuǎn)換流、聚合
  • Stream具有并行的能力,可以取代多線程數(shù)據(jù)處理
  • Stream可以是無(wú)限的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容