Stream API และ Functional Programming ใน Java

การพัฒนาโปรแกรมเชิงวัตถุในยุคใหม่ไม่ได้หยุดเพียงแค่การออกแบบ Class และ Object เท่านั้น แต่ยังรวมถึงแนวทางการเขียนเชิงฟังก์ชัน (Functional Programming) ที่ช่วยให้โค้ดมีรูปแบบเรียบง่าย อ่านเข้าใจง่าย และขยายความสามารถของโปรแกรมได้อย่างยืดหยุ่น

ตั้งแต่ Java 8 เป็นต้นมา ภาษานี้ได้เปิดตัว Stream API และ Lambda Expression เพื่อรองรับแนวคิดแบบ Functional Programming ทำให้การจัดการข้อมูลใน Collection มีความ “เป็นธรรมชาติ” มากขึ้น สามารถประมวลผลข้อมูลได้ในรูปแบบ “ลำธารของข้อมูล” (data stream)


Stream API คืออะไร

Stream API คือกลุ่มคลาสและอินเตอร์เฟซที่ช่วยให้เราสามารถประมวลผลข้อมูลใน Collection หรือ Array ได้ในเชิงประกาศ (declarative style) แทนที่จะใช้การวนลูปแบบ imperative ที่คุ้นเคยในอดีต

ตัวอย่างเชิงเปรียบเทียบ

โค้ดแบบเดิม (Imperative):

javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = new ArrayList<>();

for (String name : names) {
    if (name.startsWith("C")) {
        result.add(name.toUpperCase());
    }
}
System.out.println(result);

โค้ดแบบใช้ Stream API:

javaList<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = names.stream()
                           .filter(n -> n.startsWith("C"))
                           .map(String::toUpperCase)
                           .toList();

System.out.println(result);

คำอธิบาย

ความแตกต่างหลักระหว่างสองวิธีนี้คือ:

  • วิธีเดิม ต้องสร้าง List ใหม่และวนลูปด้วยตัวเอง ต้องควบคุมทุกขั้นตอนของการประมวลผล (imperative)
  • วิธี Stream API เขียนคำสั่งบอกสิ่งที่ต้องการให้เกิดขึ้น โดยไม่ต้องกำหนดวิธีทำทีละขั้น (declarative)

โค้ดแบบ Stream ทำให้:

  1. โค้ดสั้นลง อ่านง่าย
  2. ลดโอกาสเกิดข้อผิดพลาดเนื่องจากลด boilerplate code
  3. นำเสนอเจตนาของโปรแกรมอย่างชัดเจน

แนวคิดของ Functional Programming

Functional Programming (FP) คือแนวคิดที่มองโปรแกรมเป็นผลลัพธ์ของการประมวลผลฟังก์ชันแทนการเปลี่ยนสถานะของตัวแปรเหมือนในแนวทางเชิงคำสั่ง (Imperative Programming)

ลักษณะเฉพาะของ Functional Programming

  • ใช้ ฟังก์ชัน เป็นองค์ประกอบหลักของการคำนวณ และสามารถส่งผ่านระหว่างฟังก์ชันอื่นได้
  • หลีกเลี่ยงการเปลี่ยนค่าตัวแปร (immutable data) โดยแทนที่จะแก้ไขข้อมูลเดิม เราสร้างข้อมูลใหม่แทน
  • ฟังก์ชันสามารถคืนค่าหรือรับ ฟังก์ชันอื่นเป็นพารามิเตอร์ได้
  • ลด side effect ผลข้างเคียงที่ไม่พึงประสงค์ของโปรแกรม

ตัวอย่างฟังก์ชันเชิงฟังก์ชัน

javaFunction<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> cube = x -> x * x * x;

System.out.println(square.apply(5));  // 25
System.out.println(cube.apply(3));    // 27

ที่นี่เราเห็นว่า Function เป็น “ออบเจ็กต์ของพฤติกรรม” (object of behavior) ซึ่งสามารถเก็บหรือส่งต่อได้เช่นเดียวกับค่าทั่วไป เหมือนเป็นตัวแปรที่เก็บสูตรการคำนวณไว้


