1. 程式人生 > >Java 8系列之Stream的基本語法詳解

Java 8系列之Stream的基本語法詳解

Stream系列:

概述

Java 8系列之Lambda表示式之後,我們來了解Stream。Stream 是用函數語言程式設計方式在集合類上進行復雜操作的工具,其集成了Java 8中的眾多新特性之一的聚合操作,開發者可以更容易地使用Lambda表示式,並且更方便地實現對集合的查詢、遍歷、過濾以及常見計算等。

聚合操作

為了學習聚合的使用,在這裡,先定義一個數據類:

public class Student {
    int no;
    String name;
    String sex;
    float height;

    public Student(int no, String name, String sex, float height) {
        this.no = no;
        this.name = name;
        this.sex = sex;
        this.height = height;
    }

    ****
}

Student stuA = new Student(1, "A", "M", 184);
Student stuB = new Student(2, "B", "G", 163);
Student stuC = new Student(3, "C", "M", 175);
Student stuD = new Student(4, "D", "G", 158);
Student stuE = new Student(5, "E", "M", 170);
List<Student> list = new ArrayList<>();
list.add(stuA);
list.add(stuB);
list.add(stuC);
list.add(stuD);
list.add(stuE);

現有一個List list裡面有5個Studeng物件,假如我們想獲取Sex=“G”的Student,並打印出來。如果按照我們原來的處理模式,必然會想到一個for迴圈就搞定了,而在for迴圈其實是一個封裝了迭代的語法塊。在這裡,我們採用Iterator進行迭代:

Iterator<Student> iterator = list.iterator();
while(iterator.hasNext()) {
    Student stu = iterator.next();
    if (stu.getSex().equals("G")) {

        System.out.println(stu.toString());
    }
}

整個迭代過程是這樣的:首先呼叫iterator方法,產生一個新的Iterator物件,進而控制整
個迭代過程,這就是外部迭代 迭代過程通過顯式呼叫Iterator物件的hasNext和next方法完成迭代

而在Java 8中,我們可以採用聚合操作:

list.stream()
    .filter(student -> student.getSex().equals("G"))
    .forEach(student -> System.out.println(student.toString()));

首先,通過stream方法建立Stream,然後再通過filter方法對源資料進行過濾,最後通過foeEach方法進行迭代。在聚合操作中,與Labda表示式一起使用,顯得程式碼更加的簡潔。這裡值得注意的是,我們首先是stream方法的呼叫,其與iterator作用一樣的作用一樣,該方法不是返回一個控制迭代的 Iterator 物件,而是返回內部迭代中的相應介面: Stream,其一系列的操作都是在操作Stream,直到feach時才會操作結果,這種迭代方式稱為內部迭代。

外部迭代和內部迭代(聚合操作)都是對集合的迭代,但是在機制上還是有一定的差異:

  1. 迭代器提供next()、hasNext()等方法,開發者可以自行控制對元素的處理,以及處理方式,但是隻能順序處理;
  2. stream()方法返回的資料集無next()等方法,開發者無法控制對元素的迭代,迭代方式是系統內部實現的,同時系統內的迭代也不一定是順序的,還可以並行,如parallelStream()方法。並行的方式在一些情況下,可以大幅提升處理的效率。

Stream

如何使用Stream?

聚合操作是Java 8針對集合類,使程式設計更為便利的方式,可以與Lambda表示式一起使用,達到更加簡潔的目的。

前面例子中,對聚合操作的使用可以歸結為3個部分:

  1. 建立Stream:通過stream()方法,取得集合物件的資料集。
  2. Intermediate:通過一系列中間(Intermediate)方法,對資料集進行過濾、檢索等資料集的再次處理。如上例中,使用filter()方法來對資料集進行過濾。
  3. Terminal通過最終(terminal)方法完成對資料集中元素的處理。如上例中,使用forEach()完成對過濾後元素的列印。

在一次聚合操作中,可以有多個Intermediate,但是有且只有一個Terminal。也就是說,在對一個Stream可以進行多次轉換操作,並不是每次都對Stream的每個元素執行轉換。並不像for迴圈中,迴圈N次,其時間複雜度就是N。轉換操作是lazy(惰性求值)的,只有在Terminal操作執行時,才會一次性執行。可以這麼認為,Stream 裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在 Terminal 操作的時候迴圈 Stream 對應的集合,然後對每個元素執行所有的函式。

Stream的操作分類

剛才提到的Stream的操作有Intermediate、Terminal和Short-circuiting:

  • Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered

  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator

  • Short-circuiting:
    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

惰性求值和及早求值方法

像filter這樣只描述Stream,最終不產生新集合的方法叫作惰性求值方法;而像count這樣最終會從Stream產生值的方法叫作及早求值方法。

