stream and lambda(10) - 中間操作之排序sorted與除錯peek

語言: CN / TW / HK

Stream 操作過程中,有可能會對元素進行排序,這點 Stream 支援的也不輸集合。考慮到流內操作只有在終止操作時才會觸發導致不便除錯,Stream 也提供了偷竊利器 peek 來方便我們除錯。

sorted

方法定義

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

sorted 提供了兩個方法,一個無參,一個需要傳入比較器。兩種方法執行過後,都可以將流內的元素排序。

使用舉例

根據文件來看,sorted() 會將流內的元素進行自然排序,同時要求元素必須實現 Comparable 介面。看到這裡,問題就來了,什麼是自然排序?為什麼要實現 Comparable 介面?我們來實驗一下。

public void sortedStringTest() {
    List<String> list = Arrays.asList("i", "am", "sorted");
    System.out.println(list.stream().sorted().collect(Collectors.toList()));
}

程式輸出:

[am, i, sorted]

看來,確實按照字母的順序來排序了。可是,如果元素沒有實現 Comparable 又是什麼情況呢?

public void sortedNotComparableTest() {
    List<StudentNotComparable> students = Arrays.asList(new StudentNotComparable("zhangsan", 15), new StudentNotComparable("lisi", 23));
    students.stream().sorted().forEach(s -> System.out.println(s.getAge()));
}

public class StudentNotComparable {
    private String name;
    private int age;
}

大事不妙,程式拋異常了:

Exception in thread “main” java.lang.ClassCastException: StudentNotComparable cannot be cast to java.lang.Comparable at java.util.Comparators$NaturalOrderComparator.compare(Comparators.java:47) at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) at java.util.TimSort.sort(TimSort.java:220) at java.util.Arrays.sort(Arrays.java:1512)

LowB 的我決定一探原始碼究竟。經過幾步跳轉,找到如下關鍵程式碼:

OfRef(AbstractPipeline<?, T, ?> upstream) {
    ...//此處省略
    Comparator<? super T> comp = (Comparator<? super T>) Comparator.naturalOrder();
    this.comparator = comp;
}

好傢伙,原來,排序過程中需要比較器。那從元素物件身上又是怎麼取到比較器的呢?

enum NaturalOrderComparator implements Comparator<Comparable<Object>> {
    ...//此處省略
    @Override
    public int compare(Comparable<Object> c1, Comparable<Object> c2) {
        return c1.compareTo(c2);
    }
}

看來,猜測的沒錯了, 之所以需要元素物件實現 Comparable ,就是要用 compareTo 方法來排序 ,這就是所謂的自然排序。

好了,明白了,我們再來看一下帶有 Comparable 時執行結果。

public void sortedComparableTest() {
    List<StudentComparable> students = Arrays.asList(new StudentComparable("zhangsan", 15), new StudentComparable("lisi", 23));
    students.stream().sorted().forEach(s -> System.out.println(s.getAge()));
}

public class StudentComparable implements Comparable<StudentComparable> {
    private String name;
    private int age;

    @Override
    public int compareTo(StudentComparable o) {
        return this.age - o.getAge();
    }
}

此時,執行正確,程式依次輸出 15,23 兩個數字。

sorted(Comparator<? super T> comparator)

經過了上面的原理探究,這個帶有比較器方法的原理盲猜也可以猜到了:直接使用傳入的比較器進行排序。來,試驗一下。

public void sortedWithComparatorTest() {
    List<StudentNotComparable> students = Arrays.asList(new StudentNotComparable("zhangsan", 15), new StudentNotComparable("lisi", 23));
    students.stream().sorted((s1, s2) -> s1.getAge() - s2.getAge()).forEach(s -> System.out.println(s.getAge()));
}

果然,如果元素未實現 Comparable 介面,傳入比較器之後,程式就正常運行了,依次輸出 15,23 兩個數字。

你以為至此就結束了嗎?No,此時的我忽然冒出了另一個想法,如果元素實現了 Comparable ,此時又傳入了比較器,排序時以哪個為準? 盲猜一下: 以傳入的比較器為準,畢竟走的是傳了比較器的方法。

public void sortedWithComparatorTest2() {
    List<StudentComparable> students = Arrays.asList(new StudentComparable("zhangsan", 15), new StudentComparable("lisi", 23));
    students.stream().sorted((s1, s2) -> s2.getAge() - s1.getAge()).forEach(s -> System.out.println(s.getAge()));
}

驗證了一下,傳入一個和 Comparable 相反的比較器,程式依次輸出 23,15 兩個數字,我們的盲猜是正確的。

為什麼我能猜這麼準?因為以前我們研究過 TreeMap 啊,原理都是一樣的: Java 集合分析之 Map - 這個 Map 有順序(LinkedHashMap 和 TreeMap)

peek

方法定義

Stream<T> peek(Consumer<? super T> action);

peek 方法可以將流元素取出並進行消費。看起來和 foreach 是一樣,都傳一個 Comsumer ,不同的是, peek 方法然後返回 Stream ,這也就意味著,我們對流進行取值,不影響流內元素繼續流轉,所以就可以用作 debug 了。

使用舉例

public void peekTest() {
    List<String> list = Arrays.asList("i", "am", "sorted");
    List<Integer> totalLength = list.stream().map(String::length).peek(System.out::println).collect(Collectors.toList());
    System.out.println(totalLength);
}

在這個過程中,我們先取出了各個字串的長度並列印,最後,將所有長度轉成一個 List,並最終列印。

1 2 6 [1, 2, 6]

總結

  • sorted 方法可以對流內的元素進行排序
  • 如果不傳比較器,sorted 的元素需實現 Comparable,排序按照 compareTo 方法來排序
  • 如果 sorted 方法傳入了比較器,排序時以比較器為準
  • 偷竊利器 peek 可以用於對流 debug,peek 不影響程式執行中間狀態及最終結果

注:本文配套程式碼可在 github 檢視: stream-and-lambda