這一篇帶你係統學習一下Stream

Java8中最大的兩個亮點,一個是Lambda表達式,另一個就是Stream。新特性的加入,一定是爲了某種需求,那麼Stream是什麼,它能幫助我們做什麼?首先看下面這個例子:

有這樣一份數據,一組考卷List,每個Paper有三個屬性分別是學生名字studentName、課程名稱className和分數score。現在我們需要從中找出語文不及格(分數低於60)的學生名字,並且按分數從高到低排序。在不使用Java8新特性之前,相信大部分人都是像下面這樣寫的:

public static List<String> getFailedPaperStudentNamesByJava7(List<Paper> papers) {
    // 篩選出不及格的考卷
    List<Paper> failedPapers = new ArrayList<>();
    for (Paper paper : papers) {
        if (paper.getClassName().equals("語文") 
            && paper.getScore() < 60) {
            failedPapers.add(paper);
        }
    }
    // 按分數從高到低排序
    Collections.sort(failedPapers, new Comparator<Paper>() {
        @Override
        public int compare(Paper o1, Paper o2) {
            return o2.getScore() - o1.getScore();
        }
    });
    // 記下不及格的學生名字
    List<String> failedPaperStudentNames = new ArrayList<>();
    for (Paper failedPaper : failedPapers) {
        failedPaperStudentNames.add(failedPaper.getStudentName());
    }
    return failedPaperStudentNames;
}

下面是用Java8的Lambda表達式+Stream改寫的版本:

public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) {
    return papers.stream()
        .filter(p -> p.getClassName().equals("語文")
                && p.getScore() < 60)
        .sorted((p1, p2) -> p2.getScore() - p1.getScore())
        .map(Paper::getStudentName)
        .collect(Collectors.toList());
}

可直觀的看出,代碼量少了,只用了一行代碼把所有操作鏈接起來了。我們再細看下,從這方法的名字上去理解,首先通過stream從List獲得Stream,然後可以使用流式操作處理數據,先是filter篩選出語文課且不及格的考卷,接着sorted對分數排序,再是map獲得每個Paper中的學生名字,最後collect把所有的名字收集成一個List。可看出,從語義上的理解也更爲直觀了,在篩選語文課不及格的試卷時,我們不是使用命令式寫法(遍歷,然後判斷,再放到一個新的List裏),而是類似SQL中的where條件,通過聲明式寫法直接給出數據需要符合的條件,便能得到需要的數據。

我們說了很久的“Stream”,到底什麼是“Stream”,筆者從Stream API這個角度談自己的理解:

Stream是Java提供的一個接口,該接口允許以聲明式的寫法處理數據,可以把操作鏈接起來,形成數據處理流水線,還能將數據處理任務並行化

聲明式和鏈接操作,前面的例子已經能看出。那什麼是並行化,例如統計學生名字,我們可以將該任務劃分成多個,交給多個CPU分別計算,最後再彙總結果,這一系列複雜操作,可交給Stream完成,如下:

public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) {
    return papers.parallelStream()
        .filter(p -> p.getClassName().equals("語文")
                && p.getScore() < 60)
        .sorted((p1, p2) -> p2.getScore() - p1.getScore())
        .map(Paper::getStudentName)
        .collect(Collectors.toList());
}

只是將stream()方法改爲parallelStream()方法就能將該任務並行化,是不是十分簡單。

通過以上介紹,想必已對Stream有了初步認識,下面開始系統學習Stream。

1. 流和集合

首先我們還是要弄清楚流和集合在概念上的區別。集合,例如List、Set、Tree等,是一種存儲數據的數據結構。關於數據,是已經存在了的,我們只是通過一種數據結構將數據組織起來,便於某種方式讀取或保持某種結構。流不同於集合的地方在於數據並非在使用前全部獲得,而是在使用過程中按需獲得。例如文件流,我們可以通過readline將文件一行一行的讀取。還有視頻流,我們可以邊看邊下載,不用等將所有數據下載完畢才能觀看。

所以雖然我們都能從集合、流中獲取數據,但數據產生的時間是有區別的,集合的數據是預先產生的,而流則是根據需要實時產生的。兩者的特性也導致用途上的差異,集合側重存儲流側重計算。因此我們常聽到的流式計算的叫法。

下面說下流和集合在使用上的區別。集合,可以隨時取用,但流在創建後只能被使用一次,若重複消費,則會報錯。

List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();
stream.forEach(System.out::print);
stream.forEach(System.out::print);
// ABC
// java.lang.IllegalStateException: stream has already been operated upon or closed