long count = allArtists.stream()
    .filter(artist -> {
        System.out.println(artist.getName());
            return artist.isFrom("London");
        })
    .count();

如何判斷一個操作是惰性求值還是及早求值,其實很簡單,只需要看其返回值即可:如果返回值是Stream,那麼就是惰性求值;如果返回值不是Stream或者是void,那麼就是及早求值。上面的示例中,只是包含兩步:一個惰性求值-filter和一個及早求值-count。

前面,已經說過,在一個Stream操作中,可以有多次惰性求值,但有且僅有一次及早求值。

建立Stream

我們有多種方式生成Stream:

  1. Stream介面的靜態工廠方法(注意:Java8裡介面可以帶靜態方法);

  2. Collection介面和陣列的預設方法(預設方法,也使Java的新特性之一,後續介紹),把一個Collection物件轉換成Stream

  3. 其他

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

靜態工廠方法

of

of方法,其生成的Stream是有限長度的,Stream的長度為其內的元素個數。

- of(T... values):返回含有多個T元素的Stream
- of(T t):返回含有一個T元素的Stream

示例:

Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");

generator

generator方法,返回一個無限長度的Stream,其元素由Supplier介面的提供。在Supplier是一個函式介面,只封裝了一個get()方法,其用來返回任何泛型的值,該結果在不同的時間內,返回的可能相同也可能不相同,沒有特殊的要求。

- generate(Supplier<T> s):返回一個無限長度的Stream
  1. 這種情形通常用於隨機數、常量的 Stream,或者需要前後元素間維持著某種狀態資訊的 Stream。
  2. 把 Supplier 例項傳遞給 Stream.generate() 生成的 Stream,預設是序列(相對 parallel 而言)但無序的(相對 ordered 而言)。

示例:

Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
        return java.lang.Math.random();
    }
});

Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
Stream<Double> generateC = Stream.generate(java.lang.Math::random);

以上三種形式達到的效果是一樣的,只不過是下面的兩個採用了Lambda表示式,簡化了程式碼,其實際效果就是返回一個隨機值。一般無限長度的Stream會與filter、limit等配合使用,否則Stream會無限制的執行下去,後果可想而知,如果你有興趣,不妨試一下。

iterate

iterate方法,其返回的也是一個無限長度的Stream,與generate方法不同的是,其是通過函式f迭代對給指定的元素種子而產生無限連續有序Stream,其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限迴圈。

- iterate(T seed, UnaryOperator<T> f)

示例:

Stream.iterate(1, item -> item + 1)
        .limit(10)
        .forEach(System.out::println); 
        // 列印結果:1,2,3,4,5,6,7,8,9,10

上面示例,種子為1,也可認為該Stream的第一個元素,通過f函式來產生第二個元素。接著,第二個元素,作為產生第三個元素的種子,從而產生了第三個元素,以此類推下去。需要主要的是,該Stream也是無限長度的,應該使用filter、limit等來擷取Stream,否則會一直迴圈下去。

empty

empty方法返回一個空的順序Stream,該Stream裡面不包含元素項。

Collection介面和陣列的預設方法

在Collection介面中,定義了一個預設方法stream(),用來生成一個Stream。

    public interface Collection<E> extends Iterable<E> {


        ***

        default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }

        ***
    }

在Arrays類,封裝了一些列的Stream方法,不僅針對於任何型別的元素採用了泛型,更對於基本型別作了相應的封裝,以便提升Stream的處理效率。

public class Arrays {
    ***
    public static <T> Stream<T> stream(T[] array) {
        return stream(array, 0, array.length);
    }

   public static LongStream stream(long[] array) {
        return stream(array, 0, array.length);
    }
    ***
}

示例:

int ids[] = new int[]{1, 2, 3, 4};
Arrays.stream(ids)
        .forEach(System.out::println);

其他

  • Random.ints()
  • BitSet.stream()
  • Pattern.splitAsStream(java.lang.CharSequence)
  • JarFile.stream()

Intermediate

Intermediate主要是用來對Stream做出相應轉換及限制流,實際上是將源Stream轉換為一個新的Stream,以達到需求效果。

concat

concat方法將兩個Stream連線在一起,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是並行的,則新的Stream也是並行的;若關閉新的Stream時,原兩個輸入的Stream都將執行關閉處理。

示例:

Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5))
       .forEach(integer -> System.out.print(integer + "  "));
// 列印結果
// 1  2  3  4  5  

distinct

distinct方法以達到去除掉原Stream中重複的元素,生成的新Stream中沒有沒有重複的元素。

這裡寫圖片描述

