五分鐘學習 Java 8 的流編程

一、概述

  1. Java8中在Collection中增長了一個stream()方法,該方法返回一個Stream類型。咱們就是用該Stream來進行流編程的;
  2. 流與集合不一樣,流是隻有在按需計算的,而集合是已經建立完畢並存在緩存中的;
  3. 流與迭代器同樣都只能被遍歷一次,若是想要再遍歷一遍,則必須從新從數據源獲取數據;
  4. 外部迭代就是指須要用戶去作迭代,內部迭代在庫內完成的,無需用戶實現;
  5. 能夠鏈接起來的流操做稱爲中間操做,關閉流的操做稱爲終端操做(從形式上看,就是用.連起來的操做中,中間的那些叫中間操做,最終的那個操做叫終端操做)。

二、篩選

2.1 過濾

Stream<T> filter(Predicate<? super T> predicate);
複製代碼

filter經過指定一個Predicate類型的行爲參數對流中的元素進行過濾,最終仍是會返回一個流,由於它是中間操做。中間操做返回的結果都是一個流,因此,若是咱們想要獲得一個集合或者其餘的非流類型,就須要使用終端操做來獲取。git

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).collect(Collectors.toList());
// [4, 5, 5, 6, 7, 8, 9]
複製代碼

2.2 去重

Stream<T> distinct();
複製代碼

上面就是去重的方法的定義,它會按照流中的元素的equal()和hashCode()方法進行去重。去重以後將繼續返回一個流,因此它也是中間操做。github

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).distinct().collect(Collectors.toList());
// [4, 5, 6, 7, 8, 9]
複製代碼

2.3 限制

Stream<T> limit(long maxSize);
複製代碼

就像是SQL裏面的limit語句,在流中也有相似的limit()方法。它用於限制返回的結果的數量,將會從流的頭開始取固定數量的元素,也是中間操做,使用完以後仍然會返回一個流。編程

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).limit(3).collect(Collectors.toList());
// [4, 5, 5]
複製代碼

2.4 跳過

Stream<T> skip(long n);
複製代碼

這個方法的定義和limit()幾分類似。它也是中間操做,用於跳過從流的頭開始指定數量的元素,使用完以後仍然會返回一個流。緩存

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).skip(3).collect(Collectors.toList());
// [6, 7, 8, 9]
複製代碼

三、映射

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
複製代碼

還記得Function函數接口的方法嗎?它容許你把輸入的類型轉換成另外一種類型。上面就是它在map()方法中的應用。在流操做中使用了該方法以後,流就會嘗試將當前流中全部的元素轉換成另外一種類型。當你調用終端操做collect()的時候,天然也就獲得了另外一種類型的集合。markdown

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<String> filter = list.stream().map((integer -> String.valueOf(integer) + "-")).collect(Collectors.toList());
// 結果:[1-, 1-, 2-, 3-, 4-, 5-, 5-, 6-, 7-, 8-, 9-]
複製代碼

四、查找

Optional<T> findFirst();
Optional<T> findAny();
複製代碼

在指定的流中查找元素的時候能夠用這兩個方法,它們是Stream接口中的方法,返回的已經再也不是Stream類型了,這能夠說明它們是終端操做。因此,一般也是用來放在終端,繼續操做的話就要使用Optional接口的方法了。app

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findAny();
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findFirst();
複製代碼

上面是使用的兩個示例,這裏返回的結果是Optional類型的。Optional的設計借鑑了Guava中的Optional。使用它的好處是你不須要像之前同樣將返回的結果與null進行判斷,並在結果爲null的時候經過=賦值一個默認值了。使用Optional中的方法,你能夠更優雅地完成相同的操做。下面咱們列出Optional中的一些經常使用的方法:dom

編號 方法 說明
1 isPresent() 判斷值是否存在,存在的話就返回true,不然返回false
2 isPresent(Consumer block) 在值存在的時候執行給定的代碼
3 T get() 若是值存在,那麼返回該值;不然,拋出NoSuchElement異常
4 T orElse(T other) 若是值存在,那麼返回該值;不然,則返回other

五、匹配

boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);
複製代碼

從定義上面來看,上面的三個方法也是終端操做。它們分別用來判斷:流中的數據是否所有匹配指定的條件,流中的數據是否所有不匹配指定的條件,流中的數據是否存在一些匹配指定的條件。下面是一些示例:ide

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
boolean allMatch = list.stream().allMatch(integer -> integer < 10);
boolean anyMatch = list.stream().anyMatch(integer -> integer > 3);
boolean noneMatch = list.stream().noneMatch(integer -> integer > 100);
複製代碼

