1. 程式人生 > >Java 9 揭祕(18. Streams API 更新)

Java 9 揭祕(18. Streams API 更新)

Tips
做一個終身學習的人。

Java 9

在本章中,主要介紹以下內容:

  • Stream介面中添加了更加便利的方法來處理流
  • Collectors類中添加了新的收集器(collectors)

JDK 9中,在Streams API中添加了一些便利的方法,根據型別主要新增在:

  • Stream介面
  • Collectors

Stream介面中的方法定義了新的流操作,而Collectors類中的方法定義了新的收集器。

本章的原始碼位於名為com.jdojo.streams的模組中,其宣告如下所示。

// module-info.java
module com.jdojo.streams {
    exports com.jdojo.streams;
}

一. 新的流操作

在JDK 9中,Stream介面具有以下新方法:

default Stream<T> dropWhile(Predicate<? super T> predicate)
default Stream<T> takeWhile(Predicate<? super T> predicate)
static <T> Stream<T> ofNullable(T t)
static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)

在JDK 8中,Stream介面有兩種方法:skip(long count)limit(long count)skip()方法從頭開始跳過指定的數量元素後返回流的元素。 limit()方法從流的開始返回等於或小於指定數量的元素。第一個方法從一開始就刪除元素,另一個從頭開始刪除剩餘的元素。兩者都基於元素的數量。 dropWhile()takeWhile()相應地分別與skip()limit()方法很像;然而,新方法適用於Predicate而不是元素的數量。

可以將這些方法想象是具有異常的filter()方法。 filter()方法評估所有元素上的predicate,而dropWhile()

takeWhile()方法則從流的起始處對元素進行predicate評估,直到predicate失敗。

對於有序流,dropWhile()方法返回流的元素,從指定predicate為true的起始處丟棄元素。考慮以下有序的整數流:

1, 2, 3, 4, 5, 6, 7

如果在dropWhile()方法中使用一個predicate,該方法對小於5的整數返回true,則該方法將刪除前四個元素並返回其餘部分:

5, 6, 7

對於無序流,dropWhile()方法的行為是非確定性的。 它可以選擇刪除匹配predicate的任何元素子集。 當前的實現從匹配元素開始丟棄匹配元素,直到找到不匹配的元素。

dropWhile()方法有兩種極端情況。 如果第一個元素與predicate不匹配,則該方法返回原始流。 如果所有元素與predicate匹配,則該方法返回一個空流。

takeWhile()方法的工作方式與dropWhile()方法相同,只不過它從流的起始處返回匹配的元素,而丟棄其餘的。

Tips
使用dropWhile()takeWhile()方法處理有序和並行流時要非常小心,因為可能對效能有影響。 在有序的並行流中,元素必須是有序的,在這些方法返回之前從所有執行緒返回。 這些方法處理順序流效果最佳。

如果元素為非空,則Nullable(T t)方法返回包含指定元素的單個元素的流。 如果指定的元素為空,則返回一個空的流。 在流處理中使用flatMap()方法時,此方法非常有用。 考慮以下map ,其值可能為null:

Map<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, null);
map.put(4, "four");

如何在此map中獲取一組排除null的值? 也就是說,如何從這map中獲得一個包含“One”,“Two”和“Four”的集合? 以下是JDK 8中的內容:

// In JDK 8
Set<String> nonNullvalues = map.entrySet()
           .stream()          
           .flatMap(e ->  e.getValue() == null ? Stream.empty() : Stream.of(e.getValue()))
           .collect(toSet());

注意在flatMap()方法中的Lambda表示式內使用三元運算子。 可以使用ofNullable()方法在JDK 9中使此表示式更簡單:

// In JDK 9
Set<String> nonNullvalues = map.entrySet()
           .stream()          
           .flatMap(e ->  Stream.ofNullable(e.getValue()))
           .collect(toSet());

新的iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)方法允許使用初始種子值建立順序(可能是無限)流,並迭代應用指定的下一個方法。 當指定的hasNextpredicate返回false時,迭代停止。 呼叫此方法與使用for迴圈相同:

for (T n = seed; hasNext.test(n); n = next.apply(n)) {
    // n is the element added to the stream
}

以下程式碼片段會生成包含1到10之間的所有整數的流:

Stream.iterate(1, n -> n <= 10, n -> n + 1)

下面包含一個完整的程式,演示如何在Stream介面中使用新的方法。