Stream.of(1,2,3,1,2,3)
        .distinct()
        .forEach(System.out::println); // 列印結果:1,2,3

建立了一個Stream(命名為A),其含有重複的1,2,3等六個元素,而實際上列印結果只有“1,2,3”等3個元素。因為A經過distinct去掉了重複的元素,生成了新的Stream(命名為B),而B
中只有“1,2,3”這三個元素,所以也就呈現了剛才所說的列印結果。

filter

filter方法對原Stream按照指定條件過濾,在新建的Stream中,只包含滿足條件的元素,將不滿足條件的元素過濾掉。

這裡寫圖片描述

示例:

Stream.of(1, 2, 3, 4, 5)
        .filter(item -> item > 3)
        .forEach(System.out::println);// 列印結果:4,5

建立了一個含有1,2,3,4,5等5個整型元素的Stream,filter中設定的過濾條件為元素值大於3,否則將其過濾。而實際的結果為4,5。


filter傳入的Lambda表示式必須是Predicate例項,引數可以為任意型別,而其返回值必須是boolean型別。

這裡寫圖片描述

map

map方法將對於Stream中包含的元素使用給定的轉換函式進行轉換操作,新生成的Stream只包含轉換生成的元素。為了提高處理效率,官方已封裝好了,三種變形:mapToDouble,mapToInt,mapToLong。其實很好理解,如果想將原Stream中的資料型別,轉換為double,int或者是long是可以呼叫相對應的方法。

示例:

Stream.of("a", "b", "hello")
        .map(item-> item.toUpperCase())
        .forEach(System.out::println);
        // 列印結果
        // A, B, HELLO

傳給map中Lambda表示式,接受了String型別的引數,返回值也是String型別,在轉換行數中,將字母全部改為大寫

map傳入的Lambda表示式必須是Function例項,引數可以為任意型別,而其返回值也是任性型別,javac會根據實際情景自行推斷。

這裡寫圖片描述

flatMap

flatMap方法與map方法類似,都是將原Stream中的每一個元素通過轉換函式轉換,不同的是,該換轉函式的物件是一個Stream,也不會再建立一個新的Stream,而是將原Stream的元素取代為轉換的Stream。如果轉換函式生產的Stream為null,應由空Stream取代。flatMap有三個對於原始型別的變種方法,分別是:flatMapToInt,flatMapToLong和flatMapToDouble。

示例:

Stream.of(1, 2, 3)
    .flatMap(integer -> Stream.of(integer * 10))
    .forEach(System.out::println);
    // 列印結果
    // 10,20,30

傳給flatMap中的表示式接受了一個Integer型別的引數,通過轉換函式,將原元素乘以10後,生成一個只有該元素的流,該流取代原流中的元素。


flatMap傳入的Lambda表示式必須是Function例項,引數可以為任意型別,而其返回值型別必須是一個Stream。

這裡寫圖片描述

peek

peek方法生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函式(Consumer例項),新Stream每個元素被消費的時候都會執行給定的消費函式,並且消費函式優先執行

這裡寫圖片描述

示例:

Stream.of(1, 2, 3, 4, 5)
        .peek(integer -> System.out.println("accept:" + integer))
        .forEach(System.out::println);
// 列印結果
// accept:1
//  1
//  accept:2
//  2
//  accept:3
//  3
//  accept:4
//  4
//  accept:5
//  5

skip

skip方法將過濾掉原Stream中的前N個元素,返回剩下的元素所組成的新Stream。如果原Stream的元素個數大於N,將返回原Stream的後(原Stream長度-N)個元素所組成的新Stream;如果原Stream的元素個數小於或等於N,將返回一個空Stream。

這裡寫圖片描述

示例:
Stream.of(1, 2, 3,4,5)
.skip(2)
.forEach(System.out::println);
// 列印結果
// 3,4,5

sorted

sorted方法將對原Stream進行排序,返回一個有序列的新Stream。sorterd有兩種變體sorted(),sorted(Comparator),前者將預設使用Object.equals(Object)進行排序,而後者接受一個自定義排序規則函式(Comparator),可按照意願排序。

示例:

Stream.of(5, 4, 3, 2, 1)
        .sorted()
        .forEach(System.out::println);
        // 列印結果
        // 1,2,3,4,5

Stream.of(1, 2, 3, 4, 5)
        .sorted()
        .forEach(System.out::println);
        // 列印結果
        // 5, 4, 3, 2, 1

Terminal

collect

count

count方法將返回Stream中元素的個數。

示例:

long count = Stream.of(1, 2, 3, 4, 5)
        .count();
System.out.println("count:" + count);// 列印結果:count:5

forEach

