โหมดมืด
บทที่ 9 — Lambda, Stream, Optional
🟡 หมายเหตุระดับ: บทนี้เริ่มจากพื้นฐาน (lambda, basic stream, Optional) แต่จบที่ระดับกลาง-สูง — ส่วน
Gatherers(Java 22+),mapMulti,teeing,Stream.iterate3-arg เป็น reference ขั้นสูง มือใหม่ข้ามได้ในรอบแรก แล้วกลับมาตอนเจอใช้จริงในงาน
นี่คือ "Java สมัยใหม่" — feature ที่เปลี่ยนสไตล์การเขียน Java ตั้งแต่ Java 8 (2014) เป็นต้นมา
หลังจบบท คุณจะ:
- เขียน lambda expression (function สั้น ๆ ไม่มีชื่อ) แทน anonymous class (คลาสไม่มีชื่อใช้ครั้งเดียว)
- ใช้ Stream API (pipeline ประมวลผลข้อมูลแบบ functional — สไตล์การเขียนโค้ดที่เน้นส่ง function ไปมาแทนการแก้ไข object ทีละตัว) filter/map/reduce/collect
- จัดการ null อย่างปลอดภัยด้วย Optional (กล่องห่อค่าที่อาจไม่มี)
- เข้าใจ functional interface (interface ที่มี abstract method เดียว)
1. Lambda — function สั้น ๆ ที่ส่งไปมาได้
1.1 ปัญหาก่อนมี lambda
ใน Java 7 ถ้าต้องการ "ส่ง function" ต้องสร้าง anonymous class = class ที่สร้างและใช้ครั้งเดียว ไม่ตั้งชื่อ (เดี๋ยวจะเห็นว่าทำไม Java สมัยใหม่เลิกใช้):
java
List<String> names = new ArrayList<>(List.of("Anna", "Ben", "Cara"));
// เรียง descending
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});ยาว — แค่อยากบอก "เปรียบเทียบยังไง"
1.2 Lambda แก้ปัญหา
java
Collections.sort(names, (a, b) -> b.compareTo(a));สั้นกว่าเยอะ!
📖
compareToคืออะไร: method ของ String (และ class อื่น ๆ ที่ implementComparable) ที่เทียบสองค่า:
- คืน
< 0→ this น้อยกว่า other- คืน
0→ เท่ากัน- คืน
> 0→ this มากกว่า otherปกติ
a.compareTo(b)= น้อย→มาก · กลับเป็นb.compareTo(a)= มาก→น้อย (descending)
1.3 Syntax ของ lambda
java
(parameters) -> { body }ตัวอย่าง:
java
// ไม่มี parameter
() -> System.out.println("hello")
// 1 parameter — ไม่ต้องวงเล็บ
x -> x * 2
// 2+ parameter — ต้องวงเล็บ
(a, b) -> a + b
// body หลายบรรทัด — ต้อง {} + return
(a, b) -> {
int sum = a + b;
return sum * 2;
}1.4 Functional Interface — interface ที่มี method เดียว
Lambda ใช้ได้กับ interface ที่มี abstract method 1 ตัวเท่านั้น เรียกว่า functional interface:
java
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calc(3, 5)); // 8
System.out.println(multiply.calc(3, 5)); // 15@FunctionalInterface = annotation (คำสั่งพิเศษที่ขึ้นต้นด้วย @ ใช้บอก compiler หรือ framework) บอก compiler ว่า interface นั้น "ต้องมี abstract method เดียว" ถ้ามี abstract method ตัวที่ 2 จะ compile error
ส่วน default กับ static method มีได้ไม่นับ (ดู บทที่ 6c — interface)
💡 ความหมายของ "method เดียว" = abstract เท่านั้น — interface ยังมี
default/staticmethod เพิ่มได้ตามต้องการ (ดู java/06c) เป็นเหตุที่Predicate<T>มี methodand(),or(),negate()เพิ่มด้วยแต่ยังเป็น functional interfaceCompiler จะ match lambda กับ functional interface โดยดูจาก abstract method เดียวนั้น → จึงรู้ว่า
s -> s.length()ตรงกับFunction.apply(T)
1.5 Functional interface ที่ Java เตรียมไว้ (java.util.function)
| Interface | Method | ใช้เมื่อ |
|---|---|---|
Predicate<T> | boolean test(T t) | เช็คเงื่อนไข |
Function<T, R> | R apply(T t) | แปลง T → R |
Consumer<T> | void accept(T t) | ใช้ T (ไม่ return) |
Supplier<T> | T get() | สร้าง T (ไม่รับ input) |
BiFunction<T, U, R> | R apply(T t, U u) | 2 inputs → 1 output |
UnaryOperator<T> | T apply(T t) | T → T |
BinaryOperator<T> | T apply(T a, T b) | (T, T) → T |
java
import java.util.function.*;
Predicate<String> isEmpty = s -> s.isEmpty();
Function<String, Integer> length = s -> s.length();
Consumer<String> printer = s -> System.out.println(s);
Supplier<String> hello = () -> "Hello";
isEmpty.test(""); // true
length.apply("hello"); // 5
printer.accept("hi"); // print "hi"
hello.get(); // "Hello"1.6 Method reference — shortcut ของ lambda
java
// lambda
names.forEach(s -> System.out.println(s));
// method reference (เท่ากัน)
names.forEach(System.out::println);Class::method หรือ instance::method = "ใช้ method นี้เป็น lambda"
4 แบบ:
java
ClassName::staticMethod // String::valueOf
ClassName::instanceMethod // String::toUpperCase (ต้อง pass instance)
instance::instanceMethod // System.out::println
ClassName::new // ArrayList::new2. Stream API — pipeline ของข้อมูล
Stream = "ลำธารของข้อมูล" ที่ผ่าน operation ต่อ ๆ กัน
2.1 ปัญหาที่ Stream แก้
แบบเก่า — หา sum ของเลขคู่:
java
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = 0;
for (int n : numbers) {
if (n % 2 == 0) {
sum += n;
}
}
System.out.println(sum); // 30แบบ Stream:
java
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();อ่านได้ว่า "เอา numbers, filter เฉพาะคู่, sum"
2.2 Anatomy ของ Stream
text
source → intermediate ops (filter, map, ...) → terminal op (collect, sum, count, ...)Source — มาจากไหน
java
import java.util.Random;
import java.util.UUID;
import java.nio.file.*;
import java.util.regex.Pattern;
List<Integer> list = List.of(1, 2, 3);
list.stream(); // จาก collection
list.parallelStream(); // จาก collection แบบ parallel
Arrays.stream(new int[]{1,2,3}); // จาก primitive array
Arrays.stream(new String[]{"a","b"}); // จาก object array
Stream.of(1, 2, 3); // ตรง ๆ
Stream.empty(); // ว่าง
Stream.iterate(1, n -> n + 1); // infinite — บวก 1 เรื่อย ๆ (ต้องมี .limit())
Stream.iterate(1, n -> n < 100, n -> n + 1); // มี exit condition (Java 9+)
Stream.generate(() -> Math.random()); // infinite — generator (ต้องมี .limit())
Stream.generate(UUID::randomUUID).limit(5); // 5 UUIDs (UUID มาจาก java.util)
IntStream.range(1, 100); // 1..99 (exclusive — ไม่รวม 100)
IntStream.rangeClosed(1, 100); // 1..100 (inclusive — รวม 100)
Files.lines(Path.of("file.txt")); // stream of lines (Files/Path = อ่านไฟล์ — ดูบทที่ 11)
new BufferedReader(...).lines(); // stream of lines
Pattern.compile(",").splitAsStream("a,b,c"); // split → stream (Pattern = regex — รูปแบบข้อความ เช่น "," คือ comma — ดูบทที่ 13)
new Random().ints(10, 0, 100); // 10 random int in [0,100)Intermediate ops — return Stream
filter(Predicate)— เก็บที่ผ่าน conditionmap(Function)— แปลงแต่ละตัวflatMap— flatten nested streamdistinct()— uniquesorted()— เรียงlimit(n)— เอา n ตัวแรกskip(n)— ข้าม n ตัวแรกpeek(Consumer)— ดูระหว่าง pipeline (debug)
Terminal ops — return ค่า, ปิด stream
count()— return longforEach(Consumer)— ทำกับทุกตัวtoList()(Java 16+) — รวมเป็น Listcollect(Collectors.toList())— แบบเก่าreduce(...)— รวมเป็นค่าเดียวmin/max/sum/average(ของ IntStream/DoubleStream)anyMatch/allMatch/noneMatch— return booleanfindFirst/findAny— return Optional
2.3 ตัวอย่าง pipeline
java
List<String> names = List.of("Anna", "Ben", "Cara", "Dee", "Eve");
// ชื่อที่ยาวเกิน 3 ตัว → uppercase → เรียง
List<String> result = names.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(result); // [ANNA, CARA]2.4 ตัวอย่าง — งานจริง
สมมติมี class Order ที่มี method เหล่านี้:
📝
recordคืออะไร: บางตัวอย่างในบทนี้ใช้record Order(...)เป็นวิธีเขียน class แบบย่อที่ Java เขียน field/constructor/getter ให้อัตโนมัติ — รายละเอียดครบอยู่ใน บทที่ 10 ตอนนี้อ่านออกว่ามี field อะไรก็พอ
java
// สมมติ class Order มีหน้าตาแบบนี้
class Order {
boolean isPaid() { ... }
double getAmount() { ... }
String getCustomerName() { ... }
}
List<Order> orders = ...;
// ราคารวมของ order ที่ยังไม่จ่าย
double total = orders.stream()
.filter(o -> !o.isPaid())
.mapToDouble(Order::getAmount)
.sum();
// ชื่อ customer ของ order ที่จ่ายแล้ว — unique
List<String> customers = orders.stream()
.filter(Order::isPaid)
.map(Order::getCustomerName)
.distinct()
.sorted()
.toList();
// group order ตาม customer
// (groupingBy อธิบายละเอียดใน section 2.5 — ตอนนี้อ่านว่า "จัดกลุ่มตาม getCustomerName" ก็พอ)
Map<String, List<Order>> byCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::getCustomerName));
// count order ของแต่ละ customer
Map<String, Long> counts = orders.stream()
.collect(Collectors.groupingBy(Order::getCustomerName, Collectors.counting()));2.5 Collectors — รวมผลลัพธ์
java
import static java.util.stream.Collectors.*;
stream.collect(toList()); // → List
stream.collect(toSet()); // → Set
stream.collect(toMap(keyFn, valueFn)); // → Map
stream.collect(joining(", ")); // → String ต่อกัน
stream.collect(groupingBy(keyFn)); // group → Map<key, List>
stream.collect(partitioningBy(predicate)); // → Map<Boolean, List>
stream.collect(counting()); // → Long
stream.collect(summingInt(intFn)); // → Integer
stream.collect(averagingDouble(doubleFn)); // → Double2.6 reduce — รวมเป็น 1 ค่า
java
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.stream().reduce(0, Integer::sum); // 15
int product = nums.stream().reduce(1, (a, b) -> a * b); // 120
// ⚠️ ตัวอย่าง String::concat ข้างล่างนี้ "สาธิตหลักการ reduce เท่านั้น"
// อย่าใช้แบบนี้จริงใน production — String immutable (String ใน Java เปลี่ยนค่าไม่ได้)
// ทำให้ทุก String::concat สร้าง String object ใหม่ → O(n²) memory + เวลา
// (1 million strings = 1 million intermediate String object)
String concat = List.of("a", "b", "c").stream()
.reduce("", String::concat); // "abc" ⚠️ anti-pattern
// ✅ ใช้แบบนี้จริง:
String joined = List.of("a", "b", "c").stream()
.collect(Collectors.joining()); // "abc" — ใช้ StringBuilder ภายใน → O(n)อ่าน: "เริ่มจาก 0, รวมทีละตัวโดยใช้ Integer::sum"
2.7 ⚠️ Stream ใช้ครั้งเดียว
java
Stream<Integer> s = nums.stream();
s.count();
s.forEach(System.out::println); // ❌ IllegalStateException — stream ปิดแล้วทำไม? Stream เหมือนน้ำที่ไหลผ่านท่อ — ไหลผ่านแล้วหมด จะย้อนกลับไม่ได้ Terminal operation (เช่น count, forEach, collect) ดึงข้อมูลออกหมดแล้ว → reuse ไม่ได้อีก
🔬 สำหรับผู้อยากรู้ลึก — ทำไม stream reuse ไม่ได้ (ระดับ internal)
Terminal op จะ consume Spliterator ของ stream ก่อน (ตัวท่อง data — อ่านว่า "สปลิ-เท-เรเตอร์" มาจาก Splittable Iterator = ตัววนซ้ำที่แบ่งแยกได้เพื่อประมวลผลแบบขนาน)
พอ consume เสร็จ state ภายในของ stream จะเปลี่ยนเป็น linkedOrConsumed (แปลว่า "ถูกผูกหรือใช้ไปแล้ว") — reuse ไม่ได้อีก
การออกแบบแบบนี้จงใจ เพื่อให้ Stream ทำ lazy fusion ได้ (การรวม intermediate op หลายตัวให้รันใน 1 pass ของ data แทนที่จะรันแยกทีละขั้น — "lazy" คือไม่ทำจนกว่าจะถึง terminal op) ถ้าอนุญาตให้ reuse ต้อง re-traverse ข้อมูลใหม่ ซึ่งขัดกับความหมายของ lazy ที่ตั้งใจไว้
ใช้แบบ chain เสมอ — อย่าเก็บใส่ตัวแปร (ถ้าจำเป็นต้อง iterate ซ้ำ → เรียก collection.stream() ใหม่อีกรอบ หรือ collect ลง List แล้วค่อย stream ใหม่)
2.8 ⚠️ อย่ามี side effect ใน lambda
java
// ❌ side effect — อย่าทำแบบนี้
List<Integer> result = new ArrayList<>();
nums.stream().filter(n -> n > 2).forEach(n -> result.add(n));java
// ✅ ใช้ collect แทน
List<Integer> result = nums.stream().filter(n -> n > 2).toList();🛑 มือใหม่หยุดอ่านตรงนี้ก่อน — ด้านล่างนี้ (
mapMulti,Stream.iterate3-arg,Gatherers, primitive streams ลึก, Collectors ขั้นสูง) เป็น reference ขั้นสูง ข้ามไปอ่าน "Optional" ที่หัวข้อ 3 แล้วกลับมาตอนใช้จริง
2.8.5 mapMulti — flatMap แบบเร็วกว่า (Java 16+)
flatMap สร้าง sub-Stream แล้ว flatten ทุกตัว — มี overhead สำหรับงาน "บางตัว skip, บางตัว emit หลายค่า"
mapMulti ใช้ consumer push ค่าออกมาเองโดยตรง — ไม่ต้องสร้าง Stream กลาง:
java
// (ต้องมี import java.util.List; และ java.util.stream.Stream; ถ้า copy ไปรันแยกไฟล์)
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// flatMap: เลขคู่ → emit n และ n*10
List<Integer> a = nums.stream()
.flatMap(n -> n % 2 == 0 ? Stream.of(n, n * 10) : Stream.empty())
.toList(); // [2, 20, 4, 40]
// mapMulti: เร็วกว่า อ่านง่ายกว่า
// ศัพท์: type witness — syntax .<Integer> ก่อนชื่อ method คือการบอก compiler ตรง ๆ
// ว่า generic type parameter (T) คืออะไร เพราะกรณีนี้ compiler เดา type เองไม่ได้
// ไม่ต้องจำตอนนี้ เจอแล้วรู้ว่าไม่ใช่ error ก็พอ
List<Integer> b = nums.stream()
.<Integer>mapMulti((n, downstream) -> {
if (n % 2 == 0) {
downstream.accept(n);
downstream.accept(n * 10);
}
})
.toList(); // [2, 20, 4, 40]ใช้เมื่อ:
- ต้อง emit 0 หรือหลายค่าต่อ input
- mapping logic ซับซ้อน — เขียนเป็น block อ่านง่ายกว่า nested ternary ใน
flatMap
2.8.6 Stream.iterate 3-arg (Java 9+) — มี exit condition
java
// แบบ 2-arg เก่า — ต้อง .limit() กั้น
Stream.iterate(1, n -> n * 2).limit(10).toList(); // [1, 2, 4, ..., 512]
// แบบ 3-arg ใหม่ — มี hasNext predicate
Stream.iterate(1, n -> n < 1000, n -> n * 2).toList(); // [1, 2, 4, ..., 512]
// เหมือน: for (int n = 1; n < 1000; n *= 2)อ่านง่ายกว่า + เลิก loop ตอนเงื่อนไขจบ ไม่ต้อง .limit() ที่อาจตั้งใหญ่เผื่อ
2.9 parallelStream — multi-thread
java
nums.parallelStream().filter(...).toList();ใช้ thread หลายตัว — เร็วขึ้นเมื่อ data ใหญ่ + operation หนัก
อย่าใช้พร่ำเพรื่อ:
- มี overhead — data เล็กจะช้ากว่า sequential
- ใช้ common ForkJoinPool (กลุ่ม thread สำหรับทำงานขนาน — Java ใช้ pool เดียวกันทั้ง JVM ทำให้ถ้า task หนึ่งใช้ครบแล้ว task อื่นต้องรอ) — task blocking I/O ทำ app อื่นใน JVM พลอยช้า
- default parallelism =
Runtime.availableProcessors() - 1(ค่าเริ่มต้นนี้มีมาตั้งแต่ Java 8) - cgroup คือกลไกของ Linux ที่จำกัด resource (เช่น CPU, memory) ของแต่ละ process — ใช้กันทั่วไปใน Docker/Kubernetes
- Java 10+/11+ เพิ่มความสามารถรับรู้ cgroup ทำให้
Runtime.availableProcessors()คืนค่าตาม CPU limit ของ container แทนค่าของ host จริง - ก่อนพึ่งพา parallelStream ควรตรวจสอบค่านี้ใน container จริงก่อนเสมอ — ถ้า container จอง CPU (CPU request) ไว้น้อยกว่า 1 core, pool จะเหลือเธรดเดียว ทำให้ parallelStream แทบไม่มีประโยชน์
- default parallelism =
- ลำดับผลอาจสลับใน operation ที่ไม่ stateless
- ห้ามใช้กับ shared mutable state (ใช้
Collectors.groupingByConcurrent/toConcurrentMapถ้าจำเป็น)
กฎ: parallel เฉพาะตอน CPU-bound (งานที่ใช้ CPU หนัก เช่น คำนวณ image, เข้ารหัส) + data ใหญ่ (~10k+) + operation เป็น pure function (function ที่ผลลัพธ์ขึ้นอยู่กับ input เท่านั้น ไม่มี side effect เช่น ไม่แก้ตัวแปรภายนอก); I/O-bound (งานที่ต้องรอ เช่น อ่านไฟล์ เรียก API ต่อ database) ให้ใช้ Virtual Threads (เธรดเบาของ Java 21+ — บทที่ 12) + CompletableFuture (กลไกทำงาน async แบบ non-blocking — บทที่ 12) แทน
2.10 Primitive Streams — IntStream / LongStream / DoubleStream
ทำไมมี: Stream<Integer> ต้อง box/unbox ทุกตัว → ช้า + กิน memoryIntStream เก็บ int ดิบ — เร็วกว่ามาก สำหรับงานคำนวณ:
java
import java.util.stream.IntStream;
// สร้าง
IntStream.of(1, 2, 3);
IntStream.range(0, 100); // 0..99
IntStream.rangeClosed(1, 100); // 1..100
IntStream.iterate(1, i -> i * 2).limit(10);
// ดึง aggregate ที่มีเฉพาะ primitive stream
IntStream nums = IntStream.rangeClosed(1, 10);
int sum = nums.sum(); // 55
// (stream ใช้แล้วครั้งเดียว — ต้องสร้างใหม่)
int max = IntStream.rangeClosed(1, 10).max().getAsInt();
double avg = IntStream.rangeClosed(1, 10).average().getAsDouble();
IntSummaryStatistics stats = IntStream.rangeClosed(1, 10).summaryStatistics();
// stats.getCount()/getSum()/getMin()/getMax()/getAverage() ครบในรอบเดียวแปลงไปมา: Stream ↔ IntStream
| operator | ทิศทาง |
|---|---|
Stream<T>.mapToInt(toIntFn) | Stream → IntStream |
Stream<T>.mapToLong/Double | Stream → Long/Double Stream |
IntStream.mapToObj(intToObj) | IntStream → Stream<T> |
IntStream.boxed() | IntStream → Stream<Integer> |
IntStream.asLongStream() / asDoubleStream() | widen |
java
List<String> words = List.of("apple", "banana", "cherry");
// ความยาวรวม
int totalLen = words.stream().mapToInt(String::length).sum();
// IntStream → list ของ string
List<String> labels = IntStream.rangeClosed(1, 5)
.mapToObj(i -> "item-" + i)
.toList(); // [item-1, item-2, ...]
// boxed → ไป Collectors
List<Integer> squared = IntStream.rangeClosed(1, 5)
.map(i -> i * i)
.boxed()
.toList();
LongStreamสำหรับเลขใหญ่ (เกิน int range),DoubleStreamสำหรับ floating point — API เหมือนกัน
2.11 Collectors — เครื่องมือรวมผล (ลึก)
Collectors ใน java.util.stream มีมากกว่าแค่ toList/groupingBy:
Cheat sheet
java
import static java.util.stream.Collectors.*;
// container
toList() // → List (ไม่ guarantee mutable — ถ้าต้องการ mutable ชัด ๆ ใช้ toCollection(ArrayList::new))
toUnmodifiableList() // → unmodifiable List (Java 10+)
toSet() // → Set
toMap(keyFn, valueFn) // → Map (โยน error ถ้า key ซ้ำกัน!)
toMap(keyFn, valueFn, (a, b) -> a) // → Map (จัดการ key ซ้ำ — เก็บตัวแรก)
toMap(keyFn, valueFn, (a, b) -> a, TreeMap::new) // → TreeMap (Map ที่เรียง key)
// reduce
counting() // → Long
summingInt/Long/Double(fn) // → Sum
averagingInt/Long/Double(fn) // → Double
summarizingInt/Long/Double(fn) // → IntSummaryStatistics
minBy(comparator) // → Optional<T>
maxBy(comparator) // → Optional<T>
reducing(identity, fn) // → T
// transform
mapping(fn, downstream) // map ก่อนใส่ downstream
flatMapping(fn, downstream) // flatMap ก่อน (Java 9+)
filtering(predicate, downstream) // filter ก่อน (Java 9+)
collectingAndThen(coll, finisher) // collect → finalize
// joining
joining() // ต่อกัน
joining(", ") // ต่อด้วย ", "
joining(", ", "[", "]") // [a, b, c]
// grouping
groupingBy(keyFn) // Map<K, List<T>>
groupingBy(keyFn, downstream) // Map<K, <downstream>>
groupingBy(keyFn, TreeMap::new, downstream) // Map ที่ต้องการ
partitioningBy(predicate) // Map<Boolean, List<T>>
partitioningBy(predicate, downstream) // Map<Boolean, <downstream>>
// branching (Java 12+)
teeing(coll1, coll2, mergerFn) // collect 2 ผลขนานกันตัวอย่างใช้งานจริง
java
record Employee(String dept, String name, int salary) {}
List<Employee> emps = ...;
// 1. List ชื่อต่อ department
Map<String, List<String>> namesByDept = emps.stream().collect(
groupingBy(Employee::dept, mapping(Employee::name, toList())));
// {ENG=[Anna, Ben], HR=[Cara]}
// 2. salary เฉลี่ยต่อ dept
Map<String, Double> avgSalary = emps.stream().collect(
groupingBy(Employee::dept, averagingInt(Employee::salary)));
// 3. summary stats ต่อ dept
Map<String, IntSummaryStatistics> stats = emps.stream().collect(
groupingBy(Employee::dept, summarizingInt(Employee::salary)));
// stats.get("ENG").getMax(), .getMin(), .getAverage(), ...
// 4. partition (= boolean groupingBy)
Map<Boolean, List<Employee>> seniors = emps.stream().collect(
partitioningBy(e -> e.salary() > 100_000));
List<Employee> senior = seniors.get(true);
List<Employee> junior = seniors.get(false);
// 5. teeing — 2 ผลในรอบเดียว (Java 12+)
record MinMax(int min, int max) {}
MinMax mm = emps.stream().collect(teeing(
minBy(Comparator.comparingInt(Employee::salary)),
maxBy(Comparator.comparingInt(Employee::salary)),
(lo, hi) -> new MinMax(lo.orElseThrow().salary(), hi.orElseThrow().salary())));
// 6. toMap with conflict handler — index by name
Map<String, Employee> byName = emps.stream().collect(
toMap(Employee::name, e -> e, (existing, dup) -> existing));
// 7. joining กับ delimiter + prefix/suffix
String csv = emps.stream().map(Employee::name).collect(joining(",", "[", "]"));
// "[Anna,Ben,Cara]"
// 8. filtering downstream — กรองก่อนใส่ group (Java 9+)
Map<String, List<Employee>> highEarners = emps.stream().collect(
groupingBy(Employee::dept, filtering(e -> e.salary() > 100_000, toList())));
// 9. flatMapping — เมื่อแต่ละ employee มีหลาย skill (Java 9+)
record Emp2(String dept, List<String> skills) {}
List<Emp2> emps2 = List.of(
new Emp2("ENG", List.of("java", "spring")),
new Emp2("HR", List.of("excel")),
new Emp2("ENG", List.of("spring", "docker")));
Map<String, Set<String>> skillsByDept = emps2.stream().collect(
groupingBy(Emp2::dept, flatMapping(e -> e.skills().stream(), toSet())));
// 10. collectingAndThen — wrap ผลให้ immutable
List<String> safe = emps.stream()
.map(Employee::name)
.collect(collectingAndThen(toList(), List::copyOf));💡
toMapระวัง: ถ้า key ซ้ำ + ไม่มี merger →IllegalStateException— เสมอใส่ merger handler
2.12 Gatherers (Java 22+, stable ใน Java 24) — intermediate op แบบ custom
Gatherer = "intermediate op" (ขั้นตอนระหว่างกลางที่คืน stream ใหม่) ที่ user เขียนเองได้ — เทียบกับ Collector ที่เป็น terminal
java
import java.util.stream.Gatherers;
// sliding window (size 3)
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
// [[1,2,3], [2,3,4], [3,4,5]]
// fixed window (size 3, ไม่ overlap)
Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
// [[1,2,3], [4,5,6], [7]]
// fold = stateful reduce ที่ emit แค่ค่าสุดท้าย (เหมือน reduce)
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
// [15] (final sum เท่านั้น — ต่างจาก scan ที่ emit ทุกขั้น)
// scan = fold แต่ emit (ส่งออกค่า) ทุกขั้น
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
// [1, 3, 6, 10, 15] (cumulative sum)ก่อนมี Gatherers — pattern เหล่านี้ทำใน Stream ตรง ๆ ไม่ได้ ต้อง collect แล้วทำต่อ
📌 ใน Java 21- ใช้
Stream.iterate+ indexing เอง หรือใช้ third-party (StreamEx, Guava) แทน
3. Optional — null-safe value
3.1 ปัญหาของ null
java
User u = findUser("123");
System.out.println(u.getName()); // 💥 NPE ถ้า u == null3.2 Optional บอก "อาจไม่มีค่า"
java
import java.util.Optional;
Optional<User> maybe = findUser("123");
if (maybe.isPresent()) {
System.out.println(maybe.get().getName());
}ดีกว่ายังไง: type signature บอกชัด "อาจไม่มี" — เผลอไม่ได้
3.3 สร้าง Optional
java
Optional.of(value) // non-null value (NPE ถ้า value == null)
Optional.ofNullable(value) // value หรือ empty (กัน NPE)
Optional.empty() // ว่าง3.4 Method ที่ใช้บ่อย
java
opt.isPresent() // มีไหม
opt.isEmpty() // ว่างไหม (Java 11+)
opt.get() // ดึงค่า — throw ถ้า empty (เลี่ยง!)
opt.orElse(defaultValue) // ถ้า empty ใช้ default
opt.orElseGet(() -> compute()) // ถ้า empty ค่อยคำนวณ default
opt.orElseThrow() // throw NoSuchElement ถ้า empty
opt.orElseThrow(() -> new MyException()) // throw custom
opt.ifPresent(value -> ...) // ทำถ้ามี
opt.ifPresentOrElse(value -> ..., () -> ...) // ทำถ้ามี/ไม่มี (Java 9+)
opt.map(fn) // แปลงค่า → Optional ใหม่
opt.flatMap(fn) // map ที่ return Optional
opt.filter(predicate) // empty ถ้าไม่ผ่าน3.5 ตัวอย่างใช้งาน
java
public Optional<User> findUser(String id) { ... }
// แบบที่อ่านดี
String name = findUser("123")
.map(User::getName)
.map(String::toUpperCase)
.orElse("UNKNOWN");
// chain แบบลึก
findUser("123")
.flatMap(User::getAddress) // address อาจ null อีก
.map(Address::getCity)
.ifPresent(System.out::println);3.6 Best practice
✅ return Optional จาก method ที่อาจไม่มีค่า — ชัดเจน
✅ อย่า use Optional เป็น field — เปลือง, serialize ยาก (serialize = แปลง object เป็นรูปแบบที่บันทึกหรือส่งได้ เช่น JSON หรือ byte) และผิดเจตนา: Brian Goetz (Java architect) ระบุว่า Optional ออกแบบมาเพื่อใช้เป็น return value เท่านั้น ไม่ใช่ field/parameter
✅ อย่า use Optional เป็น parameter — ใช้ method overload แทน (เช่น void send(String msg) + void send() แทน void send(Optional<String> msg))
✅ อย่าเรียก .get() ตรง ๆ — ใช้ orElse/orElseThrow/ifPresent แทน
3.7 Optional.stream() — กรอง null ใน pipeline (Java 9+)
Optional.stream() return stream ของ 0 หรือ 1 ตัว — ใช้กับ flatMap เพื่อกรอง empty ออก:
java
List<String> ids = List.of("1", "2", "3", "4");
// findUser return Optional<User>
List<User> users = ids.stream()
.map(this::findUser) // this::findUser คือ shorthand ของ x -> this.findUser(x)
// เรียก findUser บน object ปัจจุบัน (this = instance ของ class ที่โค้ดนี้อยู่)
.flatMap(Optional::stream) // Stream<User> — empty หาย
.toList();
// แบบเก่า (Java 8):
List<User> users8 = ids.stream()
.map(this::findUser)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();3.8 or() — fallback Optional (Java 9+)
java
Optional<User> u = findInCache(id)
.or(() -> findInDb(id))
.or(() -> findInRemote(id)); // chain fallbackต่างจาก orElseGet() ตรงที่ or() คืน Optional (ใช้ chain ต่อได้) ส่วน orElseGet() คืนค่าจริง
4. ตัวอย่างรวม — analytics
มาประกอบความรู้เรื่อง Stream เข้าด้วยกันด้วยตัวอย่างวิเคราะห์ข้อมูลออเดอร์จริง — ใช้ filter, map, reduce, groupingBy, summingDouble ร่วมกัน เห็นภาพว่า Stream API จัดการข้อมูลซับซ้อนได้กระชับแค่ไหน:
java
record Order(String customerId, double amount, String category, boolean paid) {}
List<Order> orders = List.of(
new Order("A", 100, "food", true),
new Order("B", 200, "tech", true),
new Order("A", 150, "food", false),
new Order("C", 300, "tech", true),
new Order("B", 50, "food", true)
);
// total revenue (paid only)
double revenue = orders.stream()
.filter(Order::paid)
.mapToDouble(Order::amount)
.sum();
// orders ต่อ category
Map<String, Long> ordersByCategory = orders.stream()
.collect(Collectors.groupingBy(Order::category, Collectors.counting()));
// average per customer (paid only)
Map<String, Double> avgPerCustomer = orders.stream()
.filter(Order::paid)
.collect(Collectors.groupingBy(Order::customerId, Collectors.averagingDouble(Order::amount)));
// top 3 highest orders
List<Order> top3 = orders.stream()
.sorted(Comparator.comparingDouble(Order::amount).reversed())
.limit(3)
.toList();
// most popular category
String popular = orders.stream()
.collect(Collectors.groupingBy(Order::category, Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("none");(record จะอธิบายบทที่ 10)
4.5 ตารางอ้างอิงคำสั่ง Stream (เปิดหาเร็ว)
เปิดหาเร็วเวลาลืมว่า "คำสั่งนี้ใช้ทำอะไร" — จัดกลุ่มตามงานที่ทำ ถ้าอยากดูตัวอย่างเต็ม ๆ แต่ละคำสั่ง ดูที่ส่วน Cookbook ด้านล่าง
กรอง / แปลง (filter / transform)
| คำสั่ง | ใช้ทำอะไร | ตัวอย่างสั้น |
|---|---|---|
filter(x -> ...) | กรองตามเงื่อนไข — เก็บเฉพาะตัวที่ผ่าน | s.filter(n -> n > 0) |
map(x -> ...) | แปลงแต่ละตัว (transform) เป็นค่าใหม่ | s.map(String::length) |
flatMap(x -> ...) | แตก nested list (รายการซ้อนรายการ) ให้แบน (flatten) เป็นชั้นเดียว | s.flatMap(List::stream) |
mapToInt(x -> ...) | แปลงเป็น IntStream เพื่อใช้ sum()/average() ได้ | s.mapToInt(String::length) |
distinct() | เอาเฉพาะตัวไม่ซ้ำ (unique) | s.distinct() |
peek(x -> ...) | แอบดู (peek) ระหว่างทาง — มักใช้ debug ไม่เปลี่ยนค่า | s.peek(System.out::println) |
เรียง / ตัด (sort / slice)
| คำสั่ง | ใช้ทำอะไร | ตัวอย่างสั้น |
|---|---|---|
sorted() | เรียงน้อย → มาก (natural order) | s.sorted() |
sorted(Comparator.comparing(...)) | เรียงตาม field ที่เลือก | s.sorted(comparing(Person::age)) |
.reversed() | กลับลำดับ comparator → เรียงมาก → น้อย | comparing(Person::age).reversed() |
limit(n) | เอา n ตัวแรก (จำกัดจำนวน) | s.limit(3) |
skip(n) | ข้าม n ตัวแรก | s.skip(2) |
รวมกลุ่ม / สรุป (group / summarize)
ใช้ใน
collect(...)— ต้องimport static java.util.stream.Collectors.*;หรือนำหน้าด้วยCollectors.
| คำสั่ง | ใช้ทำอะไร | ตัวอย่างสั้น |
|---|---|---|
groupingBy(keyFn) | จัดกลุ่ม (group) ตาม key → Map<K, List<T>> | groupingBy(Person::city) |
partitioningBy(pred) | แบ่งเป็น 2 กลุ่ม (true/false) → Map<Boolean, List<T>> | partitioningBy(p -> p.age() >= 18) |
counting() | นับจำนวนในกลุ่ม | groupingBy(Person::city, counting()) |
summingInt(fn) / summingDouble(fn) | ผลรวม (sum) ของ field ในกลุ่ม | groupingBy(Person::city, summingInt(Person::age)) |
averagingInt(fn) | ค่าเฉลี่ย (average) ของ field ในกลุ่ม | groupingBy(Person::city, averagingInt(Person::age)) |
joining(", ") | ต่อ string เข้าด้วยกัน คั่นด้วย , | map(Person::name).collect(joining(", ")) |
ดึงค่า / ตรวจสอบ (find / check)
| คำสั่ง | ใช้ทำอะไร | ตัวอย่างสั้น |
|---|---|---|
findFirst() | เอาตัวแรก → คืน Optional (อาจไม่มี) | s.filter(...).findFirst() |
findAny() | เอาตัวไหนก็ได้ → Optional (เร็วกว่าใน parallel) | s.findAny() |
anyMatch(pred) | มีอย่างน้อย 1 ตัวตรงเงื่อนไขไหม → boolean | s.anyMatch(p -> p.age() > 60) |
allMatch(pred) | ทุกตัวตรงเงื่อนไขไหม → boolean | s.allMatch(p -> p.age() >= 0) |
noneMatch(pred) | ไม่มีตัวไหนตรงเงื่อนไขเลยใช่ไหม → boolean | s.noneMatch(p -> p.age() < 0) |
count() | นับจำนวนทั้งหมด → long | s.filter(...).count() |
min(comparator) / max(comparator) | ตัวน้อยสุด / มากสุด → Optional | s.max(comparing(Person::age)) |
mapToInt(fn).sum() | รวมผลบวก (sum) ของ field | s.mapToInt(Person::age).sum() |
รวมผลกลับ (collect back)
| คำสั่ง | ใช้ทำอะไร | ตัวอย่างสั้น |
|---|---|---|
toList() | รวมเป็น List unmodifiable (Java 16+) — ถ้าต้องการ mutable ใช้ collect(Collectors.toCollection(ArrayList::new)) | s.toList() |
collect(Collectors.toSet()) | รวมเป็น Set (ตัดตัวซ้ำ) | s.collect(toSet()) |
collect(Collectors.toMap(k, v)) | รวมเป็น Map กำหนด key/value เอง | s.collect(toMap(Person::name, Person::age)) |
reduce(...) | ยุบเหลือค่าเดียว (เช่น ผลรวม, ต่อ string) | s.reduce(0, Integer::sum) |
4.6 Cookbook: งานที่เจอบ่อย (คัดลอกไปใช้ได้)
ตัวอย่างต่อไปนี้ใช้ record Person ตัวเดียวกันทั้งหมด — copy ไปปรับใช้ได้เลย:
java
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
record Person(String name, int age, String city) {}
List<Person> people = List.of(
new Person("Anna", 30, "Bangkok"),
new Person("Ben", 25, "Chiang Mai"),
new Person("Cara", 35, "Bangkok"),
new Person("Dee", 28, "Phuket"),
new Person("Eve", 42, "Chiang Mai")
);1. กรองแล้วเอาเฉพาะบาง field → list
java
// เอา "ชื่อ" ของคนที่อายุ >= 30 (filter + map + toList)
List<String> names = people.stream()
.filter(p -> p.age() >= 30) // กรองตามเงื่อนไข
.map(Person::name) // ดึงเฉพาะ field name
.toList();
System.out.println(names); // [Anna, Cara, Eve]2. เรียงจากมากไปน้อย เอา Top N
java
// 2 คนที่อายุมากสุด (sorted reversed + limit)
List<Person> top2 = people.stream()
.sorted(Comparator.comparingInt(Person::age).reversed()) // มาก → น้อย
.limit(2) // เอา 2 ตัวแรก
.toList();
top2.forEach(p -> System.out.println(p.name() + " " + p.age()));
// Eve 42
// Cara 353. หาตัวแรกที่ตรงเงื่อนไข
java
// คนแรกที่อยู่ Bangkok (filter + findFirst + Optional)
Optional<Person> found = people.stream()
.filter(p -> p.city().equals("Bangkok"))
.findFirst();
// ดึงค่าออกจาก Optional อย่างปลอดภัย
String name = found.map(Person::name).orElse("ไม่พบ");
System.out.println(name); // Anna4. นับจำนวนตามกลุ่ม — "นับคนต่อเมือง"
java
// นับว่าแต่ละเมืองมีกี่คน (groupingBy + counting)
Map<String, Long> countByCity = people.stream()
.collect(groupingBy(Person::city, counting()));
System.out.println(countByCity);
// {Bangkok=2, Chiang Mai=2, Phuket=1}5. รวมผลบวกตามกลุ่ม — "ผลรวมอายุต่อเมือง"
java
// บวกอายุของทุกคนในแต่ละเมือง (groupingBy + summingInt)
Map<String, Integer> ageSumByCity = people.stream()
.collect(groupingBy(Person::city, summingInt(Person::age)));
System.out.println(ageSumByCity);
// {Bangkok=65, Chiang Mai=67, Phuket=28}6. ผลรวม / ค่าเฉลี่ย / มาก-น้อยสุด ของทั้ง list
java
// วิธีง่าย: ดึงทุกค่าในรอบเดียวด้วย summaryStatistics
IntSummaryStatistics stats = people.stream()
.mapToInt(Person::age)
.summaryStatistics();
System.out.println(stats.getSum()); // 160 (ผลรวม)
System.out.println(stats.getAverage()); // 32.0 (ค่าเฉลี่ย)
System.out.println(stats.getMax()); // 42 (มากสุด)
System.out.println(stats.getMin()); // 25 (น้อยสุด)
// ถ้าอยากได้ "ทั้ง object" ที่อายุมากสุด (ไม่ใช่แค่ตัวเลข) ใช้ max(...)
Optional<Person> oldest = people.stream()
.max(Comparator.comparingInt(Person::age));
System.out.println(oldest.map(Person::name).orElse("-")); // Eve7. แปลง list → Map (key=name, value=age)
java
// ทำ list ให้กลายเป็น Map ค้นหาเร็ว (toMap)
Map<String, Integer> ageByName = people.stream()
.collect(toMap(Person::name, Person::age));
System.out.println(ageByName.get("Cara")); // 35⚠️ ถ้า key ซ้ำกัน
toMapจะ throwIllegalStateException— ถ้าเป็นไปได้ที่จะซ้ำ ใส่ตัวจัดการตัวที่ 3:toMap(Person::name, Person::age, (a, b) -> a)(เก็บตัวแรก)
8. เช็คว่ามี / ทั้งหมดตรงเงื่อนไขไหม
java
// มีใครอายุเกิน 40 ไหม? (anyMatch)
boolean hasSenior = people.stream().anyMatch(p -> p.age() > 40);
System.out.println(hasSenior); // true
// ทุกคนเป็นผู้ใหญ่ (อายุ >= 18) ไหม? (allMatch)
boolean allAdults = people.stream().allMatch(p -> p.age() >= 18);
System.out.println(allAdults); // true
// ไม่มีใครอายุติดลบเลยใช่ไหม? (noneMatch)
boolean noNegative = people.stream().noneMatch(p -> p.age() < 0);
System.out.println(noNegative); // true5. Checkpoint
🛠️ Checkpoint 9.1 — Filter + map
มี list ของชื่อ — return list ของ "Hello, X!" เฉพาะชื่อที่ขึ้นต้นด้วย "A"
📖 เฉลย
java
List<String> names = List.of("Anna", "Ben", "Alex", "Cara");
List<String> result = names.stream()
.filter(n -> n.startsWith("A"))
.map(n -> "Hello, " + n + "!")
.toList();
// [Hello, Anna!, Hello, Alex!]🛠️ Checkpoint 9.2 — Word frequency
นับว่าแต่ละคำปรากฏกี่ครั้งใน string — ใช้ stream (หมายเหตุ: checkpoint 7.1 ทำแบบเดียวกันโดยใช้ for-loop + Map ธรรมดา ถ้ายังไม่เคยทำก็ไม่เป็นไร — ทำ checkpoint นี้ได้เลยโดยไม่ต้องกลับไปดู)
📖 เฉลย
java
String text = "the quick brown fox the lazy dog the";
Map<String, Long> freq = Arrays.stream(text.split(" "))
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
System.out.println(freq);🛠️ Checkpoint 9.3 — Optional chain
มี findUser(id) -> Optional<User> และ User มี getEmail() -> Optional<String> เขียน method getUserEmailUppercase(id) return String — เป็น "NONE" ถ้าไม่เจอ user หรือไม่มี email
📖 เฉลย
java
public static String getUserEmailUppercase(String id) {
return findUser(id)
.flatMap(User::getEmail)
.map(String::toUpperCase)
.orElse("NONE");
}🛠️ Checkpoint 9.4 — Sum of squares
หา sum ของ squares ของเลข 1-100 ที่หารด้วย 3 ลงตัว หา formula: 3² + 6² + 9² + ... + 99²
📖 เฉลย
java
int result = IntStream.rangeClosed(1, 100)
.filter(n -> n % 3 == 0)
.map(n -> n * n)
.sum();
System.out.println(result); // 1127616. สรุปบท
✅ Lambda (params) -> { body } — function สั้น ๆ ที่ส่งไปมาได้
✅ Functional interface = interface ที่มี abstract method เดียว (@FunctionalInterface)
✅ Java เตรียม Predicate, Function, Consumer, Supplier, BiFunction, UnaryOperator ฯลฯ
✅ Method reference 4 แบบ: Class::static, Class::instance, obj::method, Class::new
✅ Stream pipeline: source → intermediate (lazy) → terminal
✅ Primitive streams (IntStream/LongStream/DoubleStream) — ไม่มี boxing → เร็วกว่า + มี sum/max/avg/summaryStatistics
✅ Collectors ขั้นสูง — groupingBy(k, mapping(...)), partitioningBy, teeing, filtering, flatMapping, collectingAndThen
✅ Stream.iterate (3-arg) มี exit condition; Stream.generate สำหรับ generator; mapMulti เร็วกว่า flatMap ในบางกรณี
✅ Gatherers (Java 22+) — window, fold, scan; custom intermediate op
✅ parallelStream ใช้ common ForkJoinPool — ใช้เฉพาะ CPU-bound + data ใหญ่; I/O ใช้ Virtual Threads
✅ Optional แทน null — map/flatMap/filter, orElse/orElseGet/orElseThrow, ifPresent/ifPresentOrElse
✅ Optional.stream() กรอง empty ใน pipeline (Java 9+); or() chain fallback Optional
✅ อย่าเรียก .get() ตรง ๆ บน Optional; อย่าใช้ Optional เป็น field/parameter
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-23