另外,集合在遍歷時,就像我們前面描述的例子,只能通過程序員編寫 for-each 這種顯示的代碼去迭代,這被稱作外部迭代。而流在遍歷時,例如map會對流中的每個元素進行處理,所以我們不需要寫具體的迭代代碼,而是交由Java內部完成,這被稱作內部迭代。內部迭代的好處在於,它是一個黑盒。你需要的是迭代,那Java只要完成你的目標就行了。而至於如何迭代,則交由Java來完成,這就爲優化提供了可能,優化的方向有兩點,一是更優化的順序來處理,二是將操作並行化,例如我們在學生的示例中,只是將stream改成parallelStream(),後續其他的map操作可以根據判斷是並行流從而將任務並行化,而我們不用修改map操作。

下面我們來學習如何使用流。

2. 創建流

在對流進行操作之前,我們首先需要獲得一個Stream對象,創建流有以下幾種方式。

2.1 集合

Collection的默認方法stream(),可以由集合類創建流。

List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();

2.2 值

Stream的靜態方法of(T... values),通過顯示值創建流,可接受任意數量的參數。

Stream<String> stream = Stream.of("A","B","C");

2.3 數組

Arrays的靜態方法stream(T[] array)從數組創建流,接受一個數組參數。

String[] ss = new String[]{"A", "B", "C"};
Stream<String> stream = Arrays.stream(ss);

2.4 文件

NIO中有較多靜態方法創建流,例如Files的靜態方法lines(Path path)從返回指定文件中的各行構成的字符串流。

try (Stream<String> stream = Files.lines(Paths.get("data.txt"))) {
    stream.forEach(System.out::print);
} catch (IOException e) {
}

2.5 函數

Stream的靜態iterategenerate可根據函數計算創建無限流。首先看下iterate,通常用於依次生成一系列值,其聲明如下:

Stream<T> iterate(final T seed, final UnaryOperator<T> f)

seed爲初始值,UnaryOperator<T>Function<T,R>的子類,區別在於規定輸入、輸出都是T類型,下面看個示例:

Stream.iterate(0, n -> n + 1)
    .forEach(System.out::println);

該示例會根據初始值,然後通過函數計算依次得到下一個值,0、1、2......你可以試着運行下,發現根本停不下來,這就是剛剛說到的無限流。我們可以通過limit(n)來對無限流做限制。

Stream.iterate(0, n -> n + 1)
    .limit(10)
    .forEach(System.out::println);

接着是generate,不同於iterate依次根據上次計算的結果生成, 而是通過一個Supplier<T>實例提供新的值,其聲明如下:

Stream<T> generate(Supplier<T> s)

例如,我們生成10個隨機數:

Stream.generate(Math::random)
    .limit(10)
    .forEach(System.out::println);

2.6 數值流

你可能已經注意到上述介紹的Stream<T>使用了泛型,所以可以適用於任意引用類型,而對於原始類型則只能使用其包裝類,例如:

Stream.of(1, 2, 3)
    .forEach(n -> {
        System.out.println(n.getClass()); // Integer
        int x = n * 2; // 需要拆箱
    });

Stream在處理原始類型上會由於裝箱拆箱造成較大的性能損耗,所以Java8提供了三種特殊的流接口IntStreamDoubleStreamLongStream,將流中的元素特化爲intdoublelong

IntStream.of(1, 2, 3)
    .forEach(n -> {
        int x = n * 2; // n爲int
    });

除了使用of創建流,還可以將普通流轉成數值流,mapToIntmapToDoublemapToLong

Stream.of(1, 2, 3)
    .mapToInt(Integer::intValue)
    .forEach(n -> {
        int x = n * 2; // n爲int
    });

數值流也能轉成普通流,boxed裝箱。

IntStream.of(1, 2, 3)
    .boxed()
    .forEach(n -> {
        int x = n * 2; // n爲Integer
    });

3. 流的操作

流創建好了,下面學習對流進行操作。流的操作分爲兩種:

  • 中間操作:返回一個Stream對象,可以將一系列中間操作構成一條流的流水線(類似構造器模式)

  • 終端操作:執行流水線,返回不是流的結果(也可是void)

public static List<String> getFailedPaperStudentNamesByJava8(List<Paper> papers) {
    return papers.parallelStream()
        .filter(p -> p.getClassName().equals("語文")  // Stream<Paper>
                && p.getScore() < 60)
        .sorted((p1, p2) -> p2.getScore() - p1.getScore())  // Stream<Paper>
        .map(Paper::getStudentName)  // Stream<String>
        .collect(Collectors.toList()); // List<String>
}

這裏的filtersortedmap就是中間操作,collect爲終端操作。終端操作用於執行流水線是什麼意思?意思是如果沒有終端操作,將不會執行前面鏈接的中間操作。例如:

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .map(s -> {
        System.out.print(s);
        return s;
    });
// 無輸出

無終端操作的情況下,中間操作map裏的代碼塊將不會執行,不會有輸出。而我們在此基礎上加上一個終端操作forEach,便能觸發流水線的執行。

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .map(s -> {
        System.out.print(s);
        return s;
    })
    .forEach(s -> {});
