1. 程式人生 > >Java Stream API進階篇

Java Stream API進階篇

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

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

最大或最小值這類規約操作很好理解(至少方法語義上是這樣),我們著重介紹reduce()

collect(),這是比較有魔法的地方。

多面手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()最常用的場景就是從一堆值中生成一個值。用這麼複雜的函式去求一個最大或最小值,你是不是覺得設計者有病。其實不然,因為“大”和“小”或者“求和"有時會有不同的語義。

需求:從一組單詞中找出最長的單詞。這裡“大”的含義就是“長”。

// 找出最長的單詞![](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()自有其存在的理由。

Stream.reduce_parameter

需求:求出一組單詞的長度之和。這是個“求和”操作,操作物件輸入型別是String,而結果型別是Integer

// 求單詞長度之和
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()組合來達到上述目的,也是可以的。

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函數語言程式設計的熱情,不過很遺憾,下面的內容更繁瑣。

收集器(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工具類應該能滿足我們的絕大部分需求,手動實現之間請先看看文件。

參考文獻

相關推薦

Java Stream API

上一節介紹了部分Stream常見介面方法,理解起來並不困難,但Stream的用法不止於此,本節我們將仍然以Stream為例,介紹流的規約操作。 規約操作(reduction operation)又被稱作摺疊操作(fold),是通過某個連線動作將所有元素彙總成一個彙總結果的過程。元素求和、求最大值或最小值、求

Java語言程式設計-(七)多執行緒與並行程式設計【上】

1.簡單的多執行緒例子package test; public class hello { public static void main(String args[]){ Runnable printA = new PrintChar('a',100);

Java之十五 ----- JDK1.8的Lambda、Stream和日期的使用詳解(很詳細)

前言 本篇主要講述是Java中JDK1.8的一些新語法特性使用,主要是Lambda、Stream和LocalDate日期的一些使用講解。 Lambda Lambda介紹 Lambda 表示式(lambda expression)是一個匿名函式,Lambda表示式基於數學中的λ演算得名,直接對應於

Java8系列--Java Stream(流的操作)

1 流的操作的特點 1.1 流的操作的核心機制   流的操作區別於傳統的集合操作的一大特點是,在Java 8中,流的操作是通過將外部迭代轉向內部迭代來實現的。   在Java 8 Stream API中,流的操作實際上相當於對資料進行一系列的”篩選”操

java字符串轉算術表達式()

rri math .cn eth 來看 style override 算術 ger 今天我們要將前兩篇的隨筆總結一下,用面向對象的思想封裝一下,使它能夠更容易的擴展。 首先我們要設計一個類,讓他能夠同時表示操作符和操作數 public enum OperationTy

Java面向對象(包裝類,不可變類)

public 不存在 內存空間 test 都是 style system 覆蓋 位置 一. Java 8的包裝類 Java中的8種基本數據類型不支持面向對象的變成機制,也不具備對象的特性:沒有成員變量,方法可以調用。為此,Java為這8 種基本數據類型分別提供

Java設計模式之二 ----- 工廠模式

class computer 社會 進階 輕松 override out 是否 return 前言 在上一篇中我們學習了單例模式,介紹了單例模式創建的幾種方法以及最優的方法。本篇則介紹設計模式中的工廠模式,主要分為簡單工廠模式、工廠方法和抽象工廠模式。 簡單工廠模式 簡單

Java設計模式之四 -----適配器模式和橋接模式

原則 pub 是我 protect 接口 logs 將不 多說 外鏈 前言 在上一篇中我們學習了創建型模式的建造者模式和原型模式。本篇則來學習下結構型模式的適配器模式和橋接模式。 適配器模式 簡介 適配器模式是作為兩個不兼容的接口之間的橋梁。這種類型的設計模式屬於結構型模

Java設計模式之五-----外觀模式和裝飾器模式

和我 logs 適配器模式 del xtra implement () 實例化 網絡遊戲 前言 在上一篇中我們學習了結構型模式的適配器模式和橋接模式。本篇則來學習下結構型模式的外觀模式和裝飾器模式。 外觀模式 簡介 外觀模式隱藏系統的復雜性,並向客戶端提供了一個客戶端可以

Java設計模式之六 ----- 組合模式和過濾器模式

對組 www. 希望 als oid block 個人 定義 lsi 前言 在上一篇中我們學習了結構型模式的外觀模式和裝飾器模式。本篇則來學習下組合模式和過濾器模式。 組合模式 簡介 組合模式是用於把一組相似的對象當作一個單一的對象。組合模式依據樹形結構來組合對象,用來表

Java設計模式之八 ----- 責任鏈模式和命令模式

如果能 clean branch pcm tle 開始 類型 mar www 前言 在上一篇中我們學習了結構型模式的享元模式和代理模式。本篇則來學習下行為型模式的兩個模式, 責任鏈模式(Chain of Responsibility Pattern)和命令模式(Comman

Java設計模式之九----- 解釋器模式和叠代器模式

簡單 目的 java進階 使用 記錄 ace 客戶端 -- pro 前言 在上一篇中我們學習了行為型模式的責任鏈模式(Chain of Responsibility Pattern)和命令模式(Command Pattern)。本篇則來學習下行為型模式的兩個模式, 解釋器模

Java設計模式之十 ---- 訪問者模式和中介者模式

前言 在上一篇中我們學習了結構型模式的直譯器模式(Interpreter Pattern)和迭代器模式(Iterator Pattern)。本篇則來學習下行為型模式的兩個模式,訪問者模式(Visitor Pattern)和中介者模式(Mediator Pattern)。 訪問者模式 簡介 訪問者

Java設計模式之十一 ---- 策略模式和模板方法模式

前言 在上一篇中我們學習了行為型模式的訪問者模式(Visitor Pattern)和中介者模式(Mediator Pattern)。本篇則來學習下行為型模式的兩個模式,策略模式(Strategy Pattern)和模板模式(Mediator Pattern)。 策略模式 簡介 策略模式(Stra

【備戰春招/秋招系列】美團Java面經總結 (附詳解答案)

一 訊息佇列MQ的套路 1.1 介紹一下訊息佇列MQ的應用場景/使用訊息佇列的好處 ①.通過非同步處理提高系統性能 ②.降低系統耦合性 1.2 那麼使用訊息佇列會帶來什麼問題?考慮過這個問題嗎? 1.3 介紹

java面試()解答

題目來自於網路,答案是筆者整理的。僅供參考,歡迎指正 來源: https://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&mid=2247485897&idx=1&sn=25f71098bd5421722db25117

java面試()解答

題目來自於網路,答案是筆者整理的。僅供參考,歡迎指正 來源: https://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&mid=2247485779&idx=1&sn=3b06b9923df7f40f887ead8b

java面試()解答

題目來自於網路,答案是筆者整理的。僅供參考,歡迎指正 來源: https://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&mid=2247485723&idx=1&sn=f5c3bfbfab9fe01e6d4979e4

java面試()解答

題目來自於網路,答案是筆者整理的。僅供參考,歡迎指正 題目來源: https://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&mid=2247485604&idx=1&sn=d624680e941b7cd6e2b3ce

Java設計模式之十二 ---- 備忘錄模式和狀態模式

前言 在上一篇中我們學習了行為型模式的策略模式(Strategy Pattern)和模板模式(Template Pattern)。本篇則來學習下行為型模式的兩個模式,備忘錄模式(Memento Pattern)和狀態模式(Memento Pattern)。 備忘錄模式 簡介 備忘錄模式(Meme