ว่าด้วยเรื่อง “Steam” ใน Java

Phayao Boonon
4 min readJan 26, 2019

--

Photo by Debbie Pan on Unsplash

ใน 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 เป็นผลลัพธ์
https://www.logicbig.com/tutorials/core-java-tutorial/java-util-stream/stream-api-intro.html

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]

อ้างอิง

--

--

Phayao Boonon
Phayao Boonon

Written by Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish

No responses yet