// ABC

下面來看下常用的流操作。

3.1 篩選

(1)filter

Stream<T> filter(Predicate<? super T> predicate)

過濾

  • 接受一個謂詞Predicate(T -> boolean)

  • 返回一個包含所有符合謂詞的元素的流。

List<String> list = Arrays.asList("AA", "AB", "BC");
list.stream()
    .filter(s -> s.startsWith("A"))
    .forEach(System.out::println);
// AA
// AB

(2)distinct

Stream<T> distinct()

去重

  • 根據流中元素的hashCode和equals方法比較元素

  • 返回一個元素各異的流

List<String> list = Arrays.asList("A", "A", "B");
list.stream()
    .distinct()
    .forEach(System.out::print);
// AB

3.2 切片

(1)limit

Stream<T> limit(long maxSize);

截斷

  • 接受一個長度

  • 返回一個不超過給定長度的流

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .limit(2)
    .forEach(System.out::print);
// AB

(2)skip

Stream<T> skip(long n)

跳過元素

  • 指定跳過前n個元素

  • 如果元素不足n個,返回一個空流

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .skip(2)
    .forEach(System.out::print);
// C

3.3 映射

(1)map

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

對每個元素應用函數

  • 接受一個函數(T -> R)

  • 將每一個元素映射成一個新的元素

List<Paper> papers = Arrays.asList(
    new Paper("小明", "語文", 40),
    new Paper("小紅", "語文", 80),
    new Paper("小藍", "語文", 50)
);
papers.stream()
    .map(Paper::getStudentName)
    .forEach(System.out::println);
// 小明
// 小紅
// 小藍

(2)flatMap

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

流的扁平化,什麼意思,先看下面這個例子:

List<String> list = Arrays.asList("ABC", "DEF", "GHI");
list.stream()
    .map(s -> s.split("")) // Stream<String[]>
    .forEach(System.out::println); 
// [Ljava.lang.String;@2f4d3709
// [Ljava.lang.String;@4e50df2e
// [Ljava.lang.String;@1d81eb93

我們想將字符串“ABC”,“DEF”,“GHI”三個字符中的每個字符組合成一個流然後打印出來,但是上述的寫法,通過split函數拆分了String[]數組,流中的元素也被映射成了數組,例如String[]{"A", "B", "C"},所以forEach打印得到的結果是數組地址。

我們如何才能把數組中的元素組合在一起,得到"A", "B", "C", "D"...的一個流呢。這就需要扁平化的處理。flatMap接受一個函數(T -> Stream),把流中每個元素映射爲一個流,然後再把所有的流組合成一個最終的流。例如這裏的元素是String[],那我們就把數組映射成流Arrays::stream,這樣就能把每個數組裏的元素連接在一起了。

List<String> list = Arrays.asList("ABC", "DEF", "GHI");
list.stream()
    .map(s -> s.split("")) // Stream<String[]>
    .flatMap(Arrays::stream) // Steam<String>
    .forEach(System.out::println);

3.4 匹配

(1)anyMatch

boolean anyMatch(Predicate<? super T> predicate)

至少匹配一個元素

  • 接受一個謂詞(T -> boolean)

  • 如果有一個元素匹配,返回true,否則返回false

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .map(s -> {
        System.out.print(s);
        return s;
    })
    .anyMatch(s -> s.startsWith("B")); // true
// AB

當遇到B時,匹配到了,便會直接返回,不會再迭代後續元素,這是一種短路操作。

(2)allMatch

boolean allMatch<Predicate<? super T> predicate>

匹配所有元素

  • 接受一個謂詞(T -> boolean)

  • 所有所有元素匹配,返回true,否則返回false

  • 當有一個元素不匹配,就會短路返回

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .map(s -> {
        System.out.println(s);
        return s;
    })
    .allMatch(s -> s.startsWith("B")); // false
// A

(3)nonMatch

boolean nonMatch(Predicate<? super T> predicate)

所有元素不匹配

  • 接受一個謂詞(T -> boolean)

  • 所有元素匹配,返回true,否則返回false

  • 當有一個元素匹配,就會短路返回

List<String> list = Arrays.asList("A", "B", "C");
list.stream()
    .map(s -> {
        System.out.println(s);
        return s;
    })
    .noneMatch(s -> s.startsWith("B")); // false
// A

3.5 查找

(1)findAny

Optional<T> findAny()

返回當前流中的任意元素,用Optional封裝元素,迫使顯示檢查元素是否存在。

(2)findFirst

Optional<T> findFirst()

返回當前流中的第一個元素

對比:

兩者都是返回一個元素,如果不關心返回的元素是哪個,優先使用findAny,因爲這樣在並行上的限制更少,可優化的空間更大。

3.6 歸約

reduce

