JTCR-Stream API-23

x-yun發表於2024-04-24

流基礎

流是資料管道,表示一系列資料。流的操作是針對資料來源來說的,但是流的操作不會改變資料來源的資料,只會產生新的流。

最基礎的流是 BaseStream 介面。

interface BaseStream<T, S extends BaseStream<T, S>>

T 表示流中資料的型別,S 表示擴充套件了 BaseStream 的流。BaseStream 擴充套件了 AutoCloseable 介面。

Stream 介面繼承了 BaseStream 介面。

interface Stream<T>

T 表示流中資料的型別,任何引用型別資料都可以使用這個流。

流的操作分為兩種,第一種是終止(terminal)操作,該操作會消耗流,產生一個結果,被消耗的流不能再次使用;另一種是中間(intermediate)操作,該操作會產生另一個流,可以用於建立管道,進行多個操作。中間操作不會立即執行,只有當終止操作需要執行時,前面的中間操作才會執行,稱為 lazy behavior,這個機制可以提高執行效率。

中間操作又可以分為無狀態(stateless)和有狀態的(stateful)兩種。無狀態表示流中每個元素獨立處理,與其他元素無關;有狀態表示流中一個元素的處理依賴於其他元素。在併發處理流時操作有無狀態很重要。

Stream 流中的元素必須是引用型別,為了處理原始型別,提供了三種繼承自 BaseStream 的流介面,如下所示

  • DoubleStream
  • IntStream
  • LongStream

這些流除了用於處理原始型別,其他方面與 Stream 介面相似。

JDK8 開始,Collection 介面引入了 stream() 方法用於獲取流。

// 返回流
default Stream<E>  stream();

// 如果可以,返回併發流;否則返回流
default Stream<E> parallelStream();

Arrays 類的方法 stream() 方法將陣列作為流的資料來源。形式之一為

static <T> Stream<T> stream(T[] array);

其他形式的 stream() 可以返回用於原始型別的三種流。

BufferedReader 的 lines() 方法返回流。

public static void m() {
  var list = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
    list.add(i);
  }
  Stream<Integer> s = list.stream();
  
  Optional<Integer> min = s.min(Integer::compare);
  if (min.isPresent()) {
    min.get();
  }
  
  s = list.stream();
  Optional<Integer> max = s.max(Integer::compare);
  if (max.isPresent()) {
    max.get();
  }
  
  list.stream().sorted().forEach(e -> System.out.print(e + " "));
  System.out.println();
  
  list.stream().filter(e -> (e % 2) == 1).filter(e -> e > 5)
    					 .forEach(e -> System.out.print(e + " "));
  System.out.println();
}

Optional 物件要麼包含一個值要麼為空。值的型別由 T 指定。

class Optional<T>

歸約操作

歸約操作(reduction operations)是對流進行處理,最終得到一個值的操作型別。流的 count() 方法返回流中資料的個數。reduce() 方法可以使用指定條件對流進行處理得到一個值。

Optional<T> reduce(BinaryOperator<T> accu);
T reduce(T i, BinaryOperator<T> accu);

T 表示流中資料型別,i 必須滿足流中任一資料和它執行 accu 操作得到的結果是該資料。例如,若 accu 為加法,則 i = 0;如果 accu 為乘法,則 i = 1。BinaryOperator 介面繼承自 BiFunction 介面,BiFunction 的抽象方法為

R apply(T v1, U v2)

BinaryOperator 的抽象方法為

T apply(T v1, T v2)

v1 根據 reduce() 方法引數的不同首次表示第一個元素或者 i 的值,v2 表示下一個元素。之後,v1 表示最近計算得到的結果,v2 表示下一個元素。accu 操作必須滿足三個要求

  • 無狀態
  • 無干擾:流不改變資料來源
  • 結合性:類比數學中的結合律

結合性對於併發流的操作很重要。

public static void m() {
  var list = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
    list.add(i);
  }
  
  Optional<Integer> r1 = list.stream().reduce((a, b) -> a * b);
  if (r1.isPresent()) {
    r1.get();
  }
  
  int r2 = list.stream().reduce(1, (a, b) -> a * b);
}

除了 Collection 介面的 parallelStream() 方法獲取併發流之外,流呼叫 BaseStream 介面的 parallel() 方法也可以獲得併發流。當環境支援時,才能使用併發流;否則,併發流自動轉變為流。

一般情況下,對併發流執行的操作必須滿足結合性要求,其他兩個要求也應該滿足。

併發流一個特有的 reduce() 方法為

<U> U reduce(U i, BiFunction<U, ? super T, U> accu, BinaryOperator<U> comb);

其中,accu 用於歸約操作,comb 用於將部分結果合併,得到最終的結果。

public static void m() {
  var list = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
    list.add(i);
  }
  double r = list.parallelStream().reduce(1.0, 
                                          (a, b) -> a * Math.sqrt(b), 
                                          (a, b) -> a * b);
}

併發流可以呼叫 BaseStream 的 sequential() 方法獲得流。

使用併發流時,如果資料來源的資料有序,那麼流中資料有序。如果對併發流進行操作時順序不重要,可以呼叫 BaseStream 介面的 unordered() 方法獲得一個無序流,對無序流使用併發操作可以提高效能。

併發流使用 forEach() 方法不會保留流中資料的順序,如果想按照順序執行,使用 forEachOrdered() 方法。

