強大的 Stream 函數式編程

點擊下方“IT牧場”,選擇“設爲星標”

前言

Java8(又稱爲 Jdk1.8)是 Java 語言開發的一個主要版本。Oracle 公司於 2014 年 3 月 18 日發佈 Java8,它支持函數式編程,新的 JavaScript 引擎,新的日期 API,新的 Stream API 等。Java8 API 添加了一個新的抽象稱爲流 Stream,可以讓你以一種聲明的方式處理數據。Stream API 可以極大提高 Java 程序員的生產力,讓程序員寫出高效率、乾淨、簡潔的代碼。

Java8 新特性

  • Lambda 表達式 − Lambda 允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中)。

  • 方法引用 − 方法引用提供了非常有用的語法,可以直接引用已有 Java 類或對象(實例)的方法或構造器。與 lambda 聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘代碼。

  • 默認方法 − 默認方法就是一個在接口裏面有了一個實現的方法。

  • 新工具 − 新的編譯工具,如:Nashorn 引擎 jjs、類依賴分析器 jdeps。

  • Stream API − 新添加的 Stream API(java.util.stream)把真正的函數式編程風格引入到 Java 中。

  • Date Time API − 加強對日期與時間的處理。

  • Optional 類 − Optional 類已經成爲 Java8 類庫的一部分,用來解決空指針異常。

  • Nashorn JavaScript 引擎 − Java8 提供了一個新的 Nashorn javascript 引擎,它允許我們在 JVM 上運行特定的 javascript 應用。

爲什麼需要 Steam?

Java8 中的 Stream 是對集合(Collection)對象功能的增強,它專注於對集合對象進行各種非常便利、高效的聚合操作,或者大批量數據操作。

StreamAPI 藉助於同樣新出現的 Lambda 表達式,極大的提高編程效率和程序可讀性。同時它提供串行和並行兩種模式進行匯聚操作,併發模式能夠充分利用多核處理器的優勢,使用 fork/join 並行方式來拆分任務和加速處理過程。

流的操作種類

中間操作

當數據源中的數據上了流水線後,這個過程對數據進行的所有操作都稱爲“中間操作”。
中間操作仍然會返回一個流對象,因此多箇中間操作可以串連起來形成一個流水線。

終端操作

當所有的中間操作完成後,若要將數據從流水線上拿下來,則需要執行終端操作。
終端操作將返回一個執行結果,這就是你想要的數據。

java.util.Stream 使用示例

定義一個簡單的學生實體類,用於後面的例子演示:

public class Student {

    /** 學號 */
    private long id;

    /** 姓名 */
    private String name;

    /** 年齡 */
    private int age;

    /** 性別 */
    private int grade;

    /** 專業 */
    private String major;

    /** 學校 */
    private String school;

    // 省略 getter 和 setter
}

// 初始化
List<Student> students = new ArrayList<Student>() {
    {
        add(new Student(20160001"孔明"201"土木工程""武漢大學"));
        add(new Student(20160002"伯約"212"信息安全""武漢大學"));
        add(new Student(20160003"玄德"223"經濟管理""武漢大學"));
        add(new Student(20160004"雲長"212"信息安全""武漢大學"));
        add(new Student(20161001"翼德"212"機械與自動化""華中科技大學"));
        add(new Student(20161002"元直"234"土木工程""華中科技大學"));
        add(new Student(20161003"奉孝"234"計算機科學""華中科技大學"));
        add(new Student(20162001"仲謀"223"土木工程""浙江大學"));
        add(new Student(20162002"魯肅"234"計算機科學""浙江大學"));
        add(new Student(20163001"丁奉"245"土木工程""南京大學"));
    }
};

forEach

Stream 提供了新的方法’forEach’來迭代流中的每個數據。ForEach 接受一個 function 接口類型的變量,用來執行對每一個元素的操作。ForEach 是一箇中止操作,它不返回流,所以我們不能再調用其他的流操作。

以下代碼片段使用 forEach 輸出了 10 個隨機數:

// 隨機生成 10 個 0,100int 類型隨機數
new Random()
        .ints(0100)
        .limit(10)
        .forEach(System.out::println);


從集合 students 中篩選出所有武漢大學的學生:

List<Student> whuStudents = students
        .stream()
        .filter(student -> "武漢大學".equals(student.getSchool()))
        .collect(Collectors.toList());


filter/distinct

