二、Stream API

2.1 概述

Steam API 是 Java 8 中全新的特性,它基於Lambda 表達式,對集合(Collection)的各類操做進行了大幅度的加強,極大的提升了編碼的效率和程序的可讀性。java

Stream API 提供串行和並行兩種模式,並行模式會自動建立多個線程,使用 fork/join(Java 7 特性) 並行方式來拆分任務和加速處理過程,能充分利用多核處理器。牛X的是,咱們並不須要手動寫多線程處理的代碼,Stream API 自行實現了高性能的併發程序。數組

eg,看一下下面這段代碼幹了什麼?而後想一下若是不使用 Stream 操做,應該怎麼實現?安全

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.stream().filter(zoneId -> zoneId.startsWith("Asia")).skip(10).limit(5).forEach(System.out::println);

2.1.1 什麼是 Stream

這裏的 Stream 跟 IO流等其它流並非指一樣的東西,而是對集合執行流式操做的一種抽象。要分清一點,Stream 不是數據結構,自己不存儲元素,元素可能存儲在底層的集合中,或根據須要產生出來。數據結構

2.1.2 如何使用

Stream API 的用法很簡單,分3步執行,即: 建立/獲取流 -> 中間操做(過濾、轉換等) -> 終止操做( 聚合、收集結果)多線程

其中, 中間操做能夠執行屢次,而且都是延遲執行的,每次操做原有的 Stream 對象不改變,會產生一個新的 Stream; 終止操做只能有一個,一旦執行完成,Stream 就沒了。併發

2.2 建立流

2.2.1 從集合建立

Java 8 中,Collection 接口增長了 stream 方法,因此咱們能夠把任何一個集合轉成Streamdom

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
Stream<String> demo = zoneIds.stream();

2.2.2 從數組建立

Arrays 類中增長了 stream(T[] array)方法ide

String[] arr = {"1"};
Stream<String> demo = Arrays.stream(arr);

2.2.3 從靜態方法建立

Stream.of(T... values) Stream.generate(Supplier<T> s) 生成無限流 Stream.iterate(final T seed, final UnaryOperator<T> f), 生成無限流,可是多了一個seed,生成的流是 seed, f(seed), f(f(seed))...函數

Stream.of("1", "2", "2", "5", "7", "5", "6", "3", "4", "3");

Stream<Integer> stream1 = Stream.generate(() -> RandomUtils.nextInt(1, 10));
stream1.limit(10).forEach(System.out::print);  // 輸出 2223915594

Stream<Integer> stream2 = Stream.iterate(1, x -> x + 1);
stream2.limit(10).forEach(System.out::print); // 12345678910

2.2.4 其它

  • java.io.BufferedReader.lines()
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • java.util.Spliterator
  • Random.ints()
  • BitSet.stream()
  • Pattern.splitAsStream(java.lang.CharSequence)
  • JarFile.stream()

2.3 中間操做

2.3.1 過濾

  • filter 流中全部符合條件的數據轉到新流。filter方法的參數是一個 Pridicate<? super T> 類型的函數式接口,即一個返回 boolean 類型的 Lambda表達式。
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
Stream<String> oldStream = zoneIds.stream();
Stream<String> newStream = oldStream.filter(zoneId -> zoneId.startsWith("Asia"));

2.3.2 轉換

  • map 流中全部數據,通過處理後轉到新流。map方法的參數是一個 Function<? super T, ? extends R> 類型的函數式藉口,即接收一個參數並返回處理後的結果。 另,flatMap 方法會展開轉換的元素。
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
Stream<String> oldStream = zoneIds.stream();
Stream<String> newStream = oldStream.map(zoneId -> zoneId.replace("//", "-"));

2.3.3 提取

獲取一個流的一部分。工具

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.stream().skip(10).limit(5);
// 另外一個較好的例子
Stream<Integer> limit = Stream.generate(() -> RandomUtils.nextInt(1, 10)).limit(100);

2.3.4 組合

將兩個流合併成一個

