Skip to content

บทที่ 9 — Lambda, Stream, Optional

← บทที่ 8 | สารบัญ | บทที่ 10: Modern Java →

🟡 หมายเหตุระดับ: บทนี้เริ่มจากพื้นฐาน (lambda, basic stream, Optional) แต่จบที่ระดับกลาง-สูง — ส่วน Gatherers (Java 22+), mapMulti, teeing, Stream.iterate 3-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 อื่น ๆ ที่ implement Comparable) ที่เทียบสองค่า:

  • คืน < 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/static method เพิ่มได้ตามต้องการ (ดู java/06c) เป็นเหตุที่ Predicate<T> มี method and(), or(), negate() เพิ่มด้วยแต่ยังเป็น functional interface

Compiler จะ match lambda กับ functional interface โดยดูจาก abstract method เดียวนั้น → จึงรู้ว่า s -> s.length() ตรงกับ Function.apply(T)

1.5 Functional interface ที่ Java เตรียมไว้ (java.util.function)

InterfaceMethodใช้เมื่อ
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::new

2. 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) — เก็บที่ผ่าน condition
  • map(Function) — แปลงแต่ละตัว
  • flatMap — flatten nested stream
  • distinct() — unique
  • sorted() — เรียง
  • limit(n) — เอา n ตัวแรก
  • skip(n) — ข้าม n ตัวแรก
  • peek(Consumer) — ดูระหว่าง pipeline (debug)

Terminal ops — return ค่า, ปิด stream

  • count() — return long
  • forEach(Consumer) — ทำกับทุกตัว
  • toList() (Java 16+) — รวมเป็น List
  • collect(Collectors.toList()) — แบบเก่า
  • reduce(...) — รวมเป็นค่าเดียว
  • min/max/sum/average (ของ IntStream/DoubleStream)
  • anyMatch/allMatch/noneMatch — return boolean
  • findFirst/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));   // → Double

2.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.iterate 3-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 แทบไม่มีประโยชน์
  • ลำดับผลอาจสลับใน 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 ทุกตัว → ช้า + กิน memory
IntStream เก็บ 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/DoubleStream → 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 == null

3.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 ตัวตรงเงื่อนไขไหม → booleans.anyMatch(p -> p.age() > 60)
allMatch(pred)ทุกตัวตรงเงื่อนไขไหม → booleans.allMatch(p -> p.age() >= 0)
noneMatch(pred)ไม่มีตัวไหนตรงเงื่อนไขเลยใช่ไหม → booleans.noneMatch(p -> p.age() < 0)
count()นับจำนวนทั้งหมด → longs.filter(...).count()
min(comparator) / max(comparator)ตัวน้อยสุด / มากสุด → Optionals.max(comparing(Person::age))
mapToInt(fn).sum()รวมผลบวก (sum) ของ fields.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 35

3. หาตัวแรกที่ตรงเงื่อนไข

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);   // Anna

4. นับจำนวนตามกลุ่ม — "นับคนต่อเมือง"

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("-"));   // Eve

7. แปลง 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 จะ throw IllegalStateException — ถ้าเป็นไปได้ที่จะซ้ำ ใส่ตัวจัดการตัวที่ 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);  // true

5. 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);   // 112761

6. สรุปบท

✅ 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: sourceintermediate (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

→ ไปบทที่ 10: Modern Java


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-23