// 有初始值
T reduce(T identity, BinaryOperator<T> accumulator)
// 無初始值
Optional<T> reduce(BinaryOperator<T> accumulator)    

通過接收一個BinaryOperator(T, T) -> T,將兩個元素結合產生一個新值。reduce將一直執行該操作直到最後流中只剩一個元素返回。

int[] nums = new int[]{1, 2, 3, 4, 5};
int sum = IntStream.of(nums).reduce(0, Integer::sum); // 15
int max = IntStream.of(nums).reduce(Integer::max).orElse(-1); // 5
int min = IntStream.of(nums).reduce(Integer::min).orElse(-1); // 1

3.7 數值流的特殊操作

在2.6節中我們說過針對原始類型有特殊的原始類型流,由於都是數值,所以也設計了些針對數值的方法。

int[] nums = new int[]{1, 2, 3, 4, 5};
int sum = IntStream.of(nums).sum(); // 15,等同reduce(0, Integer::sum)
int max = IntStream.of(nums).max().orElse(-1); // 5,等同reduce(Integer::max).orElse(-1)
int min = IntStream.of(nums).min().orElse(-1); // 1,等同reduce(Integer::min).orElse(-1)
double avg = IntStream.of(nums).average().orElse(-1); //3.0

3.8 收集

collect在前面的示例中已經見過了,可以將流中的元素進行彙總。

<R, A> R collect(Collector<? super T, A, R> collector);

接收一個Collector收集器。在Collectors中已經內置了一些常用的收集器。

  • toList():將元素收集成一個List。

  • toSet():將元素收集成一個Set。

  • counting():統計元素數量。

  • maxBy(Comparator<? super T> comparator):獲取元素中的最大值。

  • minBy(Comparator<? super T> comparator):獲取元素中的最小值。

  • summingInt(ToIntFunction<? super T> mapper):將元素映射成一個int值,然後求和,類似的還有double和long。

  • averagingInt(ToIntFunction<? super T> mapper):將元素映射成一個int值,然後求平均。

  • summarizingInt(ToIntFunction<? super T> mapper):將元素映射成一個int值,然後得到一個IntSummaryStatistics對象,包含了統計數、總和、最大值、最小值和平均值。

  • joining():把元素toSting()的結果連接成一個字符串,還有一個重載版本,接收一個分隔符參數。

  • groupingBy(Function<? super T, ? extends K> classifier):接收一個Function,返回一個Map<K, List<T>>。通過Function的返回值作爲Key,然後將具有相同Key的元素,組合成List。

下面看示例:

List<Paper> papers = Arrays.asList(
    new Paper("小明", "語文", 40),
    new Paper("小明", "數學", 80),
    new Paper("小紅", "語文", 80),
    new Paper("小紅", "數學", 80),
    new Paper("小藍", "語文", 50),
    new Paper("小藍", "數學", 60)
);
// 所有語文卷子
List<Paper> chinesePapers = papers.stream()
    .filter(p -> p.getClassName().equals("語文"))
    .collect(toList());
// 所有學科
Set<String> classNames = papers.stream()
    .map(Paper::getClassName)
    .collect(toSet());
// 最高分的卷子,最低分改成minBy就行
Paper maxScorePaper = papers.stream()
    .collect(maxBy((p1, p2) -> p1.getScore() - p2.getScore())).get();
// 總分數
int sumScore = papers.stream()
    .collect(summingInt(Paper::getScore));
// 平均分
double avgScore = papers.stream()
    .collect(averagingInt(Paper::getScore));
// 統計數、總和、最大值、最小值和平均值
IntSummaryStatistics summaryStatistics = papers.stream()
    .collect(summarizingInt(Paper::getScore));
long count = summaryStatistics.getCount();
long sum = summaryStatistics.getSum();
int max = summaryStatistics.getMax();
int min = summaryStatistics.getMin();
double avg = summaryStatistics.getAverage();
// 學生名字連接在一起
String studentNameStr = papers.stream()
    .map(Paper::getStudentName)
    .distinct()
    .collect(joining(","));
// 按學科將卷子分組
Map<String, List<Paper>> groupPapers = papers.stream()
    .collect(groupingBy(Paper::getClassName));

以上介紹的是常用的Collector,我們還可以根據需要自定義Collector,本文就不敘述了。

4. 總結

本文列舉了在日常開發中較爲常用的流操作,但是還有未涉及之處,感興趣的讀者可以直接看Stream的API。本文也沒有講述並行流,雖然用法簡單,但是能否真正提高效率,還是要看具體情況,還缺乏經驗就不敘述了。先掌握基本的流式操作吧。

精彩推薦

一百期Java面試題彙總

SpringBoot內容聚合

IntelliJ IDEA內容聚合

Mybatis內容聚合

歡迎長按下圖關注公衆號後端技術精選

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