การพัฒนาโปรแกรมเชิงวัตถุในยุคใหม่ไม่ได้หยุดเพียงแค่การออกแบบ 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 ทำให้:
- โค้ดสั้นลง อ่านง่าย
- ลดโอกาสเกิดข้อผิดพลาดเนื่องจากลด boilerplate code
- นำเสนอเจตนาของโปรแกรมอย่างชัดเจน
แนวคิดของ 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 อย่าง Function, Predicate, หรือ 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
- Source – แหล่งข้อมูล เช่น List, Set, Array หรือแม้แต่ลำนำจากเซ็นเซอร์
- Intermediate Operations – การประมวลผลกลาง เช่น
filter(),map(),sorted()อาจมีหลายขั้นตอนหรือไม่มีเลยก็ได้ - 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
คำอธิบายลำดับการทำงาน
stream()แปลง List["java", "functional", "stream", "programming"]เป็น Streamfilter(w -> w.length() > 5)กรองเฉพาะคำที่ยาวกว่า 5 ตัว →["functional", "stream", "programming"]map(String::toUpperCase)แปลงเป็นตัวพิมพ์ใหญ่ทั้งหมด →["FUNCTIONAL", "STREAM", "PROGRAMMING"]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 > 2เป็นเท็จ ข้ามไป - ตรวจสอบ 2:
2 > 2เป็นเท็จ ข้ามไป - ตรวจสอบ 3:
3 > 2เป็นจริง → Mapping 3 → ได้ 6 → พิมพ์ - … ทำเช่นเดียวกันกับ 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