Lambda Expression

Lambda Expression คือรูปแบบย่อของการเขียนคลาสนิรนาม (anonymous class) ที่มีเพียงเมธอดเดียว เช่นใน Functional Interface อย่าง FunctionPredicate, หรือ Consumer

เหตุผลของการใช้ Lambda

ก่อนที่ Java 8 จะเปิดตัว Lambda Expression ถ้าต้องการส่งพฤติกรรม (behavior) ให้ฟังก์ชัน จำเป็นต้องสร้างคลาสนิรนามทั้งหมด ซึ่งเป็นการเขียนโค้ดที่ยาวและ verbose

ตัวอย่างเปรียบเทียบ

แบบเดิม (Anonymous Class):

javaComparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
};

แบบ Lambda Expression:

javaComparator<String> comp = (a, b) -> a.compareTo(b);

คำอธิบาย

Lambda Expression มีรูปแบบพื้นฐาน:

text(parameters) -> expression

ตัวอย่างต่างๆ:

java// ไม่มีพารามิเตอร์
Runnable r = () -> System.out.println("Hello");

// มีพารามิเตอร์หนึ่ง
Consumer<String> print = s -> System.out.println(s);

// มีพารามิเตอร์หลายตัว
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// Body มีหลายบรรทัด ต้องใช้ {}
Function<Integer, Integer> compute = x -> {
    int result = x * 2;
    return result + 10;
};

Functional Interface

Lambda Expression ทำงานได้เพราะอาศัย Functional Interface ซึ่งเป็นอินเตอร์เฟซที่มีเพียงเมธอดนามธรรมเดียว (single abstract method)

ตัวอย่างของ Functional Interface:

java@FunctionalInterface
public interface Processor {
    String process(String input);
}

// ใช้ Lambda แทนการสร้างคลาส
Processor p = str -> str.toUpperCase();
System.out.println(p.process("hello"));  // HELLO

Annotation @FunctionalInterface ช่วยให้ compiler ตรวจสอบว่าอินเตอร์เฟซเป็น functional interface จริง


การใช้ Stream Pipeline

ลำดับขั้นของ Stream API เรียกว่า Pipeline ซึ่งโดยทั่วไปประกอบด้วย 3 ขั้นตอนดังนี้

สามขั้นตอนหลักของ Pipeline

  1. Source – แหล่งข้อมูล เช่น List, Set, Array หรือแม้แต่ลำนำจากเซ็นเซอร์
  2. Intermediate Operations – การประมวลผลกลาง เช่น filter()map()sorted() อาจมีหลายขั้นตอนหรือไม่มีเลยก็ได้
  3. Terminal Operations – ปิดกระบวนการและสร้างผลลัพธ์ เช่น forEach()collect()count()

โครงสร้างการทำงาน Stream Pipeline ใน Java

ตัวอย่าง Pipeline

javaList<String> words = List.of("java", "functional", "stream", "programming");

long count = words.stream()                    // Source
                  .filter(w -> w.length() > 5)  // Intermediate
                  .map(String::toUpperCase)     // Intermediate
                  .count();                     // Terminal

System.out.println(count);  // 3

คำอธิบายลำดับการทำงาน

  1. stream() แปลง List ["java", "functional", "stream", "programming"] เป็น Stream
  2. filter(w -> w.length() > 5) กรองเฉพาะคำที่ยาวกว่า 5 ตัว → ["functional", "stream", "programming"]
  3. map(String::toUpperCase) แปลงเป็นตัวพิมพ์ใหญ่ทั้งหมด → ["FUNCTIONAL", "STREAM", "PROGRAMMING"]
  4. count() นับจำนวนรายการ → 3

Lazy Evaluation ในStream

แนวคิดที่สำคัญในการทำงานของ Stream คือ Lazy Evaluation หมายถึงว่า Intermediate Operations จะไม่ทำงานจนกว่ามี Terminal Operation