filter 方法用於通過設置的條件過濾出元素。Filter 接受一個 predicate 接口類型的變量,並將所有流對象中的元素進行過濾。該操作是一箇中間操作,因此它允許我們在返回結果的基礎上再進行其他的流操作。

以下代碼片段使用 filter 方法過濾出空字符串:

// 獲取空字符串的數量
Arrays.asList("abc""","bc","efg","abcd","""jkl")
        // stream() − 爲集合創建串行流
        .stream()
        .filter(string -> string.isEmpty())
        .count();


distinct 方法用於去除重複元素。

Arrays.asList("a""c""ac""c""a""b")
        .stream()
        .distinct()
        .forEach(System.out::println);

anyMatch/allMatch/noneMatch

匹配操作有多種不同的類型,都是用來判斷某一種規則是否與流對象相互吻合的。所有的匹配操作都是終結操作,只返回一個 boolean 類型的結果。

anyMatch 方法用於判斷集合中是否有任一元素滿足條件。

// 集合中是否有任一元素匹配以'a'開頭
boolean result = Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .anyMatch(s -> s.startsWith("a"));

allMatch 方法用於判斷集合中是否所有元素滿足條件。

// 集合中是否所有元素匹配以'a'開頭
boolean result = Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .allMatch(s -> s.startsWith("a"));

noneMatch 方法用於判斷集合中是否所有元素不滿足條件。

// 集合中是否沒有元素匹配以'a'開頭
boolean result = Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .noneMatch(s -> s.startsWith("a"));

limit/skip

limit 方法用於返回前面 n 個元素。

Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .filter(string -> !string.isEmpty())
        .limit(3)
        .forEach(System.out::println);

skip 方法用於捨棄前 n 個元素。

Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .filter(string -> !string.isEmpty())
        .skip(1)
        .forEach(System.out::println);

sorted

sorted 方法用於對流進行排序。Sorted 是一箇中間操作,能夠返回一個排過序的流對象的視圖。流對象中的元素會默認按照自然順序進行排序,除非你自己指定一個 Comparator 接口來改變排序規則。

以下代碼片段使用 filter 方法過濾掉空字符串,並對其進行自然順序排序:

List<String> strings = Arrays.asList("abc""","bc","efg","abcd","""jkl");
// 一定要記住, sorted 只是創建一個流對象排序的視圖, 而不會改變原來集合中元素的順序。
strings
        .stream()
        .filter(string -> !string.isEmpty())
        .sorted()
        .forEach(System.out::println);
// 輸出原始集合元素, sorted 只是創建排序視圖, 不影響原來集合順序
strings
        .stream()
        .forEach(System.out::println);

// 按照字符串長度進行排序, 若兩個字符串長度相同, 按照字母順序排列
strings
        .stream()
        .filter(string -> !string.isEmpty())
        // 1. 首先根據字符串長度倒序排序; 2. 然後根據字母順序排列
        .sorted(Comparator.comparing(String::length).reversed().thenComparing(String::compareTo))
        .forEach(System.out::println);

以下代碼片段根據 Person 姓名倒序排序,然後利用 Collectors 返回列表新列表:

List<Person> persons = new ArrayList();
// 1. 生成 5 個 Person 對象
for (int i = 1; i <= 5; i++) {
    Person person = new Person(i, "name" + i);
    persons.add(person);
}

// 2. 對 Person 列表進行排序, 排序規則: 根據 Person 姓名倒序排序, 然後利用 Collectors 返回列表新列表;
List<Person> personList = persons
        .stream()
        .sorted(Comparator.comparing(Person::getName).reversed())
        .collect(Collectors.toList());

parallel

流操作可以是順序的,也可以是並行的。順序操作通過單線程執行,而並行操作則通過多線程執行。可使用並行流進行操作來提高運行效率 parallelStream 是流並行處理程序的代替方法。
parallelStream()本質上基於 Java7 的 Fork-Join 框架實現,Fork-Join 是一個處理並行分解的高性能框架,其默認的線程數爲宿主機的內核數。

以下實例我們使用 parallelStream 來輸出空字符串的數量:

// 獲取空字符串的數量[parallelStream 爲 Collection 接口的一個默認方法]
Arrays.asList("abc""","bc","efg","abcd","""jkl")
        // parallelStream() − 爲集合創建並行流
        .parallelStream()
        .filter(string -> string.isEmpty())
        .count();

parallelStream 中 forEachOrdered 與 forEach 區別:

List<String> strings = Arrays.asList("a""b""c");
strings.stream().forEachOrdered(System.out::print);            //abc
strings.stream().forEach(System.out::print);                   //abc
strings.parallelStream().forEachOrdered(System.out::print);    //abc
strings.parallelStream().forEach(System.out::print);           //bca

特別注意:1、千萬不要任意地並行 Stream pipeline,如果源頭是來自 stream.iterate,或者中間使用了中間操作的 limit,那麼並行 pipeline 也不可能提升性能。因此,在 Stream 上通過並行獲取的性能,最好是通過 ArrayList、HashMap、HashSet 和 CouncurrentHashMap 實例,數組,int 範圍和 long 範圍等。這些數據結構的共性是,都可以被精確、輕鬆地分成任意大小的子範圍,使並行線程中的分工變得更加輕鬆。2、Stream pipeline 的終止操作本質上也影響了併發執行的效率。並行的最佳操作是做減法,用一個 Stream 的 reduce 方法,將所有從 pipeline 產生的元素都合併在一起,或者預先打包想 min、max、count 和 sum 這類方法。驟死式操作如 anyMatch、allMatch 和 nonMatch 也都可以並行。由 Stream 的 collect 方法執行的操作,都是可變的減法,不是並行的最好選擇,因此並行集合的成本非常高。3、一般來說,程序中所有的並行 Stream pipeline 都是在一個通用的 fork-join 池中運行的。只要有一個 pipeline 運行異常,都是損害到系統中其它不相關部分的性能。因此,如果對 Stream 進行不恰當的並行操作,可能導致程序運行失敗,或者造成性能災難。

map

map 方法用於映射每個元素到對應的結果。map 是一個對於流對象的中間操作,通過給定的方法,它能夠把流對象中的每一個元素對應到另外一個對象上。
以下代碼片段使用 map 將集合元素轉爲大寫 (每個元素映射到大寫)-> 降序排序 ->迭代輸出:

Arrays.asList("abc""","bc","efg","abcd","""jkl")
        // 通過 stream()方法即可獲取流對象
        .stream()
        // 通過 filter()過濾元素
        .filter(string -> !string.isEmpty())
        // 通過 map()方法用於映射每個元素到對應的結果
        .map(String::toUpperCase)
        // 通過 sorted()方法用於對流進行排序
        .sorted(Comparator.reverseOrder())
        // 通過 forEach()方法迭代流中的每個數據
        .forEach(System.out::println);

篩選出所有專業爲計算機科學的學生姓名:

List<String> names = students
        .stream()
        .filter(student -> "計算機科學".equals(student.getMajor()))
        .map(Student::getName).collect(Collectors.toList());

計算所有專業爲計算機科學學生的年齡之和:

int totalAge = students
        .stream()
        .filter(student -> "計算機科學".equals(student.getMajor()))
        .mapToInt(Student::getAge).sum();

peek

peek 操作接收的是一個 Consumer<T> 函數。顧名思義 peek 操作會按照 Consumer<T> 函數提供的邏輯去消費流中的每一個元素,同時有可能改變元素內部的一些屬性。

按照 Java 團隊的說法,peek() 方法存在的主要目的是用調試,通過 peek() 方法可以看到流中的數據經過每個處理點時的狀態。

Stream.of("one", "two", "three","four").filter(e -e.length() > 3)
                .peek(e -System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -System.out.println("Mapped value: " + e))
                .collect(Collectors.toList());

除去用於調試,peek() 在需要修改元素內部狀態的場景也非常有用,比如我們想將所有 Student 的名字修改爲大寫,當然也可以使用 map() 和 flatMap() 實現,但是相比來說 peek() 更加方便,因爲我們並不想替代流中的數據。

students
        .stream()
        .peek(student -student.setName(student.getName().toUpperCase()))
        .forEach(System.out::println);

那麼 peek() 和 map() 有什麼區別呢?peek 接收一個 Consumer,而 map 接收一個 Function。Consumer 是沒有返回值的,它只是對 Stream 中的元素進行某些操作,但是操作之後的數據並不返回到 Stream 中,所以 Stream 中的元素還是原來的元素。而 Function 是有返回值的,這意味着對於 Stream 的元素的所有操作都會作爲新的結果返回到 Stream 中。

findFirst/findAny

findAny 能夠從流中隨便選一個元素出來,它返回一個 Optional 類型的元素。

Optional<Stringoptional = Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .findAny();

findFirst 能夠從流中選第一個元素出來,它返回一個 Optional 類型的元素。

Optional<Stringoptional = Arrays.asList("abc""","bc","efg","abcd","""jkl")
        .stream()
        .findFirst();

collect

collect 方法是一個終端操作,它接收的參數是將流中的元素累積到彙總結果的各種方式(稱爲收集器)。

Collectors 工具類提供了許多靜態工具方法來爲大多數常用的用戶用例創建收集器,比如將元素裝進一個集合中、將元素分組、根據不同標準對元素進行彙總等。

Collectors.joining()

Collectors.joining()方法以遭遇元素的順序拼接元素。我們可以傳遞可選的拼接字符串、前綴和後綴。

List<String> strings = Arrays.asList("abc""","bc","efg","abcd","""jkl");
// 篩選列表
List<String> filtered = strings
       .stream()
       .filter(string -> !string.isEmpty())
       .collect(Collectors.toList());

// 合併字符串
String mergedString = strings
       .stream()
       .filter(string -> !string.isEmpty())
       .collect(Collectors.joining(","));

Collectors.groupingBy

Collectors.groupingBy 方法根據項目的一個屬性的值對流中的項目作問組,並將屬性值作爲結果 Map 的鍵。

  1. List 裏面的對象元素,以某個屬性來分組。

// 按學校對學生進行分組:
Map<StringList<Student>> groups = students
        .stream()
        .collect(Collectors.groupingBy(Student::getSchool));

// 多級分組, 在按學校分組的基礎之上再按照專業進行分組
Map<StringMap<StringList<Student>>> groups2 = students
        .stream()
        .collect(
                Collectors.groupingBy(Student::getSchool,  // 一級分組,按學校
                        Collectors.groupingBy(Student::getMajor)));  // 二級分組,按專業
  1. 統計 List 集合重複元素出現次數。

List<String> items = Arrays.asList("apple""apple""banana""apple""orange""banana""papaya");

// 方式一
Map<String, Long> result = items
        .stream()
        // Function.identity() 返回一個輸出跟輸入一樣的 Lambda 表達式對象, 等價於形如 t -> t 形式的 Lambda 表達式.
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

// 方式二
Map<String, Long> result2 = items
        .stream()
        // Collectors.counting() 計算流中數量
        .collect(Collectors.groupingBy(String::toString, Collectors.counting()));

//  Output :
//  {papaya=1, orange=1, banana=2, apple=3}

統計每個組的個數:

Map<String, Long> groups = students
        .stream()
        .collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));
  1. 累加求和

// 統計相同姓名, 總年齡大小
Map<String, Integer> sumMap = persons
        .stream()
        // Collectors.summingInt() 返回流中整數屬性求和
        .collect(Collectors.groupingBy(Person::getName, Collectors.summingInt(Person::getAge)));
  1. 轉換

// 按照姓名對學生分佈組,並只保留員工的年齡
Map<StringList<String>> nameMap = persons
        .stream()
        .collect(Collectors.groupingBy(Person::getName,
                Collectors.mapping(Employee::getName,   // 下游收集器
                        Collectors.toList()))); // 更下游的收集器

Collectors.toMap

Collectors.toMap 方法將 List 轉 Map。

// 根據 Person 年齡生成 Map
Map<Integer, Person> personMap = persons
        .stream()
        .collect(Collectors.toMap(Person::getAge, person -> person));

// account -> account 是一個返回本身的 lambda 表達式, 其實還可以使用 Function 接口中的一個默認方法代替, 使整個方法更簡潔優雅.
Map<Integer, Person> personMap = persons
        .stream()
        .collect(Collectors.toMap(Person::getAge, Function.identity()));

當 key 重複時,會拋出異常:java.lang.IllegalStateException: Duplicate key **

// 針對重複 key 的, 覆蓋之前的 value
Map<Integer, Person> personMap = persons
        .stream()
        .collect(Collectors.toMap(Person::getAge, Function.identity()(person, person2) -> person2));

指定具體收集的 map:

Map<Integer, Person> personMap = persons
        .stream()
        .collect(Collectors.toMap(Person::getAge, Function.identity()(person, person2) -> person2LinkedHashMap::new));

當 value 爲 null 時,會拋出異常:java.lang.NullPointerException[Collectors.toMap 底層是基於 Map.merge 方法來實現的,而 merge 中 value 是不能爲 null 的,如果爲 null,就會拋出空指針異常。]

Map<Integer, String> personMap = persons
        .stream()
        .collect(Collectors.toMap(Person::getAge, Person::getName, (person, person2) -> person2));


// 1. 解決方式 1: 用 for 循環的方式亦或是 forEach 的方式
Map<Integer, String> personMap = new HashMap<>();
for (Person person : persons) {
    personMap.put(person.getAge(), person.getName());
}

// 2. 解決方式 2: 使用 stream 的 collect 的重載方法
Map<Integer, String> personMap = persons
        .stream()
        .collect(HashMap::new, (m, v) -> m.put(v.getAge(), v.getName()), HashMap::putAll);
Collectors.collectingAndThen

Collectors.collectingAndThen 方法主要用於轉換函數返回的類型。

List 裏面的對象元素,以某個屬性去除重複元素。

List<Person> unique = persons
        .stream()
        .collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingInt(Person::getAge))), ArrayList::new));

Collectors.partitioningBy

Collectors.partitioningBy 方法主要用於根據對流中每個項目應用謂詞的結果來對項目進行分區。

“年齡小於 18”進行分組後可以看到,不到 18 歲的未成年人是一組,成年人是另外一組。

Map<Boolean, List<Person>> groupBy = persons
        .stream()
        .collect(Collectors.partitioningBy(o -> o.getAge() >= 18));

Collectors 收集器靜態方法:


Collectors 收集器靜態方法

Collectors 收集器靜態方法

數值流的使用

在 Stream 裏元素都是對象,那麼,當我們操作一個數字流的時候就不得不考慮一個問題,拆箱和裝箱。雖然自動拆箱不需要我們處理,但依舊有隱含的成本在裏面。Java8 引入了 3 個原始類型特化流接口來解決這個問題:IntStream、DoubleStream、LongStream,分別將流中的元素特化爲 int、long、double,從而避免了暗含的裝箱成本。

將對象流映射爲數值流

// 將對象流映射爲數值流
IntStream intStream = persons
        .stream()
        .mapToInt(Person::getAge);

默認值 OptinalInt

由於數值流經常會有默認值,比如默認爲 0。數值特化流的終端操作會返回一個 OptinalXXX 對象而不是數值。

// 每種數值流都提供了數值計算函數, 如 maxmin、sum 等
OptionalInt optionalInt = persons
        .stream()
        .mapToInt(Person::getAge)
        .max();

int max = optionalInt.orElse(1);

生成一個數值範圍流

// 創建一個包含兩端的數值流, 比如 1 到 10, 包含 10:
IntStream intStream = IntStream.rangeClosed(110);
// 創建一個不包含結尾的數值流, 比如 1 到 9:
IntStream range = IntStream.range(19);

將數值流轉回對象流

// 將數值流轉回對象流
Stream<Integer> boxed = intStream.boxed();

流的扁平化

案例:對給定單詞列表 [“Hello”,”World”],你想返回列表[“H”,”e”,”l”,”o”,”W”,”r”,”d”]

方法一:錯誤方式

String[] words = new String[]{"Hello""World"};
List<String[]> a = Arrays.stream(words)
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());
a.forEach(System.out::print);

// Output
// [Ljava.lang.String;@12edcd21[Ljava.lang.String;@34c45dca

返回一個包含兩個 String[]的 list,傳遞給 map 方法的 lambda 爲每個單詞生成了一個 String[]。因此,map 返回的流實際上是 Stream<String[]>類型的。

方法二:正確方式

String[] words = new String[]{"Hello""World"};
List<String> a = Arrays.stream(words)
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());
a.forEach(System.out::print);

// Output
// HeloWrd


使用 flatMap 方法的效果是,各個數組並不是分別映射一個流,而是映射成流的內容,所有使用 map(Array::stream)時生成的單個流被合併起來,即扁平化爲一個流。



參考博文

[1]. Java 8 中的 Streams API 詳解
[2]. java8 快速實現 List 轉 map 、分組、過濾等操作


source:https://morning-pro.github.io/archives/8cef11db.html

乾貨分享

最近將個人學習筆記整理成冊,使用PDF分享。關注我,回覆如下代碼,即可獲得百度盤地址,無套路領取!

001:《Java併發與高併發解決方案》學習筆記;002:《深入JVM內核——原理、診斷與優化》學習筆記;003:《Java面試寶典》004:《Docker開源書》005:《Kubernetes開源書》006:《DDD速成(領域驅動設計速成)》007:全部008:加技術羣討論

加個關注不迷路

喜歡就點個"在看"唄^_^

本文分享自微信公衆號 - IT牧場(itmuch_com)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章