ข้าม​ไป​ยัง​เนื้อหา

ขั้น​ตอน​ที่ `map` กับ `filter` เขียน​ไม่​ได้

Stream ของ Java 8 ให้​ขั้น​กลาง​มา​ชุด​หนึ่ง คือ map, filter, flatMap, distinct, sorted, limit, skip · แล้ว​ปิดตาย

เพิ่ม​ขั้น​กลาง​ของ​ตัว​เอง​เข้าไป​ไม่​ได้​เลย และ​นั่น​คือ​ข้อ​ที่​บท​นี้​แก้

จับ​รายการ​ที​ละ​สาม · ฟัง​ดู​ง่าย แต่​ไม่มี​ขั้น​กลาง​ไหน​ทำให้

สมมติ​ต้องการ​แบ่ง​รายการ​เป็น​ก้อน​ละ​สาม​ตัว · ใน Java 8 คุณ​มี​สอง​ทาง คือ​ออก​จาก Stream ไป​เขียน loop เอง หรือ​ใช้ index แล้ว groupingBy ซึ่ง​อ่าน​ยาก​และ​ต้อง​อ่าน​ทั้งหมด​เข้า​หน่วย​ความ​จำ​ก่อน

ทาง​ที่​ขาด​ไป​คือ ขั้น​กลาง​ที่​จำ​อะไร​ไว้​ระหว่าง​รายการ​ได้

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

คำ​บรรยาย​ภาพ: ปลาย​ทาง​เป็น​คน​ขอ ต้นทาง​เป็น​คน​ตอบ · จึง​ถาม​เท่า​ที่​ปลาย​ทาง​ใช้

ต้นทาง​ที่​จบ​แล้ว ก็​ถูก​ถาม​เท่า​ที่​มี​จริง ไม่ใช่​มากกว่า​นั้น

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)` · ต้นทางถูกถามกี่ครั้ง