Java8 新特性連載——Stream小結

在前面的篇幅中,小編列舉了幾個Lambda的一些特性和寫法,細心的讀者不知道有沒有發現一個出現頻率非常高的關鍵詞——Stream,在所舉的例子中出現了很多次,這個是什麼意思呢?它有什麼用?能給我們帶來哪些便利?這篇我將Stream的含義和用法已我個人的理解做個深入淺出的剖析。

一、為什麼會有Stream?

Java8新特性中,Stream絕對是其中最為亮眼的亮點,從字面意義來看是流的意思,但它與IO中的InputStream或OutputStream又沒有什麼關係。實際上從我個人理解來看,它的出現與當前大數據處理、並行計算有著千絲萬縷的聯繫,隨著信息的積累和膨脹,需要計算機處理的數據量不斷膨脹,Java8引入Stream可謂是順應技術發展潮流,或者說是順應技術發展的需要應運而生。雖然Java作為一門老牌“重型”語言,在編寫並行計算方面並不在話下,但還是對編寫者要求很高,而且很容易出錯,因此引入Stream將會降低門檻,而且增強程序的穩定性。

二、Stream使用場景

Java8引入Stream可以說是對Java現有容器操作的一種增強,因為我們需要處理的數據基本上都存放於某種類型的容器中,然後我們會對其進行各種操作,例如過濾、聚合、分組、求和、統計等,最後得出結果(結果可能還會轉換為另一個容器存儲)。所以Stream本身並不是一種特殊數據結構,它涉及到的主要是計算,例如:

  • 本月過生日的員工
  • 統計每個銷售小組當月績效
  • 挑選熱賣Top10做首頁推薦
  • 篩選出本週需要做接種的兒童名單

在沒有Java8之前,Java API提供給我們的只有簡單的遍歷(Iterator),因為我們只能這麼寫:

// 查找出所有加菲貓
List allCats= new Arraylist<>();
List carfields = new ArrayList<>();
for(Cat cat: allCats) {
if(t.getType() == Cat.GARFIELD) {
carfields.add(t);
}
}
// 所有加菲貓按照年齡從大到小排序
Collections.sort(carfields, new Comparator(){
public int compare(Cat c1, Cat c2){
return c2.getAge().compareTo(c1.getAge());
}
});
// 將排好序的加菲貓名字取出來
List<string> garfieldCatNames = new ArrayList<>();
for(Cat cat: carfields){
garfieldCatNames.add(cat.getName());
}
/<string>

換成Java8的Lambda,我們可以一行就搞定:

List<string> garfieldCatNames = allCats.Stream().filter(c->c.getType()==Cat.GARFIELD).sorted(Comparator.comparing(Cat::getAge).reversed()).map(Cat::getName).collect(Collector.toList());
/<string>

三、構造Stream

Stream更像是一個高級迭代器,Java8之前的迭代器只能串行執行,而Stream則可以串行,但是Stream迭代只能單向,不可往返,就如流水一般一去不復返。

(1)Stream的構造

-從Collection或數組中構造

Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()

-從BufferedReader構造

java.io.BufferedReader.lines()

-靜態工廠中構造

IntStream.range(2,1000)
LongStream.of()
DoubleStream.rangeClosed()
java.nio.file.Files.walk()

-自定義

// 這是可分割迭代器(Splitable Iterator)
java.util.Spliterator

***Spliterator就是為了並行遍歷元素而設計的一個迭代器,jdk1.8中的集合框架中的數據結構都默認實現了spliterator,關於Spliterator後續我會單獨開一篇詳細講解,這裡先留個懸念,童鞋們可以先記住有這麼個接口叫Spliterator。

-其他方式

Random.ints()
BitSet.stream()
Pattern.splitAsStream(java.lang.CharSequence)
JarFile.stream()

四、流的操作類型

Intermediate:一個流可以後面跟隨零個或多個 intermediate 操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然後返回一個新的流,交給下一個操作使用。這類操作都是懶加載的,就是說,僅僅調用到這類方法,並沒有真正開始流的遍歷。

Terminal:一個流只能有一個 terminal 操作,當這個操作執行後,流就被使用“光”了,無法再被操作。所以這必定是流的最後一個操作。Terminal 操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個副作用(Side Effect)。

Short-Circuiting:短路操作,當輸入的源數據為無限大(或非常巨量的),而輸出是有限的數量時,那麼迭代操作就是死循環(或接近),為了保證最終操作一定有結果,就需要短路操作來幫忙,最為典型的短路操作就是limit,其他還有anyMatch、 allMatch、 noneMatch、 findFirst、 findAny都屬於短路操作。

五、Stream經典用法

(1)映射(map/flatMap)

就是將一個對象映射為另外一個對象,例如:

// 將字符串全部轉換為大寫
List<string> output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList());

// 提取貓的名字
List<string> names=cats.stream.map(Cat::getName).collect(Collectors.toList());
/<string>/<string>

前面篇章裡也有描述,這裡就不在贅述,重點說一下map和flatMap的區別:

簡單說map是處理一對一的關係,即原先容器有幾個對象,最終結果也有幾個;而flatMap是處理一對多的關係,即原先容器內是嵌套容器(即可以理解為多維),flatMap將其扁平化降為一維:舉個例子:

Stream<list>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());
/<integer>/<list>

inputStream為多維輸入流(容器內對象為整型數組),通過flatMap降維後,outputStream變成一維的整型數組。再舉個更常用的例子:

List 
cats = new ArrayList<>();
List<string> adds = cats.stream().flatMap(c -> c.getAddress().stream()).collect(Collectors.toList));
/<string>

這樣我就把貓咪的地址(每條貓咪有多條住址)全部提取出來了。

(2)過濾器(Filter)