เหตุผลของ Lazy Evaluation

  • ประสิทธิภาพ: ถ้าในกลางทาง Terminal Operation ต้องการเพียง 10 รายการจากจำนวน 1 ล้านรายการ Stream จะหยุดลงเมื่อได้ 10 รายการ แทนที่จะประมวลผลทั้ง 1 ล้าน
  • ความสามารถในการเขียน: สามารถสร้าง infinite stream ได้

ตัวอย่างเปรียบเทียบ

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

// นี่จะไม่พิมพ์อะไร เพราะ intermediate operations ยังไม่ทำงาน
numbers.stream()
       .filter(n -> {
           System.out.println("Filtering: " + n);
           return n > 2;
       })
       .map(n -> {
           System.out.println("Mapping: " + n);
           return n * 2;
       });

// สิ่งนี้จะพิมพ์เนื่องจาก foreach เป็น terminal operation
numbers.stream()
       .filter(n -> {
           System.out.println("Filtering: " + n);
           return n > 2;
       })
       .map(n -> {
           System.out.println("Mapping: " + n);
           return n * 2;
       })
       .forEach(System.out::println);

Output:

textFiltering: 1
Filtering: 2
Filtering: 3
Mapping: 3
6
Filtering: 4
Mapping: 4
8
Filtering: 5
Mapping: 5
10

อธิบายผลการทำงาน

ประมวลผลข้อมูลทีละรายการ (element-by-element) ไม่ใช่ทีละ operation

  1. ตรวจสอบ 1: 1 > 2 เป็นเท็จ ข้ามไป
  2. ตรวจสอบ 2: 2 > 2 เป็นเท็จ ข้ามไป
  3. ตรวจสอบ 3: 3 > 2 เป็นจริง → Mapping 3 → ได้ 6 → พิมพ์
  4. … ทำเช่นเดียวกันกับ 4 และ 5

Lazy Evaluation ใน Stream Pipeline


Common Intermediate Operations

Intermediate Operations เป็นหน้าที่ของการประมวลผลข้อมูล สามารถเชื่อมต่อหลายตัวได้

filter() – การกรองข้อมูล

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .toList();

System.out.println(evenNumbers);  // [2, 4, 6]

filter() รับเงื่อนไข (predicate) และคืนเฉพาะรายการที่ผ่านเงื่อนไข

map() – การแปลงรูปแบบ

javaList<String> words = List.of("java", "stream", "api");

List<Integer> lengths = words.stream()
                             .map(String::length)
                             .toList();

System.out.println(lengths);  // [4, 6, 3]

map() แปลง Stream ของ type ใหม่โดยใช้ function ที่ให้ไป

distinct() – ลบรายการซ้ำ

javaList<Integer> numbers = List.of(1, 2, 2, 3, 3, 3);

List<Integer> unique = numbers.stream()
                              .distinct()
                              .toList();

System.out.println(unique);  // [1, 2, 3]

sorted() – เรียงลำดับ

javaList<String> names = List.of("Charlie", "Alice", "Bob");

List<String> sorted = names.stream()
                           .sorted()
                           .toList();

System.out.println(sorted);  // [Alice, Bob, Charlie]

สามารถเรียงลำดับแบบ custom ได้ด้วย:

javaList<String> descending = names.stream()
                               .sorted((a, b) -> b.compareTo(a))
                               .toList();

System.out.println(descending);  // [Charlie, Bob, Alice]

flatMap() – การรวม Stream

javaList<List<Integer>> nested = List.of(
    List.of(1, 2),
    List.of(3, 4),
    List.of(5, 6)
);

List<Integer> flat = nested.stream()
                           .flatMap(List::stream)
                           .toList();

System.out.println(flat);  // [1, 2, 3, 4, 5, 6]

flatMap() ช่วยในการ “ปูดออก” nested structure

limit() และ skip()

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// เอาเพียง 3 รายการแรก
List<Integer> first3 = numbers.stream()
                              .limit(3)
                              .toList();
System.out.println(first3);  // [1, 2, 3]

// ข้ามไป 5 รายการ แล้วเอาส่วนที่เหลือ
List<Integer> skip5 = numbers.stream()
                             .skip(5)
                             .toList();
