1. 程式人生 > 其它 >Java 8 Stream 使用總結

Java 8 Stream 使用總結

技術標籤:孵化器javaStream

我一直以為,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 的使用方式,我也嘗試著按自己的方式分了類,但難免有紕漏之處,還請見諒。如果你覺得我的文章還不錯,並對後續文章感興趣的話,或者說我們有什麼能夠交流分享的,可以通過掃描下方二維碼來關注我的公眾號!我與風來