ว่าด้วยเรื่อง “Steam” ใน Java
ใน Java 8 ได้เพิ่ม feature ใหม่คือ Stream ใน java.util
ซึ่งเป็นคลาสที่ process collection ของ object โดยที่ stream เป็นลำดับของ element ที่รองรับหลากหลาย method ซึ่งสามารถทำเป็น pipeline ที่จะ process ผลลัพธ์ต่อเนื่องไปเรื่อยๆ และที่สำคัญทำให้เราสามารถเขียน Java ในรูปแบบของ functional programming ได้ โดยคุณสมบัติหลักคือ
- Stream ไม่ได้มาแทนที่ data structure แต่สามารถรับ input เป็น Collection, Array หรือ I/O channel
- Stream ไม่ได้เปลี่ยนแปลง data structure ดั่งเดิม เพียงแต่สร้างผลลัพธ์จาก pipeline methods เท่านั้น
- แต่ล่ะ intermediate operation เป็น lazily execute และ return ผลลัพธ์เป็น stream ด้วยเหตุนี้ intermediate operation สามารถเป็น pipeline ได้ และ terminate operator จะ return เป็นผลลัพธ์
Creating Stream
Stream สามารถสร้างได้หลากหลายวิธีจาก source ที่แตกต่างกัน เช่น Collection หรือ Array ด้วย method stream()
และ of()
// Stream from Array
String[] array = new String[]{"a", "b", "c", "d", "e"};
Stream<String> streamOfArray = Arrays.stream(array);
// Stream from Collection
List<String> collection = Arrays.asList(array);
Stream<String> streamOfCollection = collection.stream();
// User Stream's 'of' method
Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
เช่นเดียวกับ Optional ที่เราสามารถสร้าง Stream ว่างๆ ได้ ด้วย method empty()
นอกจากนี้เรายังสร้าง Stream จาก primitive data type อย่าง IntStream
สำหรับ int
, DoubleStream
สำหรับ double
และ LongStream
สำหรับ long
IntStream.range(1, 4).forEach(System.out::println);
// 1
// 2
// 3
LongStream.iterate(10L, n -> n + 20L).limit(3)
.forEach(System.out::println);
// 10
// 30
// 50
DoubleStream.iterate(0.5, n -> n + 3).limit(3)
.forEach(System.out::println);
// 0.5
// 3.5
// 6.5
Multi-threading with Streams
Stream สามารถใช้งาน multi-threading ได้ง่ายๆ ด้วย method parallelStream()
ที่ดำเนินการ element ของ Stream ในโหมดขนาน (Parallel Mode) และสามารถใช้ operation พื้นฐานของ Stream ได้เลย
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
// Multi-threading Stream
list.parallelStream().forEach(System.out::println);
// c
// e
// d
// b
// a
จะเห็นได้ว่าถ้าเรารันโค้ดนี้ซ้ำกัน ในแต่ละครั้งจะได้ผลลัพย์ที่ไม่เหมือนกัน (ไม่เป็นลำดับ) เพราะว่ามีการประมวลผลแบบขนาน ซึ่งต่างจากใช้ method stream()
ที่จะได้ผลเป็นลำดับเสมอเพราะประมาลผลโหมด single thread
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
// Single-threading Stream
list.stream().forEach(System.out::println);
// a
// b
// c
// d
// e
Stream Operations
ตัวดำเนินการของ Stream มีหลากหลาย โดยแบ่งออกเป็น 2 กลุ่ม คือ
- Intermediate operation (return เป็น Stream<T>) เป็น pipeline
- Terminate operation (return เป็น Type ของ Stream)
แต่ไม่มี operation ไหนของ Stream ที่เปลี่ยนแปลงค่าดั่งเดิมได้
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
// Terminated operation
long count = list.stream().distinct().count();
// count -> 5
// Intermediate op
List<String> mapped = list.stream().map(e -> e.concat("x"))
.collect(Collectors.toList());
// mapped -> [ax, bx, cx, dx, ex]
จากตัวอย่างนี้ method distinct()
เป็น intermediate operation ที่จะ return เป็น Stream ที่สามารถใช้ Stream operation อื่นต่อได้ และ count()
เป็น terminate operation ที่จะ return ค่ากลับมาเป็นขนาดของ collection ใน Stream
ต่างจาก method map()
ที่ได้ return มาเป็น Stream ที่สามารถใช้ method ของ Stream อื่นอย่างเช่น collection
เพื่อให้ได้ return มาเป็น List
Iterating operation
Stream จะช่วยให้เรา iterate collection ได้ง่ายขึ้นอาจะใช้ method foreEach()
หรือใช้ anyMatch()
เพื่อหา element ใน collection ที่ตรงกับเงื่อนไข
Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
boolean match = stream.anyMatch(e -> e.contains("a"));
Filtering Operation
Stream มี method filter()
เพื่อใช้สำหรับคัดกลอง element ของ collection ที่ตรงกับเงื่อนไข
List<String> list = Arrays.asList("microsoft", "apple", "samsung");
Stream<String> filter = list.stream().filter(e -> e.contains("a"));
จากตัวอย่างนี้ก็จะได้ Stream ที่มี collection ของค่าที่กลองแล้ว (จะได้ “apple” และ “samsung”)
Mapping Operation
Stream สามารถแปลงค่า element ของ Stream และสร้างเป็น Stream ใหม่ได้ ด้วย method map()
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
Stream<String> mapped = list.stream().map(e -> e.concat("x"));
mapped.forEach(System.out::println);
// ax
// bx
// cx
// dx
// ex
จากตัวอย่างนี้ก็จะได้ Stream mapped
ที่ element ของ collection ที่ต่อท้ายด้วย “x”
แต่ถ้ามี Stream ที่ element ประกอบด้วย collection อีกทีหนึ่ง สามารถใช้ method flatMap()
ได้
ตัวอย่าง ก่อนที่จะใช้ flatMap()
สร้างตัวอย่าง class Foo
ที่มี collection (List) ของ class Bar
เป็น member
class Foo {
String name;
List<Bar> bars = new ArrayList<>();
public Foo(String name) {
this.name = name;
}
}
class Bar {
String name;
public Bar(String name) {
this.name = name;
}
}
หลังจากนั้นใส่ค่าเริ่มต้นให้กับ collection ของ class Foo
และ Bar
ด้วย IntStream
List<Foo> foos = new ArrayList<>();
IntStream.range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i)));
foos.forEach(f -> IntStream.range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " +f.name))));
และใช้ flatMap
เพื่อทำการ mapping ค่าของ collection ของ class Foo
และต่อด้วย iterate collection ของ class Bar
ที่เป็น member ของ class Foo
ด้วยการพิมพ์ name ของ class Bar
ออกมา
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
// Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3
Matching Operation
Stream มี method ที่ช่วยเราตรวจสอบค่าใน collection ว่ามีอยู่หรือไม่ด้วย method anyMatch() , allMatch() และ nonMatch() ซึ่งทั้งหมดเป็น terminate operation
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
boolean anyMatch = list.stream().anyMatch(e -> e.contains("a"));
boolean allMatch = list.stream().allMatch(e -> e.contains("a"));
boolean noneMatch = list.stream().noneMatch(e -> e.contains("a"));
// anyMatch -> true
// allMatch -> false
// noneMatch -> false
Reduction Operation
Stream สามารถทำ reduction operation ได้ เป็นการเอา element ของ sequence collection มารวมกันเป็นผลลัพธ์เดียว โดยทำซ้ำในลำดับของ element ด้วย method reduce()
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = list.stream().reduce(20, (x, y) -> x + y);
// sum -> 41
Collecting Operation
การทำ reduction operation ใน Stream สามารถทำด้วย method collection()
ซึ่งมีประโยชน์มากในกรณีแปลง Stream ไปเป็น Collection หรือ Map และแทน Stream ในรูปแบบของ single string โดยใช้ utility class Collectors
ใน collecting operation
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result;
result = list.stream().map(e -> e * 2).collect(Collectors.toList());
// result -> [2, 4, 6, 8, 10, 12]