六、歸約

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
複製代碼

Stream接口中的reduce方法共有三個重載版本,上面咱們給出經常使用的兩個的定義。它們基本是相似的,只是第二個方法參數列表中多了個初始值,而沒有初始值的那個,返回了Optinoal類型;因此,區別不大,咱們只要搞明白它的行爲就能夠了。下面是歸約的例子:函數

List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
String ret = list.stream().reduce("-", (a, b) -> a + b);
複製代碼

它的輸出結果是-abcdef,顯然它的效果就是:假如,$是某種操做,List是某個"數列",那麼歸約的意義就是初始值$n[0]$n[1]$n[2]$...$n[n-1]oop

七、數值流

一樣是由於裝箱的性能緣由,Java8中爲數值類型專門提供了數值流:IntStream DoubleStream和LongStream。Stream接口提供了三個中間方法來完成從任意流映射到數值流的操做:

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
複製代碼

因此你能夠用上面三個方法從任意流中獲取數值流。而後,再利用數值流的方法來完成其餘的操做。上面三個數值流和Stream接口都繼承子BaseStream,因此它們包含的方法仍是有區別的,但整體上來講大同小異。Stream比較具備通常性,上面三個數值流更有針對性,後者也提供了許多便利的方法。若是想要從數值流中獲取對象流,你能夠調用它們的boxed()方法,來獲取裝箱以後的流。

這裏稍說起一下,對於Optional,Java8也爲咱們提供了對應的數值類型:OptionalInt OptionalDouble OptionalLong。

在上面的三種數值流中還有幾個靜態方法用於獲取指定數值範圍的流:

public static LongStream range(long startInclusive, final long endExclusive)
public static LongStream rangeClosed(long startInclusive, final long endInclusive)
複製代碼

上面是用於獲取指定範圍的LongStream的方法,一個對應於數學中的開區間,一個對應於數學中的閉區間的概念。

八、構建流

上面咱們在獲取流的時候,實際上都是從Collection的默認方法stream()中獲取的流,這有些笨拙。實際上,Java8爲咱們提供了一些建立流的方法。這裏,咱們列舉一下這些方法:

public static<T> Builder<T> builder() // 1
public static<T> Stream<T> empty() // 2
public static<T> Stream<T> of(T t) // 3
public static<T> Stream<T> of(T... values) // 4
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) // 5
public static<T> Stream<T> generate(Supplier<T> s) // 6 
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) // 7
複製代碼

上面的方法都是Stream接口中的靜態方法,咱們能夠用這些方法來獲取到流。下面咱們對每一個方法作一些簡要的說明:

  1. 從名稱上就能夠看出這裏使用了構建者模式,你能夠每次調用Builder的add()方法插入一個元素來建立流;
  2. 用來建立一個空的流
  3. 建立一個只包含一個元素的流
  4. 使用不定參數建立一個包含指定元素的流
  5. 弄清楚它的原理關鍵是要搞明白後面的UnaryOperator的含義,這是一個函數式接口,而且繼承自Function,不一樣之處在於它的入參和回參類型相同。這個方法的原理是從某個種子值開始,按照後面的函數的規則進行計算,每次是在以前的值的基礎上執行某個函數的。因此Stream.iterate(2, n -> n * n).limit(3)將返回由2 4 16構成的流。
  6. 這裏的Supplier也是一個函數接口,它只有一個get()方法,無參,只接受指定類型的返回值。因此,這個方法須要你提供一個用於生成數值的函數(或者說規則),好比Math.random()等等。
  7. 這個比較容易理解,就是經過將兩個流合併來獲得一個新的流。

九、收集器

上面咱們已經見識過了流的規約操做,可是那些操做還比較幼稚。Java8的收集器爲咱們提供了更增強大的規約功能。

提及收集器,確定繞不過兩個類Collector和Collectors,它倆有啥關係呢?其實Collector只是一個接口;Collectors是一個類, 其中的靜態內部類CollectorImpl實現了該接口,而且被Collectors用來提供一些功能。Collectors中有許多的靜態方法用於獲取Collector的實例,使用這些實例咱們能夠完成複雜的功能。固然,咱們也能夠經過實現Collector接口來定義本身的收集器。

Stream的collect()方法有3個重載的版本。咱們就是經過其中的一個來使用收集器的,這是它的定義:

<R, A> R collect(Collector<? super T, A, R> collector);
複製代碼

