Java Stream API進階篇

本文github地址html

上一節介紹了部分Stream常見接口方法,理解起來並不困難,但Stream的用法不止於此,本節咱們將仍然以Stream爲例,介紹流的規約操做。java

規約操做(reduction operation)又被稱做摺疊操做(fold),是經過某個鏈接動做將全部元素彙總成一個彙總結果的過程。元素求和、求最大值或最小值、求出元素總個數、將全部元素轉換成一個列表或集合,都屬於規約操做。Stream類庫有兩個通用的規約操做reduce()collect(),也有一些爲簡化書寫而設計的專用規約操做,好比sum()max()min()count()等。git

最大或最小值這類規約操做很好理解(至少方法語義上是這樣),咱們着重介紹reduce()collect(),這是比較有魔法的地方。github

多面手reduce()

reduce操做能夠實現從一組元素中生成一個值,sum()max()min()count()等都是reduce操做,將他們單獨設爲函數只是由於經常使用。reduce()的方法定義有三種重寫形式:編程

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

雖然函數定義愈來愈長,但語義未曾改變,多的參數只是爲了指明初始值(參數identity),或者是指定並行執行時多個部分結果的合併方式(參數combiner)。reduce()最經常使用的場景就是從一堆值中生成一個值。用這麼複雜的函數去求一個最大或最小值,你是否是以爲設計者有病。其實否則,由於「大」和「小」或者「求和"有時會有不一樣的語義。api

需求:從一組單詞中找出最長的單詞。這裏「大」的含義就是「長」。數組

// 找出最長的單詞![](http://images2015.cnblogs.com/blog/939998/201703/939998-20170314192638495-351834305.png)

Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上述代碼會選出最長的單詞love,其中Optional是(一個)值的容器,使用它能夠避免null值的麻煩。固然可使用Stream.max(Comparator<? super T> comparator)方法來達到同等效果,但reduce()自有其存在的理由。oracle

Stream.reduce_parameter

需求:求出一組單詞的長度之和。這是個「求和」操做,操做對象輸入類型是String,而結果類型是Integerapp

// 求單詞長度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,並行執行時纔會用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代碼標號(2)處將i. 字符串映射成長度,ii. 並和當前累加和相加。這顯然是兩步操做,使用reduce()函數將這兩步合二爲一,更有助於提高性能。若是想要使用map()sum()組合來達到上述目的,也是能夠的。ide

reduce()擅長的是生成一個值,若是想要從Stream生成一個集合或者Map等複雜的對象該怎麼辦呢?終極武器collect()橫空出世!

>>> 終極武器collect() <<<

不誇張的講,若是你發現某個功能在Stream接口中沒找到,十有八九能夠經過collect()方法實現。collect()Stream接口方法中最靈活的一個,學會它纔算真正入門Java函數式編程。先看幾個熱身的小例子:

// 將Stream轉換成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上述代碼分別列舉了如何將Stream轉換成ListSetMap。雖然代碼語義很明確,但是咱們仍然會有幾個疑問:

  1. Function.identity()是幹什麼的?
  2. String::length是什麼意思?
  3. Collectors是個什麼東西?

接口的靜態方法和默認方法

Function是一個接口,那麼Function.identity()是什麼意思呢?這要從兩方面解釋:

  1. Java 8容許在接口中加入具體方法。接口中的具體方法有兩種,default方法和static方法,identity()就是Function接口的一個靜態方法。
  2. Function.identity()返回一個輸出跟輸入同樣的Lambda表達式對象,等價於形如t -> t形式的Lambda表達式。

上面的解釋是否是讓你疑問更多?不要問我爲何接口中能夠有具體方法,也不要告訴我你以爲t -> tidentity()方法更直觀。我會告訴你接口中的default方法是一個無奈之舉,在Java 7及以前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的,由於全部實現了該接口的類都要從新實現。試想在Collection接口中加入一個stream()抽象方法會怎樣?default方法就是用來解決這個尷尬問題的,直接在接口中實現新加入的方法。既然已經引入了default方法,爲什麼再也不加入static方法來避免專門的工具類呢!

方法引用

諸如String::length的語法形式叫作方法引用(method references),這種語法用來替代某些特定形式Lambda表達式。若是Lambda表達式的所有內容就是調用一個已有的方法,那麼能夠用方法引用來替代Lambda表達式。方法引用能夠細分爲四類:

方法引用類別 舉例
引用靜態方法 Integer::sum
引用某個對象的方法 list::add
引用某個類的方法 String::length
引用構造方法 HashMap::new

咱們會在後面的例子中使用方法引用。

收集器

相信前面繁瑣的內容已完全打消了你學習Java函數式編程的熱情,不過很遺憾,下面的內容更繁瑣。

<img src="http://images2015.cnblogs.com/blog/939998/201703/939998-20170314192733276-1662918719.png" width="500px", align="right" alt="Stream.collect_parameter" />

收集器(Collector)是爲Stream.collect()方法量身打造的工具接口(類)。考慮一下將一個Stream轉換成一個容器(或者Map)須要作哪些工做?咱們至少須要兩樣東西:

  1. 目標容器是什麼?是ArrayList仍是HashSet,或者是個TreeMap
  2. 新元素如何添加到容器中?是List.add()仍是Map.put()