// StreamTest.java
package com.jdojo.streams;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
public class StreamTest {
     public static void main(String[] args) {
        System.out.println("Using Stream.dropWhile() and Stream.takeWhile():");
        testDropWhileAndTakeWhile();
        System.out.println("\nUsing Stream.ofNullable():");
        testOfNullable();
        System.out.println("\nUsing Stream.iterator():");
        testIterator();
    }
    public static void testDropWhileAndTakeWhile() {
        List<Integer> list = List.of(1, 3, 5, 4, 6, 7, 8, 9);
        System.out.println("Original Stream: " + list);
        List<Integer> list2 = list.stream()
                                  .dropWhile(n -> n % 2 == 1)
                                  .collect(toList());
        System.out.println("After using dropWhile(n -> n % 2 == 1): " + list2);
        List<Integer> list3 = list.stream()
                                  .takeWhile(n -> n % 2 == 1)
                                  .collect(toList());
        System.out.println("After using takeWhile(n -> n % 2 == 1): " + list3);
    }
    public static void testOfNullable() {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "One");
        map.put(2, "Two");
        map.put(3, null);
        map.put(4, "Four");
        Set<String> nonNullValues = map.entrySet()
                                       .stream()          
                                       .flatMap(e ->  Stream.ofNullable(e.getValue()))
                                       .collect(toSet());        
        System.out.println("Map: " + map);
        System.out.println("Non-null Values in Map: " + nonNullValues);
    }
    public static void testIterator() {        
        List<Integer> list = Stream.iterate(1, n -> n <= 10, n -> n + 1)
                                   .collect(toList());
        System.out.println("Integers from 1 to 10: " + list);
    }
}

輸出結果為:

Using Stream.dropWhile() and Stream.takeWhile():
Original Stream: [1, 3, 5, 4, 6, 7, 8, 9]
After using dropWhile(n -> n % 2 == 1): [4, 6, 7, 8, 9]
After using takeWhile(n -> n % 2 == 1): [1, 3, 5]
Using Stream.ofNullable():
Map: {1=One, 2=Two, 3=null, 4=Four}
Non-null Values in Map: [One, Four, Two]
Using Stream.iterator():
Integers from 1 to 10: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

二. 新的收集器

Collectors類有以下兩個返回Collector新的靜態方法:

<T,A,R> Collector<T,?,R> filtering(Predicate<? super T> predicate, Collector<? super T,A,R> downstream)
<T,U,A,R> Collector<T,?,R> flatMapping(Function<? super T,? extends Stream<? extends U>> mapper, Collector<? super U,A,R> downstream)

filtering()方法返回在收集元素之前應用過濾器的收集器。 如果指定的predicate對於元素返回true,則會收集元素; 否則,元素未被收集。

flatMapping()方法返回在收集元素之前應用扁平對映方法的收集器。 指定的扁平對映方法被應用到流的每個元素,並且從扁平對映器(flat mapper)返回的流的元素的累積。

這兩種方法都會返回一個最為有用的收集器,這種收集器用於多級別的遞減,例如downstream處理分組(groupingBy )或分割槽(partitioningBy)。

下面使用Employee類來演示這些方法的使用。

// Employee.java
package com.jdojo.streams;
import java.util.List;
public class Employee {
    private String name;
    private String department;
    private double salary;
    private List<String> spokenLanguages;
    public Employee(String name, String department, double salary,
                    List<String> spokenLanguages) {
        this.name = name;
        this.department = department;
        this.salary = salary;
        this.spokenLanguages = spokenLanguages;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDepartment() {
        return department;
    }
    public void setDepartment(String department) {
        this.department = department;
    }
    public double getSalary() {
        return salary;
    }
    public void setSalary(double salary) {
        this.salary = salary;
    }
    public List<String> getSpokenLanguages() {
        return spokenLanguages;
    }
    public void setSpokenLanguages(List<String> spokenLanguages) {
        this.spokenLanguages = spokenLanguages;
    }
    @Override
    public String toString() {
        return "[" + name + ", " + department + ", " + salary + ", " + spokenLanguages +
               "]";
    }
    public static List<Employee> employees() {
        return List.of(
                new Employee("John", "Sales", 1000.89, List.of("English", "French")),
                new Employee("Wally", "Sales", 900.89, List.of("Spanish", "Wu")),
                new Employee("Ken", "Sales", 1900.00, List.of("English", "French")),
                new Employee("Li", "HR", 1950.89, List.of("Wu", "Lao")),
                new Employee("Manuel", "IT", 2001.99, List.of("English", "German")),
                new Employee("Tony", "IT", 1700.89, List.of("English"))
        );
    }
}

一個員工具有姓名,部門,工資以及他或她所說的語言等屬性。 toString()方法返回一個表示所有這些屬性的字串。 static employees()方法返回員工們的列表,如表下所示。

Name Department Salary Spoken Languages
John Sales 1000.89 English, French
Wally Sales 900.89 Spanish, Wu
Ken Sales 1900.00 English, French
Li HR 1950.89 Wu, Lao
Manuel IT 2001.99 English, German
Tony IT 1700.89 English

可以按照以下方式獲取按部門分組的員工列表:

Map<String,List<Employee>> empGroupedByDept = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment, toList()));                
System.out.println(empGroupedByDept);

