天天看點

Java 8 Stream 使用總結

我一直以為,Stream 我接觸的算晚了,可在工作中漸漸發現,盡管很多同僚手握 Java8,但仍然遵循着傳統的程式設計模式,并未充分利用 Java 8 新的特性。是以,這篇文章将談談 Stream 實戰,并在實戰中引出少部分概念。

文章通過兩個用例,一個是如何從容器對象構造 Stream 的用例,另一個則是如何使用 Stream 的用例,通過這兩個用例,你可以收獲 Stream 的使用姿勢。

容器對象構造 Stream 用例

// Construct Stream
// 1
Integer[] arrays = {1, 2, 3};
Stream<Integer> integerStream1 = Stream.of(arrays);
// 2
Stream<Integer> integerStream2 = Stream.of(1, 2, 3);
// 3
Stream<Integer> integerStream3 = Stream.<Integer>builder()
        .add(1).add(2).add(3).build();
// 4
IntStream intStream = IntStream.range(1, 4);

           

Stream 的構造非常簡單,不過需要注意的是,除了 Stream 外,還有 IntStream, LongStream, DoubleStream 這類基本類型對應的流,這些流中增加了一些求和,求平均值等操作,并做了一些優化。

// Stream constructed by collection class
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 1
Stream<Integer> listStream = list.stream();
// 2
Stream<Integer> parallelStream =list.parallelStream();
           

集合接口中新增了

stream

預設方法,調用該方法即可傳回 Stream 對象。

parallelStream

方法傳回的是支援并行的 Stream 對象。

在前文中,我們也介紹過接口的預設方法,并提到它是為了友善這些新增的方法加入原有設計接口,并保持相容的。

使用 Stream 用例

周遊

list.stream().forEach(System.out::println);
list.stream().forEachOrdered(System.out::println);
list.stream().peek(System.out::println).count();
           

上述代碼是對流中的元素進行周遊。注意,建立的流對象不能重複使用,再次使用需要重新建立。

peek 方法為什麼需要再調用一個 count 操作呢?這是因為 peek 方法是一個中間操作,并不會立馬執行。forEach 和 forEachOrdered 都是終結操作,會立馬執行。是以 peek 方法需要再調用一個終結操作的方法來觸發代碼執行。

peek 方法這樣使用并不推薦,這種使用方式在文檔中被描述為 “副作用”,也就是并未合理地使用方法。

流的方法是否為終結操作可以通過文檔檢視。不過在日常使用中,我們按照方法的正常的調用邏輯來思考即可,比如,使用流對元素進行多種操作,包括後續介紹的過濾等,并不會多次的周遊流,因為多次周遊帶來的性能損耗是不能接受的。

計算

統計元素個數:

// 統計元素個數
println(list.stream().count()); //計數
           

比對元素,傳回 true 或 false

// 流中的所有元素都小于 4 ,則傳回 true
println(list.stream().allMatch(e -> e < 4));
// 流中的任意一個元素等于 1 ,則傳回 true
println(list.stream().anyMatch(e -> e == 1));
// 流中沒有一個元素等于 1,則傳回 true
println(list.stream().noneMatch(e -> e == 1));
           

查找元素

// 随機傳回一個元素,如果沒有的話,則傳回 -1
println(list.stream().findAny().orElse(-1));
// 傳回第一個元素,如果沒有的話,傳回 -1
println(list.stream().findFirst().orElse(-1));
// 傳回最大值,沒有的話,傳回 -1
println(list.stream().max(Comparator
  .comparingInt(Integer::intValue)).orElse(-1));
// 傳回最小值,沒有的話,傳回 -1
println(list.stream().min(Comparator
  .comparingInt(Intger::intValue)).orElse(-1));
           

findAny

方法的行為是不确定的,是以,利用它的随機性擷取這一特性不太可取,它主要是為了最大化實作并行流操作的性能而設計的。

這類方法傳回的都是

Optional

類,将該類用作調用擷取實體對象方法的傳回值時,可以非常有效的避免 NPE 問題,這是一個非常值得學習的編碼習慣。

注意:不建議将任何的 Optional 類型作為字段或參數,optional 設計為:有限的機制讓類庫方法傳回值清晰的表達 “沒有值”。 optional 是不可被序列化的,如果類是可序列化的就會出問題。

也不建議将其用作擷取集合對象的方法傳回,擷取集合對象的方法為了避免 NPE ,建議傳回空集合。

數值計算:

這裡的 Student 類以及 studentList 在緊接着的下文給出,不過有着豐富經驗的你,應該能猜到它們指的是什麼。

//數值計算
// 求和
int ageSum1 = studentList.stream().mapToInt(Student::getAge).sum();
int ageSum2 = studentList.stream()
  .map(Student::getAge).mapToInt(Integer::intValue).sum();
int ageSum3 = studentList.stream()
  .map(Student::getAge).flatMapToInt(IntStream::of).sum();
// 平均值
double averageAge =studentList.stream()
  .mapToInt(Student::getAge).averae().orElse(0.0);
           

sum 以及 average 是在基本類型的流中才有的方法。這裡是将對象流轉換為基本類型的流,即 Stream 轉換為 IntStream。

