Java 自學筆記 09 - Stream API

Java Stream

Java Stream

Stream 是一個元素形成的隊列,其來源 (Source) 可以是集合或數組,Stream 本身並不會存儲元素,其操作也不會影響到原始來源,單純以數據流的方式來處理數據,依性質可以分為兩類:

  • 中間操作 (Intermediate Operations)
    Stream 的中間步驟,一個 Stream 可以有多個中間操作,會返回一個新的 stream,屬於惰性求值 (Lazy Evaluation),意思是不會馬上計算結果,而是等到終端操作時才一併計算。
  • 終端操作 (Terminal Operations)
    Stream 的最後一步,一個 stream 只能有一個終端操作,會產生一個新的集合或值。

創建 Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// array to stream
int[] array = {1, 2, 3};
Arrays.stream(array);
Stream.of(array);

// collecitions to stream
List<Integer> numbers = Arrays.asList(1, 2, 3);
Set<Integer> set = new HashSet<>(numbers);
Map<Integer, Integer> map = new HashMap<>(){{
put(1, 1); put(2, 2); put(3,3)
}};

numbers.stream();
set.stream();
map.keySet().stream();
map.values().stream();

Parallel Stream

Stream 支援併行流 (parallelStream),底層默認使用 ForkJoinPool,所謂的 ForkJoinPool 其實就是 divide and conquer 算法,將任務不斷拆分成小任務處理,能充分利用 CPU 的效能,細節可以參考: ForkJoin Pool

ForkJoinPool

可以直接創建 Parallel Stream 也可以從 Stream 轉換:

1
2
numbers.parallelStream();    // or
numbers.stream().parallel();

遍歷/匹配 (foreach/find/match)

findFirst、findAny 等操作返回的為一個 Optional 物件,用來表示一個可能為 null 的值,常用方法為:

  • get():获取 Optional 对象中的值,如果值为 null,则抛出 NoSuchElementException 异常。
  • orElse(T other):获取 Optional 对象中的值,如果值为 null,则返回指定的默认值。
  • isPresent():判断 Optional 对象是否包含值。
  • ifPresent(Consumer<? super T> consumer):如果 Optional 对象包含值,则执行指定的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// forEach
numbers.stream()
.forEach(System.out:: println);

// findFirst (返回第一個)
Optional<Integer> first = numbers.stream()
.filter(n -> n > 2)
.findFirst();
first.get();

// findAny (返回任一個)
numbers.parallelStream()
.filter(n -> n > 2)
.findAny()
.get();

// anyMatch (任一符合)
boolean match = numbers.parallelStream()
.filter(n -> n > 2)
.anyMatch();

篩選 (filter)

1
2
3
4
numbers.stream()
.filter(n -> n % 2 == 0)
.sorted((n1, n2)-> n2 - n1) // Descending
.forEach(System.out::println);

映射 (map/flatMap)

  • map: 把一個元素映射到一個新的元素
  • flatMap: 把一個元素映射到一個新的 stream
1
2
3
numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());

假設要將 {"Hello", "World"} 轉換成 {"H", "e", "l", "o", "W", "r", "d"},那我們不能直接用 map(s->s.split("")),因為 map 回傳的是一個元素,所以會是 String[],這時候就可以改用flatMap:

1
2
3
4
5
6
helloworld = {"H", "e", "l", "o", "W", "r", "d"};

helloworld.stream()
.flatMap(word-> Arrays.stream(word.split("")))
.distinct()
.collect(Collectors.toList());

歸約 (reduce)

  • identity: 初始值,沒設置就是用第一筆資料當初始值。
  • accumulator: 累加器函數,第一個參數是累加值,第二個參數是當前值。
  • combiner: 組合器函數,在 parallel 情況下用來合併各個線程的結果。

accumulator

1
2
3
4
5
6
// Optional<T> reduce(
// BinaryOperator<T> accumulator
// )
Optional<Integer> opt_sum = numbers.stream()
.reduce((acc, x) -> acc + x);
int sum1 = opt_sum.get();

identity + accumulator

1
2
3
4
5
6
// T reduce(
// T identity,
// BinaryOperator<T> accumulator
// )
int sum2 = numbers.stream()
.reduce(0, (acc, x) -> acc + x);

identity + accumulator + combiner

1
2
3
4
5
6
7
8
9
10
11
12
13
// <U> U reduce(
// U identity,
// BiFunction<U, ? super T, U> accumulator,
// BinaryOperator<U> combiner
// )
numbers.parallelStream()
.reduce(0, (acc, x)->{
System.out.println("acc: " + acc + ", x: " + x);
return acc+x;
},(comb, x)->{
System.out.println("comb: " + comb + ", x: " + x);
return comb+x;
});

output:

1
2
3
4
5
acc: 0, x: 2    // 各線程
acc: 0, x: 1
acc: 0, x: 3
comb: 2, x: 3 // 合併線程
comb: 1, x: 5

可以發現每個元素都被當作是一個新的線程了 (acc 都是 0),等於 accumulator 根本沒派上用場,事實上,accumulator 在這裡的作用反而更像是一個中介的轉換器,負責將原本的類型 T 轉換到 U,可以參考: Why is a combiner needed for reduce method that converts type in java 8

收集 (collect)

collect 的目的是用來將 stream 轉存為其他資料型態。

stream to collections

1
2
3
4
5
6
numbers.stream()
.collect(Collectors.toList());
numbers.stream()
.collect(Collectors.toSet());
numbers.stream()
.collect(Collectors.toMap(x->x, x->x));

partitioningBy / groupingBy

  • partitioningBy: 分為兩群 (Map<boolean, List<T>>)
  • groupingBy: 分為多群 (Map<T, List<T>>)

partitioningBy

1
2
3
4
numbers.stream()
.collect(Collectors.partitioningBy(x -> x>2));
numbers.stream()
.collect(Collectors.groupingBy(x -> x%4));

joining (String)

1
2
3
String joined = numbers.stream()
.map(String::valueOf)
.collect(Collectors.joining("-"));

統計 (max/min/sum/average/count)

max / min

max / min 相關的操作都是返回 Optional 物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// int
Optional<Integer> max = numbers.stream()
.max(Integer::compareTo);
max.get();

// String
names.stream()
.min(Comparator.comparing(String::valueOf))
.get();

// stream
numbers.stream()
.collect(Collectors.maxBy(Integer::compareTo))
.get();

sum / average / count

注意 average 回傳的是 double

1
2
3
4
5
6
7
8
9
10
11
// sum
numbers.stream()
.collect(Collectors.summingInt(x->x));

// average
double average = numbers.stream()
.collect(Collectors.averagingInt(x->x));

// count
names.stream().count();
names.stream().collect(Collectors.counting());

參考資料

Java 8 Stream | 菜鸟教程
Java8 Stream 流的创建、筛选、映射、排序、归约、分组、聚合、提取与组合、收集、接合、foreach遍历
java8中stream的map和flatmap的理解
Java 8系列之Stream中万能的reduce