Stream.concat(Stream.of("1", "2"), Stream.of("4", "5", "6"));

2.3.5 其餘

  • distinct 去重
  • sorted 排序
  • peek 監聽。能夠指定一個函數,當元素被處理的時候被每一個元素調用
  • parallel 轉爲並行流
  • sequential 轉爲串行流
  • unordered 不知道
Stream.of("1", "2", "2", "5", "7", "5", "6", "3", "4", "3")
        .peek(x -> System.out.println(x+" "))
        .filter(x -> x.compareTo("3")>0)
        .map(x -> x.replace("5", "9"))
        .distinct()
        .sorted()
        .skip(1)
        .limit(5)
        .forEach(System.out::print);

最終結果:

1 
2 
2 
5 
7 
5 
6 
3 
4 
3 
679

2.3.6 小結

思考一個疑問,咱們對一個 Stream 進行屢次轉換操做,每次都會產生一個新的 Stream,這樣操做是否是比咱們本身寫單次循環耗費更多的時間?

不是,轉換操做都是lazy的,多個轉換操做只會在終止操做的時候融合起來,一次循環完成。

2.4 終止操做

這部分是 Stream API 中最重要的內容,從上文中咱們已經知道如何建立一個流以及如何對其進行轉換。如今,咱們要從流中獲取結果。

2.4.1 聚合操做

  • reduce 這是一個通用的聚合方法,最基本的 reduce 方法接收一個 BinaryOperator<T> 類型的參數,使用的方式是這個樣子的:
IntStream is = IntStream.of(1, 2, 3 , 4, 5);
System.out.println(is.reduce((a, b) -> a+b));  // 輸出 OptionalInt[15]

在這個操做裏能夠看到,reduce 方法有兩個參數。其中第1個參數是上次操做返回的結果,第2個參數是 Stream 中下一個元素。若是是第一次執行,第1個參數是 Stream 中的第一個元素, 第2個參數是Stream 中的第2個元素。簡單描述一下這個操做的執行過程,

操做 結果
首先流初始化後有5個元素 1, 2, 3, 4, 5
第1次聚合 3,3, 4, 5
第2次聚合 6, 4, 5
第3次聚合 10, 5
第4次聚合 15

reduce方法還有一個很經常使用的變種,接收兩個參數: T reduce(T identity, BinaryOperator<T> accumulator),相對上面已經介紹過方法的多了一個參數:它容許用戶提供一個值,若是Stream爲空,就直接返回該值。

IntStream is = IntStream.of();
System.out.println(is.reduce(5, (a, b) -> a+b));  // 輸出 5

2.4.2 聚合方法

不少狀況下,咱們所作的聚合操做都是同樣的。因此爲了方便,Stream API 直接提供了經常使用的聚合方法

  • sum
IntStream s = IntStream.of(1,2,3,4);
System.out.println(s.sum());

注:只有 IntStream、LongStream、DoubleStream 纔有 sum 方法

  • count、max、min
System.out.println(ZoneId.getAvailableZoneIds().stream().count());   // 輸出 590
System.out.println(ZoneId.getAvailableZoneIds().stream().min(String::compareTo));  // 輸出 Optional[Africa/Abidjan]
System.out.println(ZoneId.getAvailableZoneIds().stream().max(String::compareTo));  // 輸出 Optional[Zulu]
  • findFirst、findAny
System.out.println(ZoneId.getAvailableZoneIds().stream().findFirst());  // Optional[Asia/Aden]
System.out.println(ZoneId.getAvailableZoneIds().stream().findAny());    // Optional[Asia/Aden]
  • anyMatch、 allMatch、 noneMatch allMatch:Stream 中所有元素符合傳入的 predicate,返回 true anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true
System.out.println(ZoneId.getAvailableZoneIds().stream().allMatch(x -> x.startsWith("A")));   // false
System.out.println(ZoneId.getAvailableZoneIds().stream().anyMatch(x -> x.startsWith("A")));   // true
System.out.println(ZoneId.getAvailableZoneIds().stream().noneMatch(x -> x.startsWith("A")));  // false