forEach方法前面已經用了好多次,其用於遍歷Stream中的所元素,避免了使用for迴圈,讓程式碼更簡潔,邏輯更清晰。

示例:

Stream.of(5, 4, 3, 2, 1)
    .sorted()
    .forEach(System.out::println);
    // 列印結果
    // 1,2,3,4,5

forEachOrdered

forEachOrdered方法與forEach類似,都是遍歷Stream中的所有元素,不同的是,如果該Stream預先設定了順序,會按照預先設定的順序執行(Stream是無序的),預設為元素插入的順序。

示例:

Stream.of(5,2,1,4,3)
        .forEachOrdered(integer -> {
            System.out.println("integer:"+integer);
        }); 
        // 列印結果
        // integer:5
        // integer:2
        // integer:1
        // integer:4
        // integer:3

max

max方法根據指定的Comparator,返回一個Optional,該Optional中的value值就是Stream中最大的元素。至於Optional是啥,後續再做介紹吧。

原Stream根據比較器Comparator,進行排序(升序或者是降序),所謂的最大值就是從新進行排序的,max就是取重新排序後的最後一個值,而min取排序後的第一個值。

示例:

Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .max((o1, o2) -> o2 - o1);
System.out.println("max:" + max.get());// 列印結果:max:1

對於原Stream指定了Comparator,實際上是找出該Stream中的最小值,不過,在max方法中找最小值,更能體現出來Comparator的作用吧。max的值不言而喻,就是1了。

min

min方法根據指定的Comparator,返回一個Optional,該Optional中的value值就是Stream中最小的元素。至於Optional是啥,後續再做介紹吧。

示例:

Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .max((o1, o2) -> o1 - o2);
System.out.println("max:" + max.get());// 列印結果:min:5

剛才在max方法中,我們找的是Stream中的最小值,在min中我們找的是Stream中的最大值,不管是最大值還是最小值起決定作用的是Comparator,它決定了元素比較大小的原則。

reduce

Short-circuiting

allMatch

allMatch操作用於判斷Stream中的元素是否全部滿足指定條件。如果全部滿足條件返回true,否則返回false。

示例:

boolean allMatch = Stream.of(1, 2, 3, 4)
    .allMatch(integer -> integer > 0);
System.out.println("allMatch: " + allMatch); // 列印結果:allMatch: true 

anyMatch

anyMatch操作用於判斷Stream中的是否有滿足指定條件的元素。如果最少有一個滿足條件返回true,否則返回false。

示例:

boolean anyMatch = Stream.of(1, 2, 3, 4)
    .anyMatch(integer -> integer > 3);
System.out.println("anyMatch: " + anyMatch); // 列印結果:anyMatch: true 

findAny

findAny操作用於獲取含有Stream中的某個元素的Optional,如果Stream為空,則返回一個空的Optional。由於此操作的行動是不確定的,其會自由的選擇Stream中的任何元素。在並行操作中,在同一個Stram中多次呼叫,可能會不同的結果。在序列呼叫時,Debug了幾次,發現每次都是獲取的第一個元素,個人感覺在序列呼叫時,應該預設的是獲取第一個元素。

示例:

Optional<Integer> any = Stream.of(1, 2, 3, 4).findAny();

findFirst

findFirst操作用於獲取含有Stream中的第一個元素的Optional,如果Stream為空,則返回一個空的Optional。若Stream並未排序,可能返回含有Stream中任意元素的Optional。

示例:

Optional<Integer> any = Stream.of(1, 2, 3, 4).findFirst();

limit

limit方法將擷取原Stream,擷取後Stream的最大長度不能超過指定值N。如果原Stream的元素個數大於N,將擷取原Stream的前N個元素;如果原Stream的元素個數小於或等於N,將擷取原Stream中的所有元素。

這裡寫圖片描述

示例:

Stream.of(1, 2, 3,4,5)
        .limit(2)
        .forEach(System.out::println);
        // 列印結果
        // 1,2

傳入limit的值為2,也就是說被擷取後的Stream的最大長度為2,又由於原Stream中有5個元素,所以將擷取原Stream中的前2個元素,生成一個新的Stream。

noneMatch

noneMatch方法將判斷Stream中的所有元素是否滿足指定的條件,如果所有元素都不滿足條件,返回true;否則,返回false.

示例:

    boolean noneMatch = Stream.of(1, 2, 3, 4, 5)
        .noneMatch(integer -> integer > 10);
    System.out.println("noneMatch:" + noneMatch); // 列印結果 noneMatch:true

    boolean noneMatch_ = Stream.of(1, 2, 3, 4, 5)
            .noneMatch(integer -> integer < 3);
    System.out.println("noneMatch_:" + noneMatch_); // 列印結果 noneMatch_:false

參考資料