系統學習Stream

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

有這樣一份數據,一組考卷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這個角度談本身的理解:app

Stream是Java提供的一個接口,該接口容許以聲明式的寫法處理數據,能夠把操做連接起來,造成數據處理流水線,還能將數據處理任務並行化。

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

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()方法就能將該任務並行化,是否是十分簡單。ide

經過以上介紹,想必已對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的靜態iterate和generate可根據函數計算建立無限流。首先看下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、一、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提供了三種特殊的流接口IntStream、DoubleStream、LongStream,將流中的元素特化爲int、double和long。

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

除了使用of建立流,還能夠將普通流轉成數值流,mapToInt、mapToDouble和mapToLong。

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>
}

這裏的filter、sorted、map就是中間操做,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。本文也沒有講述並行流,雖然用法簡單,可是可否真正提升效率,仍是要看具體狀況,還缺少經驗就不敘述了。先掌握基本的流式操做吧。

相關文章
相關標籤/搜索