這個比較好理解,就是根據某種條件將對象從容器中篩選出來

(3)遍歷(forEach)

這是Lambda風格的遍歷。

cats.forEach(System.out::println);

與普通遍歷不同的是,如果我們需要並行遍歷時可以這麼寫:

cats.parallelStream().forEach(System.out::println);

如果我們按照Java8之前寫並行遍歷的話會複雜得多,我們需要增加處理大量的併發的代碼來保證遍歷的準確性,另外一點需要注意的是一旦並行遍歷時,對象的順序將不再保證。

(4)limit、findFirst

當需要從大量數據中獲得少量特定數據時,需要用到limit或findFirst,這兩個也兼職著短路操作:

// 查找出年紀最大的10只貓
cats.stream().sorted(Comparator.comparing(Cat::getAge).reversed()).map(Cat::getAge).limit(10).collect(Collectors.toList());

// 查找出年紀最大的貓的名字
Optional<string> oldestCat = cats.stream().sorted(Comparator.comparing(Cat::getAge).reversed()).map(Cat::getName).findFirst()
/<string>

(5)reduce

學習過Hadoop的童鞋對此應該不會陌生,Hadoop中經典的術語就是Map-Reduce,這裡Reduce也是相同的含義,可以翻譯為規約,即對於容器內的元素按照某種算子進行兩兩計算。reduce有兩種方法形式:

一是不需要初始值的,例如求和

// 算出所有貓的年齡總和
Optional<integer> total = cats.stream().map(Cat::getAge).reduce(Integer::sum);

// 求最大值(上面看到我們用sorted和findFirst也實現了求最大值)
// 這種求最大值要比sorted和findFirst高效一些
Optional<string> oldestCat = cats.stream().reduce((a, b) -> a.getAge() > b.getAge() ? a : b).map(Cat::getName));
/<string>/<integer>

**注意的是返回是一個Optional對象,因為容器本身有可能為空。

二是有初始值的情況:

// 拼接字符串
String concat = Stream.of("A", "B", "C", "D").reduce("初始值", String::concat);
// 輸出:
初始值ABCD

(6)distinct

與數據庫中的distinct含義相同,即去重的意思,因為經常會用到,所以單獨說一下

// 找出名叫kitty的所有貓咪
List kitty = cats.stream().distinct().collect(Collectors.toList());

需要特別注意的是,如果distinct去重的是對象,那麼該對象一定要實現equals和hashCode方法。

(7)match

match匹配與filter不同,filter過濾後會返回對象,而match只會返回真或者假,match有三種形式:

-allMatch:當stream中所有元素都符合條件時返回true;

-anyMatch:stream中只要有一個約束符合條件就返回true;

-noneMatch:stream中沒有任何元素符合條件返回true;

這些都是短路操作,並不會遍歷所有元素(當然極端情況除外)

來看一組例子:

// 有沒有貓咪寶寶
boolean isThereCatBaby = cats.stream().anyMatch(c->c.getAge() < 2);
// 有沒有叫Tom的貓

boolean isThereAnyTomCat = cats.stream().noneMatch(c->c.getName().equals("Tom"));

(8)groupingBy/partitioningBy

分組操作,先說一下grouping與partitioning的區別:

groupingBy方法有三個實現:

groupingBy(Function super T, ? extends K> classifier);

groupingBy(Function super T, ? extends K> classifier, Collector super T, A, D> downstream);

groupingBy(Function super T, ? extends K> classifier,
Supplier mapFactory,
Collector super T, A, D> downstream)

查看源碼可以知道這三個返回值是一樣的,都是Collector接口,其中分類器會作為KEY,VALUE為Collector,這裡Collector你可以理解為一個子流或者下游,關於這一塊的實現我先不做詳細解釋(因為這個一解釋又需要大量篇幅,後續若有時間我會單開一個篇來討論,反正童鞋們只要先記住用法)。

partitioningBy是分區,它接受參數為Predicate,Predicate是一個條件,因此,分區是按照某種條件進行分組的,我們知道Predicate只有true或者false,因此partitioningBy最終只會分成兩個組,返回值KEY為Boolean,即Map<boolean>>/<boolean>

我們舉幾個例子來說明:

// 按貓咪名稱分組
Map<string>> c = cats.stream().collect(Collectors.groupingBy(Cat::getName));

// 按貓咪名稱分組並得到每個名字貓咪的數量
Map<string> catCounts = cats.stream().collect(Collectors.groupingBy(Cat::getName, Collectors.counting()));

// 按貓咪雌雄分組(Gender字段為字符串)
Map<string>> groupByGender = cats.stream().collect(Collectors.groupingBy(Cat::getGender));

// 我們也可以用分組接口對貓咪雌雄分組(注意返回值的差別)
Map<boolean>> groupByGender = cats.stream().collect(Collectors.partitioningBy(cat -> "公貓".equals(cat.getGender())));
/<boolean>/<string>/<string>/<string>

我們看到不管是groupingBy還是partioningBy,由於他們的結果都是Collector接口,因此這兩個是可以混合使用(實際上只要返回是Collector的都可以混合),例如我們的統計需求是先按名字分組然後分組裡面統計出公貓和母貓各有幾隻:

Map<string>> result = cats.stream().collect(Collectors.groupingBy(Cat::getName,
Collectors.partitioningBy(cat -> "".equals(cat.getType()), Collectors.counting())));
/<string>

五、小結

我們最後對stream做一個簡短的ending:

(1)Stream所有操作都不會對原始容器的要素做任何修改,除非用戶自己對要素做修改;

(2)Stream是單向的不可往返的

(3)Stream是支持並行的

(4)對Stream操作時,一定要時刻注意當前操作的對象是什麼


分享到:


相關文章: