stream and lambda(7) - stream 的建立

語言: CN / TW / HK

Stream 的建立, Stream 介面本身提供了一些方法來完成這個操作。而除此之外,JDK 本身也提供了其他類來完成這個操作。考慮到 Stream 的集合息息相關,合理猜測 ArraysCollectionMap 可能會提供建立 stream 的方法。但 Stream<T> 只有單泛型,而 Map<K,V> 是鍵值對,目測 Map 中不含有 Stream 相關的方法。來,大家可以看原始碼驗證一下。

通過檢視 JDK 原始碼發現, ArraysCollection 均提供 stream 相關的方法,而 Map 並未提供對應的方法。

Arrays

從方法來看,Arrays 提供了多個過載方法,可以將 intlongdouble 以及例項物件陣列轉為 Stream ,並且提供了擷取陣列區間的功能。如以下用例,就是將陣列轉成 stream。

public void arraysCreateTest() {
    String[] strings = {"I", "am", "arrays", "demo"};
    int[] nums = {1, 0, 0, 8, 6};
    Arrays.stream(strings).forEach(System.out::println);
    Arrays.stream(nums, 2, 5).forEach(System.out::println);
}

Collection

Collection 提供的 stream()parallelStream() 方法已經在介面內實現,其子類,如 ListSet 可以直接使用。

public void collectionCreateTest() {
    List<Integer> integers = Arrays.asList(1, 2, 9, 11, 5, 7, 3);
    integers.stream().sorted().forEach(System.out::println);
    integers.parallelStream().sorted().forEachOrdered(System.out::println);
}

Stream

empty

empty() 方法,返回的是一個空的 stream。既然沒有任何元素,那這個 stream 的意義又在哪裡?

不報 NPE 異常,還不夠你臭屁的?要知道, java.util.Collections 還單獨提供了 emptySet()emptyList() 方法呢。

來試一試,下面的程式碼能不能正常執行。

public void emptyTest(){
    Stream<Integer> empty = Stream.empty();
    System.out.println(empty.findFirst().orElse(-1));
        
    empty = Stream.empty();
    empty.forEach(System.out::println);
}

generate

generate 是個可以產生無盡流的方法。首先,我們先看看下面的程式碼的執行結果。

public void endlessGenerateTest() {
    Stream<Integer> generate = Stream.generate(() -> new SecureRandom().nextInt());
    AtomicLong al = new AtomicLong(0);
    generate.forEach(integer -> System.out.println(al.incrementAndGet() + " : " + integer));
}

怎麼樣,讓我聽到你們電腦的歡呼聲(此處應有滑稽表情)。好了,效果大家也看到了,我們來看下方法的定義:

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

通過傳入一個 Supplier ,來生成一個 stream。生成內部元素的時候,每生成一個,就會向 Supplier 再要一個,如此下來產生的就是無盡流。

不過,也不用擔心使用該方法後程序就停不下來,或者是 Supplier 產生了過量的元素導致效能低下。

public void limitGenerateTest() {
    Stream<Integer> generate = Stream.generate(() -> {
        System.out.println("#######");
        return new SecureRandom().nextInt();
    });
    AtomicLong al = new AtomicLong(0);
    generate.limit(3).forEach(integer -> System.out.println(al.incrementAndGet() + " : " + integer));
}

輸出結果如下:

####### 1 : -35704083
####### 2 : -1875554696
####### 3 : 541096732

可以看出來,使用 limit(3) 的時候,是每次需要時才產生新元素。

of

先看方法定義:

static <T> Stream<T> of(T t) static <T> Stream<T> of(T… values)

好的食材,往往通過最簡單的烹飪即可;而好用的方法,往往是一看就知道,一上手就會。 of 方法,傳入可變個數的引數,均可以轉為 stream。

public void ofTest() {
    Stream<String> strStream = Stream.of("this is stream.of demo");
    strStream.forEach(System.out::println);

    Stream<Integer> intStream = Stream.of(1, 0, 0, 8, 6);
    intStream.forEach(System.out::println);

    List<String> list = Arrays.asList("i", "am", "list", "of", "method");
    Stream<List<String>> listStream = Stream.of(list);
    listStream.forEach(System.out::println);
}

注意:如果傳入的引數是一個集合,如 List 或者 Set,流內的元素也是隻有一個,而不是集合內的元素。流內的元素,就是那個集合。

iterate

iterate 聽起來是不是有那麼點熟悉?再想想,集合的迭代是怎麼實現的?如果還沒想到,再提示下,關鍵字 hasNext 。雖然集合有不少不用 Iterator 的遍歷寫法,但是這個迭代器還是要了解的。來先看下 iterate 方法定義:

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

其中, T seed 是第一個元素,後續元素通過迭代計算出來。這樣滾動迭代,就產生了無限流。這點和 generate 其實有些相似,只不過 generate 的前後元素沒有關聯。

Stream.iterate(1, iter -> iter + 3).limit(5).forEach(System.out::print);

比如這個例子,將依次輸出 1,4,7,10,13

concat

嚴格來說, concat 並不算是建立型別的,畢竟建立型別是從無到有,是創造;而 concat 站在了前人的肩膀上,是創新,將已有的兩個流合併到一起。

照例,我們先看方法定義:

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

concat,將已有的兩個流合併到一起。文件更明確的說了以下的點:

  1. 更準確的說,是將第二個流放到第一個流屁股後面。
  2. 如果兩個流都是 ordered,那麼得到的新流也是 ordered。
  3. 如果兩個流有一個是並行流,則得到的新流是並行流。
  4. 關閉合並結果流時,會呼叫兩個輸入流的關閉方法。

當然,看原始碼的實現,上面的這些點是可以看出來的。拋開原始碼,我們來驗證一下。

合成新流的元素順序

public void concatTest1() {
    Stream<Integer> first = Stream.of(1, 3, 5);
    Stream<Integer> second = Stream.of(2, 3, 4);
    System.out.println(Stream.concat(first, second).collect(Collectors.toList()));
}

經驗證,輸出的內容是:

[1, 3, 5, 2, 3, 4]

就這驗證了,合成的流,順序確實是第一個流 + 第二個流, 並且不會去重

什麼是 ordered

流是 ordered, 並不是指對流的元素進行排序 。而是指流內的元素,是有固定的順序約束的。比如 List ,元素是有約束順序的: ArrayList 有陣列座標, LinkedList 有連結串列連線。所以, List 生成的流是 ordered。

更詳細的內容,下次單獨再說吧。

合併後的並行流

public void concatTest2() {
    Stream<Integer> first = Stream.of(1, 3, 5).parallel();
    Stream<Integer> second = Stream.of(2, 3, 4);
    Stream.concat(first, second).map(i -> i + " ").forEach(System.out::print);
}

這次的測試,把第一個流換成了並行流。如果合併後的流不是並行,那麼輸出的內容應該是原定的順序 1 3 5 2 3 4

驗證後的結果卻是輸出是無序的,說明了合併後的流是並行流:

合併後流的關閉

public void concatTest3() {
    Stream<Integer> first = Stream.of(1, 3, 5);
    Stream<Integer> second = Stream.of(2, 3, 4);
    System.out.println(Stream.concat(first, second).collect(Collectors.toList()));
    Stream.concat(first, second).map(i -> i + " ").forEach(System.out::print);
}

為了驗證這個問題,我們把流合併兩次看一下。在執行的時候,實際拋瞭如下異常:

java.lang.IllegalStateException: stream has already been operated upon or closed

其實,從原始碼來看,在合併成新流之後,first 和 second 兩個流都執行了 close 。

總結

Stream 的建立, ArraysCollection 也是出了一把力的。打鐵還需自身硬, Stream 本身提供的這些建立方法,覆蓋面也是極廣的。畢竟出門靠朋友, Stream 就這麼和集合打成一片了。

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