2.4.3 收集結果

  • collect 把結果收集到容器中,collect 方法的基本方法是這個
<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

可是這個方法用着比較麻煩, 通常直接用另外的工具方法

  • 收集到集合
List<String> list = ZoneId.getAvailableZoneIds().stream().limit(5).collect(Collectors.toList());
Set<String> set = ZoneId.getAvailableZoneIds().stream().collect(Collectors.toSet());
  • 收集到Map
Map<String, String> map = ZoneId.getAvailableZoneIds().stream()
        .limit(3)
        .collect(Collectors.toMap(x -> x, y -> y));
System.out.println(map);  
// 輸出 {Asia/Aden=Asia/Aden, America/Cuiaba=America/Cuiaba, Etc/GMT+9=Etc/GMT+9}

2.4.5 遍歷結果

並非全部時候咱們都須要把結果匯聚在一塊兒,咱們也能夠只是遍歷各個元素

  • iterator 轉成傳統的迭代器
Stream<String> zoneIdStream = ZoneId.getAvailableZoneIds().stream();
Iterator<String> it = zoneIdStream.iterator();
  • forEach、 forEachOrdered
ZoneId.getAvailableZoneIds().stream().limit(5).forEach(System.out::print);

2.4.6 分組

  • groupingBy 基本的分組方法描述是這樣的,第一個參數是分組函數,第二個參數是返回的類型,第三個參數是一個收集器
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                              Supplier<M> mapFactory,
                              Collector<? super T, A, D> downstream)

最簡單的用法是隻提供一個分組函數,那麼默認會返回

Map<String, List<String>> map = ZoneId.getAvailableZoneIds().stream()
        .limit(5)
        .collect(Collectors.groupingBy(x -> x.substring(0, x.indexOf("/")))
);
System.out.println(map);
// 輸出 {Etc=[Etc/GMT+9, Etc/GMT+8], Asia=[Asia/Aden], Africa=[Africa/Nairobi], America=[America/Cuiaba]}

若是咱們想控制類型,能夠用Collectors改變,好比

Map<String, Set<String>> map = ZoneId.getAvailableZoneIds().stream()
        .limit(5)
        .collect(Collectors.groupingBy(x -> x.substring(0, x.indexOf("/")), Collectors.toSet())
);

Stream Api 還提供其餘一些收集器,好比counting,計算每一個分組的元素數量

Map<String, Long> map = ZoneId.getAvailableZoneIds().stream()
        .limit(5)
        .collect(Collectors.groupingBy(x -> x.substring(0, x.indexOf("/")), Collectors.counting()));
System.out.println(map);
// 輸出 {Etc=2, Asia=1, Africa=1, America=1}

2.4.7 Optional 類型

在前文的介紹中,你應該看到了 Optional 這個東西,有不少終止操做的結果是Optional<T>。那麼,Optional是什麼? Optional 是一個容器,它可能包含也可能不包含一個值,用意是替代不安全的返回類型 NULL。好比,咱們常見的一種代碼,

public Integer getInteger() {
    return null;
}

若是咱們須要使用這個方法,通常是這樣的

Integer i = getSomething();
if(i != null) {
    System.out.println(i);
    // do something
}

調用一個返回T類型的方法後,爲了安全咱們必須驗證這個對象不是 NULL,形成代碼的閱讀星和維護性都比較差,可是若是沒有這樣作,就極可能出現 NullPointException。 Optional 就是爲了解決這個問題而設計的,它提供了一個接受 Consumer<? super T>類型參數的方法 ifPresent,只有當存在可用的元素時,才執行邏輯

public Optional<Integer> getSomethingOptinal() {
    return Optional.empty();
}

咱們直接調用這個方法

getSomethingOptinal().ifPresent(System.out::print);

正常執行,沒有異常。 因此如今當咱們一個方法返回List集合時,應該老是返回一個空的List,而不是Null; 當一個返回T類型的方法有可能爲Null 時,應該該用Optional<T>。

相關文章
相關標籤/搜索