Mapping

對映指透過某種規則將一個元素轉換成另一個元素。通用方法為 map(),形式為

<R> Stream<R> map(Function<? super T, ? extends R> mapF);

T 表示當前流中元素型別,R 表示返回流中元素型別。mapF 函式必須是無狀態和無干擾的,Function 的抽象方法為

R apply(T val)
public static void m() {
  var list = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
    list.add(i);
  }
  double r = list.stream().map(e -> Math.sqrt(e)).reduce(1.0, (a, b) -> a * b);
}

map() 方法的其他版本如下

IntStream mapToInt(ToIntFunction<? super T> mapF);
LongStream mapToLong(ToLongFunction<? super T> mapF);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapF);

ToIntFunction 的方法為 applyAsInt(T v),必須返回 int 型別的結果。

public static void m() {
  var list = new ArrayList<Double>();
  list.add(1.2);
  list.add(3.9);
  list.add(8.1);
  list.add(2.4);
  
  list.stream()
      .mapToInt(e -> (int)Math.ceil(e)).forEach(e -> System.out.print(e + " "));
  System.out.println();
}

flatMap() 方法用於將一個由聚合元素組成的流拆分成由單個元素組成的流。有 flatMapToXX() 方法,XX 表示 Int/Long/Double。

flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
public static void m() {
  String[][] a = new String[][] {
    {"a", "b"},
    {"c", "d"}
  };
  var r = Arrays.stream(a).flatMap(Arrays::stream).toArray(String[]::new);
  Arrays.stream(a).flatMap(Arrays::stream).forEach(e -> System.out.print(e + " "));
}

Collecting

流的 collect() 方法提供了從流中獲取資料構建集合的功能。一種形式如下

<R, A> R collect(Collector<? super T, A, R> colFun);

R 表示結果型別,A 表示中間結果型別,T 表示流中資料型別。Collector 介面定義如下,型別引數同前述

interface Collector<T, A, R>;

Collectors 類定義了若干返回 colletor 的靜態方法。例如

static <T> Collector<T, ?, List<T>> toList();
static <T> Collector<T, ?, Set<T>> toSet();

分別返回將流中資料構建 List 和 Set 的 collector。

public static void m() {
  var list = new ArrayList<Integer>();
  for (int i = 0; i < 10; i++) {
    list.add(i);
  }
  var stream = list.stream().filter(e -> e % 2 == 0);
  List<Integer> r1 = stream.collect(Collectors.toList());
  Set<Integer> r2 = stream.collect(Collectors.toSet());
}

collect() 方法的另一種形式為

<R> R
  collect(Supplier<R> target, BiConsumer<R, ? super T> accu, BiConsumer<R, R> comb);

類似於 reduce() 方法。target 表示建立集合的方式,Supplier 的方法 get() 返回一個 R 型別的引用,accu 表示的 accept(T v1, U v2) 中,v1 表示集合,v2 表示元素;comb 表示的accept(T v1, U v2) 中,v1 和 v2 都表示集合。

LinkedList<Integer> r3 = stream.collect(() -> new LinkedList(),
                                       (a, b) -> a.add(b),
                                       (a, b) -> a.addAll(b));

HashSet<Integer> r4 = stream.collect(HashSet::new,
                                    HashSet::add,
                                    HashSet::addAll);

迭代器和流

流的 iterator() 方法返回和流關聯的迭代器。

Iterator<T> iterator();

如果流中資料型別是原始型別,則返回與原始型別對應的流。

public static void m() {
  var list = new ArrayList<String>();
  list.add("al");
  list.add("bc");
  list.add("ed");
  
  Iterator<String> ite = list.stream().iterator();
  while (ite.hasNext()) {
    System.out.print(ite.next() + " ");
  }
  System.out.println();
}

使用 spliterator 迭代器遍歷元素時,使用 tryAdvance() 方法,當有元素需要遍歷時,執行遍歷操作,遍歷操作由引數提供,返回 true;沒有下一個元素時,返回 false。

boolean tryAdvance(Consumer<? super T> action);
public static void m() {
  var list = new ArrayList<String>();
  list.add("al");
  list.add("bc");
  list.add("ed");
  
  Spliterator<String> ite = list.stream().spliterator();
  while (ite.tryAdvance(e -> System.out.print(e + " ")));
  System.out.println();
}

Spliterator 介面的 forEachRemaining() 方法接收一個 Consumer 函式式介面物件,對剩餘的每個元素執行物件表示的操作。和 tryAdvance() 相比,不需要使用 while 語句。

default void forEachRemaining(Consumer<? super T> action);

Spliterator 介面的 trySplit() 方法將迭代器關聯的資料劃分成兩份,一份由原迭代器關聯,另一個份由返回的迭代器關聯,併發程式設計時加快資料的處理。當迭代器不允許分割時,返回 null。

public static void m() {
  var list = new ArrayList<String>();
  list.add("al");
  list.add("bc");
  list.add("ed");
  
  Spliterator<String> ite = list.stream().spliterator();
  Spliterator<String> it2 = ite.trySplit();
  if (it2 != null) {
    it2.forEachRemaining(e -> System.out.println(e));
  }
  System.out.println();
  ite.forEachRemaining(e -> System.out.println(e));
}

參考

[1] Herbert Schildt, Java The Complete Reference 11th, 2019.
[2] https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
[3] java-8-flatmap-example