System.out.println(skip5);  // [6, 7, 8, 9, 10]

Common Terminal Operations

Terminal Operations ปิดการประมวลผล Stream และสร้างผลลัพธ์ขั้นสุดท้าย

forEach() – วนลูปแต่ละรายการ

javaList<String> names = List.of("Alice", "Bob", "Charlie");

names.stream()
     .forEach(System.out::println);

// Output:
// Alice
// Bob
// Charlie

collect() – รวบรวมผลลัพธ์

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

List<Integer> doubled = numbers.stream()
                               .map(n -> n * 2)
                               .collect(Collectors.toList());

System.out.println(doubled);  // [2, 4, 6, 8, 10]

Collectors มีเมธอดช่วยมากมาย:

java// เป็น Set
Set<Integer> uniqueNumbers = List.of(1, 1, 2, 2, 3)
                                 .stream()
                                 .collect(Collectors.toSet());

// เป็น String (join)
String joined = List.of("Hello", "World")
                    .stream()
                    .collect(Collectors.joining(", "));
System.out.println(joined);  // "Hello, World"

// จัดกลุ่ม (grouping)
Map<Integer, List<String>> byLength = 
    List.of("a", "bb", "ccc", "dd")
        .stream()
        .collect(Collectors.groupingBy(String::length));

count() – นับจำนวน

javalong count = List.of(1, 2, 3, 4, 5)
                 .stream()
                 .filter(n -> n > 2)
                 .count();

System.out.println(count);  // 3

findFirst() และ findAny()

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

// หาค่าแรก
Optional<Integer> first = numbers.stream()
                                 .filter(n -> n > 2)
                                 .findFirst();

System.out.println(first.get());  // 3

// หาค่าใดค่าหนึ่ง (อาจใช้ได้ดีกับ parallel stream)
Optional<Integer> any = numbers.stream()
                               .filter(n -> n > 2)
                               .findAny();

System.out.println(any.get());  // 3 (หรือค่าอื่น)

anyMatch(), allMatch(), noneMatch()

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

// มีรายการใดบ้าง > 3
boolean hasGreater = numbers.stream()
                            .anyMatch(n -> n > 3);
System.out.println(hasGreater);  // true

// ทั้งหมดเป็นจำนวนบวก
boolean allPositive = numbers.stream()
                             .allMatch(n -> n > 0);
System.out.println(allPositive);  // true

// ไม่มีรายการใด > 10
boolean noneGreater = numbers.stream()
                             .noneMatch(n -> n > 10);
System.out.println(noneGreater);  // true

เทคนิคการรวมค่า (Reduction)

Reduction (หรือ fold) คือการรวบรวมข้อมูลทั้งหมดใน Stream เป็นค่าเดียว เช่น การหาผลรวม ผลคูณ หรือค่ามากสุด/น้อยสุด

reduce() – การรวมค่าทั่วไป

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

// ผลรวม
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);
System.out.println(sum);  // 15

// ผลคูณ
int product = numbers.stream()
                     .reduce(1, (a, b) -> a * b);
System.out.println(product);  // 120

// ค่ามากสุด
int max = numbers.stream()
                 .reduce(Integer.MIN_VALUE, Math::max);
System.out.println(max);  // 5

// ค่าน้อยสุด
int min = numbers.stream()
                 .reduce(Integer.MAX_VALUE, Math::min);
System.out.println(min);  // 1

ความหมายของ reduce(identity, accumulator)

  • identity: ค่าเริ่มต้น (ค่า neutral เช่น 0 สำหรับการบวก, 1 สำหรับการคูณ)
  • accumulator: ฟังก์ชันที่รวมค่าสองค่าเข้าด้วยกัน

reduce() แบบไม่มี identity

javaList<Integer> numbers = List.of(1, 2, 3, 4, 5);

Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a + b);

if (sum.isPresent()) {
    System.out.println(sum.get());  // 15
}

คืนค่าเป็น Optional เพราะ Stream อาจว่างเปล่า

