Java lambda stream functional Optional

[Java] Lambda 與 Stream

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 是指 mapfilter 這類中間操作只是把動作登記下來,不真的執行;直到呼叫終端操作collectforEachcount 等)才一次串起來跑完。終端操作跑完,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 運算、無共享可變狀態、資料量大(>104> 10^4 量級)。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 變數本身也可能是 nullOptional<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