咱們注意一下這個方法的參數和返回類型. 從上面咱們能夠看出傳入的Collector有3個泛型,其中的最後一個泛類型R與返回的類型是一致的. 這很重要——能夠預防你調用了某個方法殊不知道最終返回的是什麼類型。

咱們先來看一些簡單的例子,這裏的stream是由Student對象構成的流:

Optional<Student> student = stream.collect(Collectors.maxBy(comparator))  // 須要傳入一個比較器到maxBy()方法中
long count = stream.collect(Collectors.counting())
複製代碼

上面的兩種方式比較雞肋,由於你可使用count()和max()方法來替代它們。下面咱們再看一些收集器的其餘例子,注意在這些例子中,我並無使用lambda簡化函數式接口,是由於想要你更清楚地看到它的泛類型和方法定義。這可能有助於你理解這些方法的做用機理。

9.1 計算平均值和總數

下面的語句用於計算平均值,相似的還有summingInt()用於計算總數。它們的用法是類似的。

Double d = stream.collect(Collectors.averagingInt(new ToIntFunction<Student>() {
    @Override
    public int applyAsInt(Student value) {
        return value.getGrade();
    }
}));
複製代碼

從上面咱們看出,調用averagingInt()方法的時候須要傳入一個ToIntFunction函數式接口,用於根據指定的類型返回一個整數值。

9.2 鏈接字符串

joining()工廠方法是專門用來鏈接字符串的,它要求流是字符串流,因此在對Student流進行拼接以前,須要先將其映射成字符串流:

String members = stream.map(new Function<Student, String>() {
    @Override
    public String apply(Student student) {
       return student.getName();
    }
}).collect(Collectors.joining(", ")); // 使用','將字符串拼接起來
複製代碼

9.3 廣義的規約彙總

Optional<Student> optional = stream.collect(Collectors.reducing(new BinaryOperator<Student>() {
    @Override
    public Student apply(Student student, Student student2) {
        return student.getGrade() > student2.getGrade() ? student : student2;
    }
}));
複製代碼

上面的就是用來規約的函數。咱們用了reducing工廠方法,並向其中傳入一個BinaryOperator類型。這裏咱們指定最終的返回類型是Student。因此,上面的代碼的效果是獲取成績最大的學生。

9.4 分組

Collectors中的分組仍是比較有意思的。咱們先看groupingBy方法的定義:

Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
複製代碼

groupingBy方法有3個重載的版本,這裏咱們給出其中經常使用的兩個。第一個方法是經過指定規則對流進行分組的,而第二個方法先經過classifier指定的規則對流進行分組,而後用downstream的規則對分組後的流進行後續的操做。注意第二個參數仍然是Collector類型,這說明咱們仍然能夠對分組後的流再次收集,好比再分組、求最大值等等。

Map<Integer, List<Student>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}));
複製代碼

以上是groupingBy()方法的第一個例子。注意這裏咱們是經過將Student經過'班級字段'映射成一個整數來進行分組的。下面是一個二次分組的例子。這裏的用了上面的第二個groupingBy()方法,並在downstream中指定了另外一個分組操做。

Map<Integer, Map<Integer, List<Student>>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}, Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
        return student.getGrade() == 100 ? 1 : student.getGrade() > 90 ? 2 : student.getGrade() > 80 ? 3 : 4;
    }
})));
複製代碼

9.5 分區

與分組相似的還有一個分區的操做,分區只是分組的一種特例。它們的使用方式也基本一致,它的方法簽名與上面的groupingBy方法相似。咱們直接看它的一個使用的方式好了:

Map<Boolean, List<Student>> map = stream.collect(Collectors.partitioningBy(new Predicate<Student>() {
    @Override
    public boolean test(Student student) {
        return student.getGrade() > 90;
    }
}));
複製代碼

這就是分區的使用方式。它經過一個指定的函數式接口,將指定的類型映射到一個布爾類型。因此,它相似與分組,只不過它分組的結果只有兩種,要麼true,要麼false。固然,相似於分組,你也能夠在partitioningBy()方法的第二個參數中再指定一個收集器,這樣就能夠對分區後的流進行後續的操做了。

總結:

以上就是Java8中的流的常見的用法,這裏只是列舉了一些常見的、Java8 API中提供的一些類和方法。重點仍然是搞清楚其中的設計的原理,不要盲目記憶。學習的時候結合JDK源碼進行,看到方法的定義就大體瞭解了它的設計原理。最後,不得不說的是,使用流編程確實很簡潔和優雅。

相關代碼:Github

相關文章
相關標籤/搜索