轉換

普通實體類 Studeng.java:

public static class Student{

    private String name;

    private Integer age;

    public Student(String name, Integer age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
           

多個 Student 對象通過 Stream 組裝為 List:

// 将多個對象組裝為 list
List<Student> studentList = Stream.of(new Student("老大", 20)
        , new Student("老二", 18), new Student("老三", 16))
        .collect(Collectors.toList());
           

過濾并轉換為 List, Set, map:

// 傳回 (studentList 中 年齡小于 18) 的 list
List<Student> studentList1 = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toList());

// 傳回 (studentList 中 年齡小于 18) 的 set
Set<Student> studentSet = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toSet());

// 傳回 (studentList 中 年齡小于 18 ,且 name 到 age) 的 map
Map<String, Integer> nameAgeMap = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toMap(Student::getName, Student::getAge, (u1, u2) -> u2));

// 傳回 (studentList 中 年齡小于 18 ,且 name 到 age) 的有序的 map
Map<String, Integer> nameAgeSortedMap = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toMap(Student::getName,Student::getAge, (u1, u2) -> u2,LinkedHashMap::new));
           

轉換為 Map 的重載方法比較多,這是因為它需要考慮在 key 沖突後,如何存儲值,即 (u1, u2) -> u2。自定義該 Lambda 表達式,可以決定當 key 重複時,如何選擇 value 值。

LinkedHashMap::new

方法引用産生的 Map,将決定最後生成的 Map 的實作類。

不過再多的重載方法,都無法逃脫一個限制,那就是 key 或者 value 不能為 null。可在 HashMap 容器中,兩者是可以為 null 的。是以,為了做到這個,我們需要選擇使用原生的 collect 方法,而不是類庫提供給我們的便捷的 Collectors:

Map<String, Integer> nameAgeMap = studentList.stream()
.collect(HashMap::new, (map, ele) -> map.put(ele.getName(), ele.getAge()), HashMap::putAll);
           

這裡可以得到一個

Map<String, Integer>

對象,可能是和提升的類型推斷有關,後續會有相關文章介紹這個特性。

那麼,如果傳回的 Set 接口 也想使用不同的實作類呢?(

Collectors.toSet()

最終傳回的是

HashSet

)我們可以使用下面的方法:

// 傳回 (studentList 中 年齡小于 18) 的 LinkedHashSet
 Set<Student> studentLinkedHashSet = studentList.stream().filter(student ->
         student.getAge() < 18
 ).collect(LinkedHashSet::new, Set::add, Set::addAll);
           

collect 方法除了接收 Colltors 提供的已經封裝好的對象外,還支援自定義。

LinkedHashSet::new

代表生成的容器對象,

Set::add

代表如何往容器中添加元素,

Set::addAll

代表在并行流中,如何合并兩個容器。前文為了解決生成的 HashMap key 不能為 null 的問題,我們已自定義實作過。

轉換為持有不同元素類型的 List:

// 從 Student 集合中傳回一個去重且有序的 age 集合
List<Integer> ageSorted = studentList.stream()
 .map(Student::getAge).distinct().sorted()
 .collect(Colectors.toList());
           

map 方法接收一個 Lambda 表達式,表達式最終産生的值的類型将更改目前 Stream 的參數化類型。例如,在這裡 studentList.stream() 傳回的是一個 Stream ,但經過 map(Student::getAge) 方法後,産生的是一個 Stream ,是以,最終産生的 List 的參數化類型是 Integer。

實作分頁

// 實作分頁: 第一頁開始,每頁 2 條,這裡傳回第二頁的資料
List<Student> pagingStudentList = studentList.stream()
  .skip(2).limit(2)
  .collect(Collectors.toList());
           

分組

// 相同年齡的 Student,即 age -> Students 的 Map
Map<Integer, List<Student>> ageStudentMap =studentList.stream()
  .collect(Collectors.groupingBy(Sudent::getAge, Collectors.toList()));
           

根據 age 進行分組,傳回的 Map 中 value 為

List<Student>

Collectors.toList()

也可以替換為

Collectors.mapping(Student::getName, Collectors.toList())

// 相同年齡的 Student,即 age -> names 的 Map
Map<Integer, List<String>> ageNamesMap = studentList.stream()
n.collect(Collectors.groupingBy(Student::getAge,
Collectors.mapping(Student::getName, Collectors.toList())));
           

這時,傳回的 value 由

List<Student>

轉換為了

List<String>

寫在最後

其實,在日常中,Stream 使用的多了,也就熟練了。不過文章提到的很多小的點,也還是需要多了解一二,這樣在使用的時候就完全能夠遊刃有餘啦。

這是 Java 8 系列的第二篇文章,這篇注重實戰,介紹了很多 Stream 的使用方式,我也嘗試着按自己的方式分了類,但難免有纰漏之處,還請見諒。如果你覺得我的文章還不錯,并對後續文章感興趣的話,或者說我們有什麼能夠交流分享的,可以通過掃描下方二維碼來關注我的公衆号!

Java 8 Stream 使用總結