輸出結果為:

{Sales=[[John, Sales, 1000.89, [English, French]], [Wally, Sales, 900.89, [Spanish, Wu]], [Ken, Sales, 1900.0, [English, French]]], HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]], [Tony, IT, 1700.89, [English]]]}

此功能自JDK 8以來一直在Streams API中。現在,假設想獲取按部門分組的員工列表,員工的工資必須大於1900才能包含在列表中。 第一個嘗試是使用過濾器,如下所示:

Map<String, List<Employee>> empSalaryGt1900GroupedByDept = Employee.employees()
                .stream()
                .filter(e -> e.getSalary() > 1900)
                .collect(groupingBy(Employee::getDepartment, toList()));                
System.out.println(empSalaryGt1900GroupedByDept);

輸出結果為:

{HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]]]}

從某種意義上說,已經達到了目標。 但是,結果不包括任何員工工資沒有大於1900的部門。這是因為在開始收集結果之前過濾了所有這些部門。 可以使用新的filtering()方法返回的收集器來實現此目的。 這個時候,如果收入1900以上的部門沒有員工,該部門將被列入最終結果,並附上一份空的員工列表。

Map<String, List<Employee>> empGroupedByDeptWithSalaryGt1900 = Employee.employees()
                   .stream()
                   .collect(groupingBy(Employee::getDepartment,
                                       filtering(e -> e.getSalary() > 1900.00, toList())));                
System.out.println(empGroupedByDeptWithSalaryGt1900);

輸出結果為:

{Sales=[], HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]]]}

這一次,結果包含Sales部門,即使沒有此部門有沒有工資在1900以上的員工。

讓我們嘗試一下按部門分組的員工所說的語言屬性的集合。 以下程式碼片段嘗試使用Collectors類的mapping()方法返回的Collector

Map<String,Set<List<String>>> langByDept = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                         mapping(Employee::getSpokenLanguages, toSet())));                
System.out.println(langByDept);

輸出的結果為:

{Sales=[[English, French], [Spanish, Wu]], HR=[[Wu, Lao]], IT=[[English, German], [English]]}

如輸出所示,使用mapping()方法接收到的是Set<List<String>>而不是Set<String>。 在將字串收集到一個集合中之前,需要對List <String>進行扁平化以獲取字串流。 使用新的flatMapping()方法返回的收集器來做這項任務:

Map<String,Set<String>> langByDept2 = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                         flatMapping(e -> e.getSpokenLanguages().stream(), toSet())));                
System.out.println(langByDept2);

輸出結果為:

{Sales=[English, French, Spanish, Wu], HR=[Lao, Wu], IT=[English, German]}

這次得到了正確的結果。 下面包含一個完整的程式,演示如何在收集資料時使用過濾和扁平對映(flat mapping)。

