ขั้นตอนที่ `map` กับ `filter` เขียนไม่ได้
Stream ของ Java 8 ให้ขั้นกลางมาชุดหนึ่ง คือ map, filter, flatMap, distinct,
sorted, limit, skip · แล้วปิดตาย
เพิ่มขั้นกลางของตัวเองเข้าไปไม่ได้เลย และนั่นคือข้อที่บทนี้แก้
ตัวอย่างของงานที่เขียนใน Stream เดิมไม่ได้
หัวข้อที่มีชื่อว่า “ตัวอย่างของงานที่เขียนใน Stream เดิมไม่ได้”จับรายการทีละสาม · ฟังดูง่าย แต่ไม่มีขั้นกลางไหนทำให้
สมมติต้องการแบ่งรายการเป็นก้อนละสามตัว · ใน Java 8 คุณมีสองทาง
คือออกจาก Stream ไปเขียน loop เอง หรือใช้ index แล้ว groupingBy
ซึ่งอ่านยากและต้องอ่านทั้งหมดเข้าหน่วยความจำก่อน
ทางที่ขาดไปคือ ขั้นกลางที่จำอะไรไว้ระหว่างรายการได้
gather เปิดช่องนั้น
หัวข้อที่มีชื่อว่า “gather เปิดช่องนั้น”
Stream.gatherรับตัวจัดการของเราเอง · และGatherersมีของสำเร็จรูปให้ชุดหนึ่ง
import java.util.stream.Gatherers;import java.util.stream.Stream;
var windows = Stream.of(1, 2, 3, 4) .gather(Gatherers.windowFixed(2)) .toList();[[1, 2], [3, 4]]Stream.gatherStream.gatherขั้นกลางของ `Stream` ที่รับตัวจัดการของเราเอง · เปิดทางให้เขียนขั้นตอนที่ `map` และ `filter` ทำไม่ได้ เช่นการมองย้อนหลังไปหลายรายการ เป็นขั้นกลางเหมือน map ทุกประการ · ต่อท้ายอะไรก็ได้ ต่อหน้าอะไรก็ได้
· ต่างกันที่มันรับตัวจัดการที่ ถือสถานะข้ามรายการได้
GatherersGatherersชุดตัวจัดการสำเร็จรูปสำหรับ `gather` · `windowFixed` แบ่งเป็นก้อน `scan` สะสมค่าไปเรื่อยๆ · ทั้งหมดขี้เกียจ จึงใช้กับต้นทางที่ไม่มีวันจบได้ มีของสำเร็จรูปที่ใช้บ่อยให้แล้ว · windowFixed แบ่งเป็นก้อน
· scan สะสมค่าไปเรื่อยๆ
var running = Stream.of(1, 2, 3, 4) .gather(Gatherers.scan(() -> 0, Integer::sum)) .toList();[1, 3, 6, 10]พิสูจน์ว่ามันขี้เกียจ ด้วยการนับ
หัวข้อที่มีชื่อว่า “พิสูจน์ว่ามันขี้เกียจ ด้วยการนับ”ข้ออ้างว่า “มันขี้เกียจ” ตรวจได้ · ไม่ต้องเชื่อ
ต่อ gather เข้ากับต้นทางที่ไม่มีวันจบ แล้วขอแค่สองก้อน
· ถ้ามันไม่ขี้เกียจ โปรแกรมจะไม่มีวันจบ
AtomicInteger asked = new AtomicInteger();var first = Stream.iterate(1, i -> i + 1) .peek(i -> asked.incrementAndGet()) .gather(Gatherers.windowFixed(3)) .limit(2) .toList();windows=[[1, 2, 3], [4, 5, 6]]asked=6หกครั้งพอดี · ไม่ใช่เจ็ด ไม่ใช่ร้อย · สองก้อนละสามตัว ต้องใช้หกตัว แล้วมันหยุดถามทันที
lazy evaluationlazy evaluationการไม่คำนวณจนกว่าจะมีคนขอผลจริง · พิสูจน์ได้ด้วยการนับว่าต้นทางถูกถามกี่ครั้ง ไม่ใช่ด้วยการอ่าน code แบบนี้พิสูจน์ด้วยการนับ ไม่ใช่ด้วยการอ่าน code
· และการนับใช้เพียง peek กับตัวนับหนึ่งตัว
flowchart LR S["Stream.iterate<br/>ไม่มีวันจบ"] --> P["peek · นับ"] --> G["gather<br/>windowFixed(3)"] --> L["limit(2)"] --> T["toList"] T -.->|"ขอสองก้อน"| L L -.->|"ขอทีละก้อน"| G G -.->|"ขอทีละตัว รวมหกตัว"| P
คำบรรยายภาพ: ปลายทางเป็นคนขอ ต้นทางเป็นคนตอบ · จึงถามเท่าที่ปลายทางใช้
scan ถามเท่าที่มี
หัวข้อที่มีชื่อว่า “scan ถามเท่าที่มี”ต้นทางที่จบแล้ว ก็ถูกถามเท่าที่มีจริง ไม่ใช่มากกว่านั้น
Gatherers.scan เหนือรายการสี่ตัว ถามต้นทาง 4 ครั้ง และคืน [1, 3, 6, 10]
ต่างจาก windowFixed ตรงที่ scan คืนผลหนึ่งตัวต่อรายการหนึ่งตัว
· ส่วน windowFixed เก็บไว้จนครบก้อนแล้วค่อยคืน
gather รับ Gatherer ตัวไหนก็ได้ ไม่จำเป็นต้องมาจาก Gatherers
· คุณเขียนเองได้เมื่อของสำเร็จรูปไม่พอ
แต่ก่อนจะเขียนเอง ให้ถามสองคำถาม
- ขั้นตอนนี้ต้องจำอะไรไว้ระหว่างรายการหรือเปล่า ถ้าไม่
mapกับfilterพอแล้ว - มันหยุดกลางทางได้ไหม ถ้าได้ ต้องเขียนให้มันบอกปลายทางเป็น ไม่ใช่แค่หยุดคืนค่า
ตัวจัดการที่เขียนผิดจะทำให้ความขี้เกียจหายไป · แล้วต้นทางที่ไม่มีวันจบ จะทำให้โปรแกรมไม่มีวันจบ ซึ่งเป็นความพังที่หายากที่สุดแบบหนึ่ง
สิ่งที่บทนี้ยังไม่ทำ
หัวข้อที่มีชื่อว่า “สิ่งที่บทนี้ยังไม่ทำ”สามอย่างที่ยังไม่มี และแต่ละอย่างมาถึงเมื่อมีเหตุผลให้ต้องมี
- ยังไม่ได้เขียน
Gathererเอง บทนี้ใช้ของสำเร็จรูปสองตัวเท่านั้น - ยังไม่ได้แตะการทำงานพร้อมกัน
gatherใช้กับ parallel stream ได้ แต่ตัวจัดการที่ถือสถานะทำให้เรื่องนั้นซับซ้อนขึ้นมาก - ยังไม่ได้เทียบกับ
Collectorซึ่งเป็นของคนละที่ในสายท่อ ·Collectorปิดท้าย ส่วนGathererอยู่ตรงกลาง
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้เพิ่มของหนึ่งอย่าง และให้วิธีตรวจว่ามันทำงานอย่างที่โฆษณา
gather ทำให้ Stream เปิดรับขั้นกลางที่เราเขียนเอง · ซึ่งเป็นข้อที่ปิดตายมาตั้งแต่ Java 8
และข้ออ้างว่ามันขี้เกียจ ตรวจได้ด้วยตัวนับตัวเดียว · ต้นทางที่ไม่มีวันจบ ถูกถาม 6 ครั้ง สำหรับสองก้อนละสามตัว
บทหน้าเราไปเรื่องที่คนพูดถึงมากที่สุดของ Java รุ่นใหม่ — virtual thread · และเราจะวัดมันด้วยจำนวน ไม่ใช่ด้วยเวลา
ตัวเลขทั้งสองมาจากการรันจริง:
- วัดเมื่อ 2026-09-01 ·
openjdk 25.0.4.1 2026-08-18 - รันซ้ำเองได้ด้วย
npm run verify:java-modernซึ่งใช้peekกับตัวนับ แล้วอ่านค่าออกมาหลังสายท่อทำงานจบ - JEP 485 — Stream Gatherers (เข้าถึง 2026-09-01) — สถานะ final และรายชื่อตัวจัดการสำเร็จรูป
- Gatherers
(เข้าถึง 2026-09-01) —
windowFixedและscan
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3ต่อ `gather(Gatherers.windowFixed(3))` เข้ากับ `Stream.iterate` ที่ไม่มีวันจบ แล้ว `limit(2)` · ต้นทางถูกถามกี่ครั้ง