Java 8(2014)是分水嶺:lambda、Stream API、Optional 一次到位,整個語言的寫法跟著變。前面集合、IO、Comparator 看到的 -> 與 Stream<T>,這篇正式展開。重點不是「會寫 lambda」,而是把 functional interface、method reference、stream pipeline 串成一套思路。
Lambda 語法
Java 8+ 的核心想法:把「行為」當參數傳。
// 完整型
(int a, int b) -> { return a + b; }
// 型別可推斷
(a, b) -> { return a + b; }
// 單一表達式可省 { } 與 return
(a, b) -> a + b
// 單一參數可省括號
x -> x * 2
// 無參數
() -> System.out.println("hi")
Lambda 只能賦值給 functional interface——只有一個抽象方法的介面。
內建 functional interface
常用的組合 java.util.function 都幫你定義好了:
| 介面 | 方法 | 範例 |
|---|---|---|
Function<T,R> | R apply(T) | String::length |
Predicate<T> | boolean test(T) | s -> s.isEmpty() |
Consumer<T> | void accept(T) | System.out::println |
Supplier<T> | T get() | ArrayList::new |
BiFunction<T,U,R> | R apply(T,U) | (a,b) -> a + b |
UnaryOperator<T> | T apply(T) | s -> s.toUpperCase() |
BinaryOperator<T> | T apply(T,T) | Integer::sum |
Function<String, Integer> len = String::length;
Predicate<String> isEmpty = String::isEmpty;
Consumer<String> print = System.out::println;
Supplier<List<Integer>> newList = ArrayList::new;
len.apply("hello"); // 5
isEmpty.test(""); // true
method reference
共四種:
String::length // instance method on type → x -> x.length()
System.out::println // instance method on object → x -> System.out.println(x)
Integer::parseInt // static method → s -> Integer.parseInt(s)
ArrayList::new // constructor → () -> new ArrayList<>()
只要 lambda 的內容只是「呼叫一個既有方法」,就改寫成 method reference——更短更乾淨。
Stream 是什麼
Stream 是「集合的處理管線」,不是新的資料結構。從來源(集合 / 陣列 / IO)出發,串接中間操作(lazy),最後由一個終端操作觸發實際計算。
List<String> names = List.of("Jeremy", "Aira", "Tom");
long count = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.count();
三個特性:
- lazy:中間操作不會實際跑,等終端操作才走一遍
- single-use:跑完一個終端操作就用完了,不能重複
- 不修改來源:原 collection 不變
展開來說,lazy 是指 map、filter 這類中間操作只是把動作登記下來,不真的執行;直到呼叫終端操作(collect、forEach、count 等)才一次串起來跑完。終端操作跑完,stream 就「耗盡」了,再呼叫會丟 IllegalStateException——stream 不能重用,要重跑就從 source 再開一條。
建立 Stream
list.stream();
Stream.of(1, 2, 3);
Arrays.stream(arr);
IntStream.range(0, 10); // 0..9
IntStream.rangeClosed(1, 10); // 1..10
Stream.iterate(1, x -> x + 1).limit(10);
Stream.generate(Math::random).limit(5);
Files.lines(path); // 來源是檔案
中間操作
.filter(p) // 條件保留
.map(f) // 轉換
.flatMap(f) // 一對多後攤平
.distinct() // 去重
.sorted() // 自然排序
.sorted(comparator)
.peek(c) // 偷看(debug 用)
.limit(n) // 取前 n
.skip(n) // 跳前 n
flatMap 範例:
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList();
// [1, 2, 3, 4]
終端操作
.count()
.sum() / .average() / .min() / .max() // 在 IntStream 等
.anyMatch(p) / .allMatch(p) / .noneMatch(p)
.findFirst() / .findAny() // 回 Optional
.forEach(c)
.toList() // Java 16+,不可變
.toArray()
.reduce(...)
.collect(...)
toList() 是 Java 16+ 的捷徑,等同 .collect(Collectors.toUnmodifiableList())。舊版用 .collect(Collectors.toList())。
reduce
從一堆值算出一個值:
int sum = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); // 10
String joined = Stream.of("a","b","c").reduce("", String::concat); // "abc"
// 不帶 identity,回 Optional
OptionalInt max = IntStream.of(3, 1, 4).max();
Collectors:強大的收尾
import static java.util.stream.Collectors.*;
record Person(String name, String department, int age) {}
List<Person> people = List.of(
new Person("Alice", "Eng", 30),
new Person("Bob", "Sales", 25),
new Person("Carol", "Eng", 28)
);
List<String> list = stream.collect(toList());
Set<String> set = stream.collect(toSet());
String joined = stream.collect(joining(", ", "[", "]"));
Map<String, Integer> byName = people.stream()
.collect(toMap(Person::name, Person::age));
// 分組
Map<String, List<Person>> byDept = people.stream()
.collect(groupingBy(Person::department));
// 分組後再計數
Map<String, Long> countByDept = people.stream()
.collect(groupingBy(Person::department, counting()));
// 分組後再加總
Map<String, Integer> ageSumByDept = people.stream()
.collect(groupingBy(Person::department, summingInt(Person::age)));
// 兩分(true / false)
Map<Boolean, List<Person>> adultOrNot = people.stream()
.collect(partitioningBy(p -> p.age() >= 18));
數值 stream:IntStream / LongStream / DoubleStream
數值專用的 stream,省掉裝箱拆箱的開銷:
int sum = IntStream.rangeClosed(1, 100).sum(); // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);
// 物件 stream → 數值 stream
int total = people.stream().mapToInt(Person::age).sum();
// 數值 stream → 物件 stream
List<Integer> boxed = IntStream.of(1, 2, 3).boxed().toList();
平行 stream
long count = list.parallelStream()
.filter(...)
.count();
底層走 ForkJoinPool。要划算,得同時滿足三個條件:純 CPU 運算、無共享可變狀態、資料量大( 量級)。I/O 密集(DB、網路)會阻塞 ForkJoinPool 的 worker;資料量小或 forEach 帶 side effect,好處都會被同步與 fork 的開銷吃掉,反而更慢。日常 99% 用 sequential 就好。
Optional:替代 null
Optional 把「可能有值、可能沒有」包成一個型別:
Optional<User> u = repo.findById(1);
u.isPresent();
u.isEmpty(); // Java 11+
u.get(); // ❌ 沒值會丟,少用
u.orElse(defaultUser);
u.orElseGet(() -> defaultUser); // 預設值生成有成本時用
u.orElseThrow(); // 沒值丟 NoSuchElementException
u.orElseThrow(() -> new NotFoundException("user"));
u.ifPresent(user -> log(user));
u.ifPresentOrElse(this::log, this::warn);
u.map(User::name).orElse("anon");
u.filter(user -> user.isActive());
使用慣例:
- 回傳值「可能不存在」時用
Optional<T> - 欄位、參數、集合元素不要用 Optional(過度包裝)
- 不要回傳
null Optional(永遠Optional.empty())
為什麼欄位、參數、集合元素不要用 Optional?首先 Optional 變數本身也可能是 null(Optional<T> x = null),反而多一層 NullPointerException 風險;再來序列化框架(Jackson、JPA)處理 Optional 欄位一向麻煩;最後,與其寫 List<Optional<T>>,不如直接用 List<T> 並過濾掉 null,語意更清楚。Optional 的定位就是當回傳型別,標示「可能沒有」。
常見組合範例
計詞頻
Map<String, Long> freq = Arrays.stream(text.split("\\s+"))
.collect(groupingBy(Function.identity(), counting()));
top N
List<Person> top3 = people.stream()
.sorted(Comparator.comparingInt(Person::score).reversed())
.limit(3)
.toList();
找最大值的物件
Optional<Person> oldest = people.stream()
.max(Comparator.comparingInt(Person::age));
把 Map 反轉
Map<V, K> inverted = m.entrySet().stream()
.collect(toMap(Map.Entry::getValue, Map.Entry::getKey));
寫 stream 的取捨
- 鏈式讀得順就用 stream,邏輯一旦多重巢狀就回頭寫 for
- side effect(修改外部變數)放
forEach,不要塞進peek - 別追求「全 stream」,可讀性優先
下一篇是 Java 系列收尾——建置工具與測試:Maven vs Gradle、JUnit 5 寫法、Mockito 基礎。一個專案能不能交出去,靠這層支撐。
Latest Updates
- 2026.06.11 Content updated
