流基礎
流是資料管道,表示一系列資料。流的操作是針對資料來源來說的,但是流的操作不會改變資料來源的資料,只會產生新的流。
最基礎的流是 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