min() และ max() – shorthand

javaList<Integer> numbers = List.of(5, 2, 8, 1, 9);

int max = numbers.stream()
                 .max(Integer::compare)
                 .get();

int min = numbers.stream()
                 .min(Integer::compare)
                 .get();

System.out.println("Max: " + max);  // Max: 9
System.out.println("Min: " + min);  // Min: 1

การนำไปประยุกต์ใช้จริง

ในโปรแกรมขนาดใหญ่ Stream API นิยมใช้กับข้อมูลจากฐานข้อมูล, ข้อมูลไฟล์, หรือ API ต่างๆ เพื่อประมวลผลข้อมูลเชิงประกาศ เช่น การกรอง สรุปค่า และแปลงรูปแบบก่อนนำไปใช้งานต่อ

ตัวอย่างการใช้ในการวิเคราะห์ข้อมูล

javaclass Employee {
    private String name;
    private String department;
    private double salary;

    // Constructor และ getter
    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getDepartment() { return department; }
    public double getSalary() { return salary; }
    public String getName() { return name; }
}

List<Employee> employees = List.of(
    new Employee("Alice", "IT", 50000),
    new Employee("Bob", "IT", 55000),
    new Employee("Charlie", "HR", 45000),
    new Employee("David", "IT", 60000),
    new Employee("Eve", "HR", 48000)
);

// หาเงินเดือนเฉลี่ยของแผนก IT
double avgSalary = employees.stream()
                            .filter(e -> e.getDepartment().equals("IT"))
                            .mapToDouble(Employee::getSalary)
                            .average()
                            .orElse(0);

System.out.println("Average salary in IT: " + avgSalary);  // 55000.0

// หาพนักงานใน IT ที่มีเงินเดือน > 52000
List<String> highEarners = employees.stream()
                                    .filter(e -> e.getDepartment().equals("IT"))
                                    .filter(e -> e.getSalary() > 52000)
                                    .map(Employee::getName)
                                    .toList();

System.out.println(highEarners);  // [Bob, David]

ตัวอย่างการประมวลผลข้อมูลจากไฟล์

java// อ่านจากไฟล์ และประมวลผลข้อมูล
Files.lines(Paths.get("data.txt"))
     .filter(line -> !line.isEmpty())
     .map(String::trim)
     .map(String::toUpperCase)
     .forEach(System.out::println);

ตัวอย่างการจัดกลุ่มข้อมูล

javaMap<String, List<Employee>> byDepartment = 
    employees.stream()
             .collect(Collectors.groupingBy(Employee::getDepartment));

// พิมพ์แต่ละแผนก
byDepartment.forEach((dept, emps) -> {
    System.out.println(dept + ": " + 
        emps.stream()
            .map(Employee::getName)
            .collect(Collectors.joining(", ")));
});

// Output:
// IT: Alice, Bob, David
// HR: Charlie, Eve

เปรียบเทียบ Imperative vs Functional

เพื่อให้เข้าใจความแตกต่างและประโยชน์ของ Functional Programming ลองดูตัวอย่างนี้:

ตัวอย่างการคำนวณ: หา Top 3 เงินเดือนสูงสุดของแผนก IT

แบบ Imperative:

javaList<Employee> topThree = new ArrayList<>();
for (Employee e : employees) {
    if (e.getDepartment().equals("IT")) {
        topThree.add(e);
    }
}

// เรียงลำดับแบบจากสูงไปต่ำ
topThree.sort(new Comparator<Employee>() {
    @Override
    public int compare(Employee a, Employee b) {
        return Double.compare(b.getSalary(), a.getSalary());
    }
});

// เอาเพียง 3 คน
if (topThree.size() > 3) {
    topThree = topThree.subList(0, 3);
}

// พิมพ์ผลลัพธ์
for (Employee e : topThree) {
    System.out.println(e.getName() + ": " + e.getSalary());
}

แบบ Functional:

javaemployees.stream()
         .filter(e -> e.getDepartment().equals("IT"))
         .sorted((a, b) -> Double.compare(b.getSalary(), a.getSalary()))
         .limit(3)
         .forEach(e -> System.out.println(e.getName() + ": " + e.getSalary()));