// StreamCollectorsTest.java
package com.jdojo. streams;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.stream.Collectors.filtering;
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
public class StreamCollectorsTest {
    public static void main(String[] args) {
        System.out.println("Testing Collectors.filtering():");
        testFiltering();
        System.out.println("\nTesting Collectors.flatMapping():");
        testFlatMapping();
    }
    public static void testFiltering() {
        Map<String, List<Employee>> empGroupedByDept = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment, toList()));                
        System.out.println("Employees grouped by department:");
        System.out.println(empGroupedByDept);
        // Employees having salary > 1900 grouped by department:
        Map<String, List<Employee>> empSalaryGt1900GroupedByDept = Employee.employees()
                .stream()
                .filter(e -> e.getSalary() > 1900)
                .collect(groupingBy(Employee::getDepartment, toList()));                
        System.out.println("\nEmployees having salary > 1900 grouped by department:");
        System.out.println(empSalaryGt1900GroupedByDept);
        // Group employees by department who have salary > 1900
        Map<String, List<Employee>> empGroupedByDeptWithSalaryGt1900 = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                         filtering(e -> e.getSalary() > 1900.00, toList())));                
        System.out.println("\nEmployees grouped by department having salary > 1900:");
        System.out.println(empGroupedByDeptWithSalaryGt1900);
        // Group employees by department who speak at least 2 languages
        // and 1 of them is English
        Map<String, List<Employee>> empByDeptWith2LangWithEn = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                        filtering(e -> e.getSpokenLanguages().size() >= 2
                                  &&
                                  e.getSpokenLanguages().contains("English"),
                                  toList())));                        
        System.out.println("\nEmployees grouped by department speaking min. 2" +
                 " languages of which one is English:");
        System.out.println(empByDeptWith2LangWithEn);
    }
    public static void testFlatMapping(){
        Map<String,Set<List<String>>> langByDept = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                                mapping(Employee::getSpokenLanguages, toSet())));                
        System.out.println("Languages spoken by department using mapping():");
        System.out.println(langByDept);
        Map<String,Set<String>> langByDept2 = Employee.employees()
                .stream()
                .collect(groupingBy(Employee::getDepartment,
                                flatMapping(e -> e.getSpokenLanguages().stream(), toSet())));  
        System.out.println("\nLanguages spoken by department using flapMapping():");
        System.out.println(langByDept2) ;      
    }        
}

輸出的結果為:

Testing Collectors.filtering():
Employees grouped by department:
{Sales=[[John, Sales, 1000.89, [English, French]], [Wally, Sales, 900.89, [Spanish, Wu]], [Ken, Sales, 1900.0, [English, French]]], HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]], [Tony, IT, 1700.89, [English]]]}
Employees having salary > 1900 grouped by department:
{HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]]]}
Employees grouped by department having salary > 1900:
{Sales=[], HR=[[Li, HR, 1950.89, [Wu, Lao]]], IT=[[Manuel, IT, 2001.99, [English, German]]]}
Employees grouped by department speaking min. 2 languages of which one is English:
{Sales=[[John, Sales, 1000.89, [English, French]], [Ken, Sales, 1900.0, [English, French]]], HR=[], IT=[[Manuel, IT, 2001.99, [English, German]]]}
Testing Collectors.flatMapping():
Languages spoken by department using mapping():
{Sales=[[English, French], [Spanish, Wu]], HR=[[Wu, Lao]], IT=[[English, German], [English]]}
Languages spoken by department using flapMapping():
{Sales=[English, French, Spanish, Wu], HR=[Lao, Wu], IT=[English, German]}

三. 總結

JDK 9向Streams API添加了一些便利的方法,使流處理更容易,並使用收集器編寫複雜的查詢。

Stream介面有四種新方法:dropWhile()),takeWhile()ofNullable()iterate()。對於有序流,dropWhile()方法返回流的元素,從指定predicate為true的起始處丟棄元素。對於無序流,dropWhile()方法的行為是非確定性的。它可以選擇刪除匹配predicate的任何元素子集。當前的實現從匹配元素開始丟棄匹配元素,直到找到不匹配的元素。 takeWhile()方法的工作方式與dropWhile()方法相同,只不過它從流的起始處返回匹配的元素,而丟棄其餘的。如果元素為非空,則Nullable(T t)方法返回包含指定元素的單個元素的流。如果指定的元素為空,則返回一個空的流。新的iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)方法允許使用初始種子值建立順序(可能是無限)流,並迭代應用指定的下一個方法。當指定的hasNextpredicate返回false時,迭代停止。

Collectors類在JDK 9中有兩種新方法:filtering()flatMapping()filtering()方法返回在收集元素之前應用過濾器的收集器。如果指定的predicate對於元素返回true,則會收集元素;否則,元素未被收集。 flatMapping()方法返回在收集元素之前應用扁平對映方法的收集器。指定的扁平對映方法被應用到流的每個元素,並且從扁平對映器返回的流的元素的累積。