若是並行的進行規約,還須要告訴collect() 3. 多個部分結果如何合併成一個。

結合以上分析,collect()方法定義爲<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三個參數依次對應上述三條分析。不過每次調用collect()都要傳入這三個參數太麻煩,收集器Collector就是對這三個參數的簡單封裝,因此collect()的另外一定義爲<R,A> R collect(Collector<? super T,A,R> collector)Collectors工具類可經過靜態方法生成各類經常使用的Collector。舉例來講,若是要將Stream規約成List能夠經過以下兩種方式實現:

// 將Stream規約成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

一般狀況下咱們不須要手動指定collect()的三個參數,而是調用collect(Collector<? super T,A,R> collector)方法,而且參數中的Collector對象大都是直接經過Collectors工具類得到。實際上傳入的收集器的行爲決定了collect()的行爲

使用collect()生成Collection

前面已經提到經過collect()方法將Stream轉換成容器的方法,這裏再彙總一下。將Stream轉換成ListSet是比較常見的操做,因此Collectors工具已經爲咱們提供了對應的收集器,經過以下代碼便可完成:

// 將Stream轉換成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

上述代碼可以知足大部分需求,但因爲返回結果是接口類型,咱們並不知道類庫實際選擇的容器類型是什麼,有時候咱們可能會想要人爲指定容器的實際類型,這個需求可經過Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

// 使用toCollection()指定規約容器的類型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

上述代碼(3)處指定規約結果是ArrayList,而(4)處指定規約結果爲HashSet。一切如你所願。

使用collect()生成Map

前面已經說過Stream背後依賴於某種數據源,數據源能夠是數組、容器等,但不能是Map。反過來從Stream生成Map是能夠的,但咱們要想清楚Mapkeyvalue分別表明什麼,根本緣由是咱們要想清楚要幹什麼。一般在三種狀況下collect()的結果會是Map

  1. 使用Collectors.toMap()生成的收集器,用戶須要指定如何生成Mapkeyvalue
  2. 使用Collectors.partitioningBy()生成的收集器,對元素進行二分區操做時用到。
  3. 使用Collectors.groupingBy()生成的收集器,對元素作group操做時用到。

狀況1:使用toMap()生成的收集器,這種狀況是最直接的,前面例子中已提到,這是和Collectors.toCollection()並列的方法。以下代碼展現將學生列表轉換成由<學生,GPA>組成的Map。很是直觀,無需多言。

// 使用toMap()統計學生GPA
Map<Student, Double> studentToGPA =
     students.stream().collect(Collectors.toMap(Functions.identity(),// 如何生成key
                                     student -> computeGPA(student)));// 如何生成value

狀況2:使用partitioningBy()生成的收集器,這種狀況適用於將Stream中的元素依據某個二值邏輯(知足條件,或不知足)分紅互補相交的兩部分,好比男女性別、成績及格與否等。下列代碼展現將學生分紅成績及格或不及格的兩部分。

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

狀況3:使用groupingBy()生成的收集器,這是比較靈活的一種狀況。跟SQL中的group by語句相似,這裏的groupingBy()也是按照某個屬性對數據進行分組,屬性相同的元素會被對應到Map的同一個key上。下列代碼展現將員工按照部門進行分組:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

以上只是分組的最基本用法,有些時候僅僅分組是不夠的。在SQL中使用group by是爲了協助其餘查詢,好比1. 先將員工按照部門分組,2. 而後統計每一個部門員工的人數。Java類庫設計者也考慮到了這種狀況,加強版的groupingBy()可以知足這種需求。加強版的groupingBy()容許咱們對元素分組以後再執行某種運算,好比求和、計數、平均值、類型轉換等。這種先將元素分組的收集器叫作上游收集器,以後執行其餘運算的收集器叫作下游收集器(downstream Collector)。

// 使用下游收集器統計每一個部門的人數
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.counting()));// 下游收集器

上面代碼的邏輯是否是越看越像SQL?高度非結構化。還有更狠的,下游收集器還能夠包含更下游的收集器,這毫不是爲了炫技而增長的把戲,而是實際場景須要。考慮將員工按照部門分組的場景,若是咱們想獲得每一個員工的名字(字符串),而不是一個個Employee對象,可經過以下方式作到:

// 按照部門對員工分佈組,並只保留員工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

若是看到這裏你尚未對Java函數式編程失去信心,恭喜你,你已經順利成爲Java函數式編程大師了。

使用collect()作字符串join

這個確定是你們喜聞樂見的功能,字符串拼接時使用Collectors.joining()生成的收集器,今後告別for循環。Collectors.joining()方法有三種重寫形式,分別對應三種不一樣的拼接方式。無需多言,代碼過目難忘。

// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

collect()還能夠作更多

除了可使用Collectors工具類已經封裝好的收集器,咱們還能夠自定義收集器,或者直接調用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法,收集任何形式你想要的信息。不過Collectors工具類應該能知足咱們的絕大部分需求,手動實現之間請先看看文檔。

本文github地址

參考文獻

  1. https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#package.description
  2. https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
  3. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html
  4. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
  5. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
相關文章
相關標籤/搜索