ชัดเจนถึงความแตกต่าง: โค้ดแบบ Functional เรียบร้อย และบ่งบอกเจตนาได้ชัดขึ้น


Method Reference

Method Reference เป็นวิธีอื่นในการสั้นโค้ด Lambda มายิ่งขึ้น ใช้ :: เพื่ออ้างอิงเมธอด

ประเภทของ Method Reference

java// 1. Static method reference
List<Integer> numbers = List.of(5, 2, 8, 1);
numbers.stream()
       .forEach(System.out::println);  // เรียก System.out.println

// 2. Instance method reference
String text = "hello";
Optional<Integer> length = Optional.of(text)
                                   .map(String::length);

// 3. Constructor reference
List<String> strings = List.of("a", "bb", "ccc");
List<Integer> lengths = strings.stream()
                               .map(String::length)
                               .toList();

// 4. Specific instance method reference
List<String> words = List.of("java", "stream", "api");
words.stream()
     .map(String::toUpperCase)  // เรียก toUpperCase บน instance แต่ละตัว
     .forEach(System.out::println);

ข้อควรระวัง

1. Stream ใช้ครั้งเดียวเท่านั้น

javaStream<Integer> stream = List.of(1, 2, 3).stream();

// ครั้งแรก OK
stream.forEach(System.out::println);

// ครั้งที่สอง → IllegalStateException
stream.forEach(System.out::println);  // Error!

หากต้องใช้หลายครั้ง ต้องสร้าง Stream ใหม่

2. Intermediate operations ต้องมี Terminal operation

java// สิ่งนี้ไม่ทำอะไร (ไม่มี terminal operation)
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
       .filter(n -> n > 2)
       .map(n -> n * 2);

// ต้องมี terminal operation เช่น
numbers.stream()
       .filter(n -> n > 2)
       .map(n -> n * 2)
       .forEach(System.out::println);  // หรือ collect(), count() เป็นต้น

3. null ใน Stream

Stream ไม่ชอบ null มากนัก ต้องระวังการ handle null:

javaList<String> values = List.of("a", null, "c");

// วิธีที่ปลอดภัย
values.stream()
      .filter(v -> v != null)
      .forEach(System.out::println);

// หรือใช้ Optional
values.stream()
      .flatMap(v -> v != null ? Stream.of(v) : Stream.empty())
      .forEach(System.out::println);

สรุป

Stream API และ Functional Programming เป็นการเปลี่ยนแปลงครั้งสำคัญของ Java ที่ช่วยให้การเขียนโปรแกรมมีรูปแบบที่ชัดเจนและปลอดภัยยิ่งขึ้น

ผ่านการศึกษาเรื่องนี้ คุณได้เรียนรู้:

  • Stream API ช่วยให้ประมวลผลข้อมูล Collection ได้ด้วยวิธีประกาศแบบ functional
  • Lambda Expression ทำให้การเขียนโค้ดสั้นลงและสื่อความหมายได้ชัดเจน
  • Intermediate Operations เช่น filter()map()sorted() ใช้ประมวลผลข้อมูล
  • Terminal Operations เช่น collect()forEach()reduce() ปิดกระบวนการ
  • Lazy Evaluation ช่วยเพิ่มประสิทธิภาพของโปรแกรม
  • Method Reference ให้วิธีอื่นในการเขียน Lambda ที่ยิ่งสั้นลง

ด้วยความเข้าใจเรื่องเหล่านี้ คุณสามารถเขียนโปรแกรม Java ที่ยืดหยุ่น อ่านง่าย และประสิทธิภาพสูงได้ แนวคิด Functional Programming จะเป็นพื้นฐานสำคัญในการศึกษาเรื่องขั้นสูงต่างๆ ในการพัฒนาแอปพลิเคชันสมัยใหม่ เช่น การประมวลผลข้อมูลขนาดใหญ่ (Big Data) หรือการเขียนโปรแกรมแบบ reactive