- 1. Iterator Pattern คืออะไร
- 2. ปัญหาที่ต้องการแก้
- 3. องค์ประกอบของ Pattern ใน Java
- 4. แยก Iterable กับ Iterator ให้ชัด
- 5. เวิร์กช็อป: สร้างตัวเดินชั้นหนังสือ
- 6. ติดตามสถานะการเดินทีละขั้น
- 7. ทำไมใช้ for-each ได้
- 8. งานจริงควรใช้ Iterator ที่มีอยู่ก่อน
- 9. การลบข้อมูลระหว่างเดิน
- 10. เลือกวิธีวนข้อมูลอย่างไร
- 11. ข้อดี ข้อจำกัด และความซับซ้อน
- 12. ข้อผิดพลาดที่พบบ่อย
- 13. แบบฝึกหัดต่อยอด
- 14. ประเด็นสำหรับใช้สอน
- เอกสารอ้างอิง
เดินผ่านสมาชิกของกลุ่มข้อมูลทีละรายการ โดยให้โค้ดผู้ใช้ไม่ต้องรู้รายละเอียดการจัดเก็บภายใน

หมวดหมู่: Behavioral Design Pattern
#1. Iterator Pattern คืออะไร
Iterator Pattern เป็นรูปแบบการออกแบบที่แยก การเก็บข้อมูล ออกจาก การเดินผ่านข้อมูล โครงสร้างข้อมูลรับผิดชอบสมาชิกที่จัดเก็บ ส่วน Iterator รับผิดชอบสถานะและลำดับการเดินในแต่ละรอบ
ลองนึกถึงชั้นหนังสือ ผู้ใช้ต้องการอ่านชื่อหนังสือทีละเล่ม โดยไม่จำเป็นต้องทราบว่าระบบเก็บหนังสือไว้ในอาร์เรย์ ลิงก์ลิสต์ หรือโครงสร้างต้นไม้
แนวคิดหลักมีสองส่วน:
- กลุ่มข้อมูล: ให้ทางเข้าสำหรับขอตัวเดินข้อมูล
- ตัวเดินข้อมูล: จำตำแหน่งปัจจุบัน ตรวจว่ามีรายการต่อไปหรือไม่ และคืนรายการถัดไป
จุดสำคัญคือ ตำแหน่งการเดินเป็นของ Iterator แต่ละตัว เมื่อสร้าง Iterator ใหม่ โค้ดจึงสามารถเดินชุดข้อมูลเดียวกันหลายรอบหรือหลายตำแหน่งได้อย่างอิสระ หาก implementation ออกแบบให้รองรับ
#2. ปัญหาที่ต้องการแก้
สมมติว่าโค้ดผู้ใช้เข้าถึงอาร์เรย์ภายในโดยตรง:
// แนวคิดก่อนปรับโครงสร้าง ไม่ใช่โค้ดจาก BookShelf ในตัวอย่างหลัก
Book[] books = shelf.getBooks();
for (int i = 0; i < books.length; i++) {
System.out.println(books[i].title());
}
โค้ดนี้ผูกกับอาร์เรย์ และอาจเปิดโอกาสให้ผู้ใช้แก้ข้อมูลภายในผ่านอาร์เรย์ที่ได้รับ หากเปลี่ยนวิธีจัดเก็บ โค้ดผู้ใช้ก็อาจต้องเปลี่ยนตาม
เมื่อมี Iterator ผู้ใช้สื่อสารผ่านสัญญาการเดินข้อมูล:
Iterator<Book> iterator = shelf.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().title());
}
ตัว Iterator ต้องรู้โครงสร้างภายในเพื่อเดินข้อมูล แต่ผู้ใช้ไม่ต้องรู้รายละเอียดนั้น ทั้งนี้ for แบบใช้ดัชนียังเหมาะเมื่อจำเป็นต้องใช้ดัชนีหรือเข้าถึงตำแหน่งแบบสุ่ม
#3. องค์ประกอบของ Pattern ใน Java
| บทบาทใน Pattern | ตัวแทนในตัวอย่าง | หน้าที่ |
|---|---|---|
| Aggregate | Iterable<Book> |
กำหนดทางเข้าสำหรับขอ Iterator |
| Concrete Aggregate | BookShelf |
เก็บหนังสือและสร้างตัวเดินข้อมูล |
| Iterator | Iterator<Book> |
กำหนดสัญญาการเดินข้อมูล |
| Concrete Iterator | BookIterator |
จำตำแหน่งและเดินอาร์เรย์หนังสือ |
| Client | IteratorDemo |
ขอ Iterator และอ่านข้อมูลผ่านสัญญา |
แผนภาพต่อไปนี้แสดงความสัมพันธ์ของตัวอย่าง โดยย่อชนิดสมาชิกให้เป็น Book เพื่อให้อ่านง่าย:
classDiagram
direction TB
class IterableBook {
<<interface>>
+iterator() IteratorBook
}
class IteratorBook {
<<interface>>
+hasNext() boolean
+next() Book
}
class BookShelf {
-Book[] books
+iterator() IteratorBook
}
class BookIterator {
-int index
+hasNext() boolean
+next() Book
}
class IteratorDemo
IterableBook <|.. BookShelf
IteratorBook <|.. BookIterator
BookShelf ..> BookIterator : creates
BookIterator --> BookShelf : reads books
IteratorDemo ..> IterableBook : requests iterator
IteratorDemo ..> IteratorBook : traverses
ในโค้ดจริง IterableBook และ IteratorBook ในแผนภาพหมายถึง Iterable<Book> และ Iterator<Book> ของ Java ตามลำดับ ไม่ต้องสร้างอินเทอร์เฟซใหม่
#4. แยก Iterable กับ Iterator ให้ชัด
| ประเด็น | Iterable<T> |
Iterator<T> |
|---|---|---|
| แนวคิด | แหล่งที่ขอตัวเดินข้อมูลได้ | ตัวเดินข้อมูลหนึ่งชุดที่มีสถานะ |
| เมธอดหลัก | iterator() |
hasNext() และ next() |
| ตำแหน่งการเดิน | ตัวอย่างนี้ไม่เก็บตำแหน่งไว้ที่แหล่งข้อมูล | เก็บใน Iterator แต่ละตัว |
ใช้กับ enhanced for |
ใช้ได้ | การ implement เพียง Iterator ยังไม่เพียงพอ |
| เริ่มรอบใหม่ | ตัวอย่างนี้เรียก iterator() เพื่อได้ตัวใหม่ |
ไม่มี reset() ในอินเทอร์เฟซมาตรฐาน |
Iterable ช่วยให้ออบเจ็กต์ใช้กับ for-each ได้ ส่วนอาร์เรย์ใช้กับ for-each ได้เช่นกันตามกฎของภาษา แม้อาร์เรย์ไม่ได้ implement Iterable [2][4]
การคืน Iterator ใหม่ทุกครั้งเป็นนโยบายของ
BookShelfในบทความนี้ ไม่ใช่ข้อรับประกันว่าทุก implementation ของIterableต้องอ่านซ้ำได้
#5. เวิร์กช็อป: สร้างตัวเดินชั้นหนังสือ
#5.1 ขอบเขตของตัวอย่าง
- ชั้นหนังสือเก็บข้อมูลในอาร์เรย์ส่วนตัว
- คัดลอกอาร์เรย์ที่รับเข้าใน constructor เพื่อแยกจากอาร์เรย์ของผู้เรียก
- สมาชิกและลำดับในชั้นหนังสือเปลี่ยนไม่ได้หลังสร้าง
- ใช้
record Bookที่มีชื่อเป็นStringเพื่อให้ข้อมูลหนังสือในตัวอย่างเปลี่ยนไม่ได้เช่นกัน - ทุกการเรียก
iterator()ได้ตัวใหม่ที่เริ่มจากตำแหน่งศูนย์ - ไม่รองรับการลบสมาชิกผ่าน Iterator
การคัดลอกอาร์เรย์เป็น shallow copy: คัดลอกช่องเก็บ reference ไม่ได้สร้าง Book ใหม่ทุกเล่ม แต่เพียงพอสำหรับตัวอย่างนี้เพราะข้อมูลของ Book เปลี่ยนไม่ได้ หากสมาชิกเป็นออบเจ็กต์ที่แก้ค่าได้ ต้องกำหนดนโยบายการเปลี่ยนข้อมูลเพิ่มเติม
#5.2 โค้ดครบในไฟล์เดียว
สร้างไฟล์ชื่อ IteratorDemo.java แล้ววางโค้ดต่อไปนี้:
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
record Book(String title) {
Book {
Objects.requireNonNull(title, "title must not be null");
}
}
final class BookShelf implements Iterable<Book> {
private final Book[] books;
BookShelf(Book... books) {
Objects.requireNonNull(books, "books must not be null");
this.books = books.clone();
for (Book book : this.books) {
Objects.requireNonNull(book, "book must not be null");
}
}
@Override
public Iterator<Book> iterator() {
return new BookIterator();
}
private final class BookIterator implements Iterator<Book> {
private int index = 0;
@Override
public boolean hasNext() {
return index < books.length;
}
@Override
public Book next() {
if (!hasNext()) {
throw new NoSuchElementException("No more books");
}
return books[index++];
}
}
}
public class IteratorDemo {
public static void main(String[] args) {
BookShelf shelf = new BookShelf(
new Book("Effective Java"),
new Book("Clean Code"),
new Book("Design Patterns")
);
System.out.println("Explicit iterator:");
Iterator<Book> iterator = shelf.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().title());
}
System.out.println("Enhanced for:");
for (Book book : shelf) {
System.out.println(book.title());
}
System.out.println("Independent cursors:");
Iterator<Book> left = shelf.iterator();
Iterator<Book> right = shelf.iterator();
System.out.println("left: " + left.next().title());
System.out.println("left: " + left.next().title());
System.out.println("right: " + right.next().title());
System.out.println("Exhausted iterator:");
try {
iterator.next();
} catch (NoSuchElementException ex) {
System.out.println(ex.getClass().getSimpleName());
}
}
}
BookIterator เป็น inner class จึงอ่านอาร์เรย์ส่วนตัวของ BookShelf ได้ โดยไม่ต้องเปิดเผยอาร์เรย์ผ่านเมธอดสาธารณะ
#5.3 คอมไพล์และรัน
ตรวจว่าติดตั้ง JDK แล้ว จากนั้นเปิด Terminal หรือ Command Prompt ที่โฟลเดอร์ของไฟล์:
javac -version
javac IteratorDemo.java
java IteratorDemo
โค้ดใช้ record ซึ่งใช้ได้ใน JDK 17 ตามข้อกำหนดของเวิร์กช็อป ไม่ต้องใช้ Maven, Gradle หรือ dependency เพิ่มเติม
#5.4 ผลลัพธ์ที่คาดหวัง
Explicit iterator:
Effective Java
Clean Code
Design Patterns
Enhanced for:
Effective Java
Clean Code
Design Patterns
Independent cursors:
left: Effective Java
left: Clean Code
right: Effective Java
Exhausted iterator:
NoSuchElementException
right ยังอ่านหนังสือเล่มแรก แม้ left อ่านไปสองเล่มแล้ว เพราะแต่ละตัวมี index ของตนเอง ส่วน iterator ตัวแรกอ่านครบตั้งแต่ช่วงแรก จึงเกิดข้อยกเว้นเมื่อขอรายการเพิ่ม
#6. ติดตามสถานะการเดินทีละขั้น
ตารางนี้อ้างถึง Iterator ตัวหนึ่งที่เพิ่งสร้างและมีหนังสือสามเล่ม:
| คำสั่ง | index ก่อน |
ผลลัพธ์ | index หลัง |
|---|---|---|---|
hasNext() |
0 | true |
0 |
hasNext() ซ้ำ |
0 | true |
0 |
next() |
0 | Effective Java |
1 |
next() |
1 | Clean Code |
2 |
next() |
2 | Design Patterns |
3 |
hasNext() |
3 | false |
3 |
next() |
3 | NoSuchElementException |
3 |
ใน implementation นี้ hasNext() ตรวจเงื่อนไขอย่างเดียว ส่วน next() อ่านสมาชิกแล้วเลื่อนตำแหน่งด้วย index++ จึงเรียก hasNext() ซ้ำได้โดยไม่ข้ามข้อมูล
เมื่อไม่มีสมาชิกเหลือ สัญญา Iterator.next() กำหนดให้โยน NoSuchElementException ไม่ควรคืน null แทนการสิ้นสุด [1]
#7. ทำไมใช้ for-each ได้
สำหรับแหล่งข้อมูลที่เป็น Iterable โครงสร้างการทำงานของ:
for (Book book : shelf) {
System.out.println(book.title());
}
สามารถอธิบายเชิงแนวคิดได้ด้วย:
Iterator<Book> it = shelf.iterator();
while (it.hasNext()) {
Book book = it.next();
System.out.println(book.title());
}
นี่คือเหตุผลที่สร้าง Iterable<Book> แล้วใช้งานกับไวยากรณ์วนซ้ำของภาษาได้ทันที กรณีอาร์เรย์มีกฎการแปลงของตัวเอง [4]
หากต้องการประมวลผลเฉพาะรายการที่เหลือ อาจใช้ forEachRemaining() กับ Iterator เช่น หลังอ่านเล่มแรกแล้วให้พิมพ์เล่มที่เหลือ [1]:
Iterator<Book> remaining = shelf.iterator();
remaining.next();
remaining.forEachRemaining(book -> System.out.println(book.title()));
// Clean Code
// Design Patterns
#8. งานจริงควรใช้ Iterator ที่มีอยู่ก่อน
การเขียนตัวเดินเองมีประโยชน์สำหรับการเรียนรู้หรือโครงสร้างข้อมูลเฉพาะ หากคลาสเพียงห่อรายการหนังสือ ใช้ Iterator ของ collection ภายในได้ โดยยังคงซ่อนรายละเอียดจากผู้ใช้:
import java.util.Iterator;
import java.util.List;
final class Catalog implements Iterable<Book> {
private final List<Book> books;
Catalog(List<Book> books) {
this.books = List.copyOf(books);
}
@Override
public Iterator<Book> iterator() {
return books.iterator();
}
}
ตัวอย่างนี้ใช้ Book จากตัวอย่างหลัก List.copyOf() สร้างรายการที่แก้ไขผ่านรายการนั้นไม่ได้ และไม่รับสมาชิก null [5] โค้ดของ Catalog จึงลดภาระการดูแลตรรกะตัวเดินของตนเอง ขณะที่ผู้ใช้ยังเรียก for (Book book : catalog) ได้
#9. การลบข้อมูลระหว่างเดิน
remove() เป็นความสามารถทางเลือก ไม่ใช่ทุก Iterator ที่รองรับ ใน BookIterator เราไม่ได้ override เมธอดนี้ จึงใช้ default implementation ที่โยน UnsupportedOperationException [1]
หากใช้ ArrayList ที่แก้ไขได้และต้องการลบระหว่างเดิน สามารถใช้ Iterator ของมัน:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
// วางภายในเมธอด เช่น main()
List<String> languages = new ArrayList<>(
List.of("Java", "PHP", "JavaScript")
);
Iterator<String> it = languages.iterator();
while (it.hasNext()) {
String language = it.next();
if (language.equals("PHP")) {
it.remove();
}
}
System.out.println(languages); // [Java, JavaScript]
สำหรับ Iterator ที่รองรับการลบ ต้องเรียก next() ก่อน และลบได้หนึ่งครั้งต่อการอ่านสมาชิกหนึ่งครั้ง [1]
อย่าใช้ languages.remove(...) เพื่อเปลี่ยนโครงสร้าง ArrayList ขณะที่ Iterator เดิมกำลังเดินอยู่ เพราะอาจเกิด ConcurrentModificationException นโยบาย fail-fast ของ ArrayList ตรวจจับแบบ best effort จึงไม่ควรใช้ข้อยกเว้นนี้เป็นกลไกควบคุมความถูกต้องของโปรแกรม [3]
การแก้ข้อมูลระหว่างเดินเป็นนโยบายของแต่ละ implementation: Iterator บางประเภทอ่านจาก snapshot หรือมีนโยบายรองรับการเปลี่ยนแปลงเฉพาะของตน การมีอินเทอร์เฟซ Iterator เพียงอย่างเดียวไม่ได้ทำให้ข้อมูลปลอดภัยสำหรับหลายเธรด
#10. เลือกวิธีวนข้อมูลอย่างไร
| ความต้องการ | วิธีที่เหมาะ |
|---|---|
| อ่านสมาชิกทั้งหมดด้วยโค้ดสั้น | enhanced for |
| ควบคุมการขอสมาชิกทีละรายการ | Iterator |
| ลบสมาชิกขณะเดินและ implementation รองรับ | Iterator.remove() |
| ต้องใช้ตำแหน่งหรือเข้าถึงสมาชิกแบบสุ่ม | ดัชนีบนอาร์เรย์หรือรายการที่เหมาะสม |
เดินหน้าและถอยหลังใน List |
ListIterator [6] |
| แปลงและกรองข้อมูลเป็น pipeline | Stream API ตามลักษณะงาน |
| ซ่อนวิธีเดินโครงสร้างเฉพาะ | สร้าง Iterator หรือห่อ Iterator ที่มีอยู่ |
Iterator เป็นกลไกสำหรับควบคุมการเดินข้อมูล ส่วน Stream API เพิ่มการประกอบขั้นตอนประมวลผล เช่น กรองและแปลงข้อมูล การเลือกใช้ขึ้นกับงานและความชัดเจนของโค้ด
#11. ข้อดี ข้อจำกัด และความซับซ้อน
ข้อดี
- ผู้ใช้ไม่ต้องพึ่งรายละเอียดการจัดเก็บภายใน
- แยกสถานะการเดินออกจากข้อมูล ช่วยรองรับหลายรอบที่เป็นอิสระ
- ทำงานกับ
for-eachและ API ที่รับIterableได้ - เปลี่ยนวิธีเดินหรือเพิ่มตัวเดินอีกแบบได้ โดยรักษาสัญญาที่ผู้ใช้เรียก
ข้อจำกัด
- เพิ่มคลาสและสถานะที่ต้องดูแล หากเขียนตัวเดินเอง
- ต้องกำหนดพฤติกรรมเมื่อข้อมูลเปลี่ยนระหว่างเดิน
- Iterator ทั่วไปไม่ได้รับประกันการถอยหลัง การอ่านซ้ำ หรือความปลอดภัยต่อหลายเธรด
- ลำดับข้อมูลเป็นนโยบายของโครงสร้างนั้น ในตัวอย่างนี้คือลำดับอาร์เรย์ ไม่ได้หมายความว่าทุก collection มีลำดับเหมือนกัน
ความซับซ้อนต่อไปนี้เป็นของ BookShelf ในบทความนี้เท่านั้น:
| การทำงาน | เวลา | หน่วยความจำเพิ่ม |
|---|---|---|
| สร้างชั้นหนังสือและคัดลอกอาร์เรย์ n เล่ม | O(n) | O(n) สำหรับอาร์เรย์ใหม่ |
| สร้าง Iterator หนึ่งตัว | O(1) | O(1) |
hasNext() หรือ next() หนึ่งครั้ง |
O(1) | O(1) |
| เดินครบ n เล่ม | O(n) | O(1) สำหรับสถานะการเดิน |
ตัวเดินต้นไม้หรือข้อมูลจากระบบภายนอกอาจมีต้นทุนต่างออกไป Pattern ไม่ได้รับประกันว่าการอ่านสมาชิกถัดไปทุกแบบต้องใช้เวลา O(1)
#12. ข้อผิดพลาดที่พบบ่อย
| ข้อผิดพลาด | ผลที่อาจเกิด | แนวทางแก้ |
|---|---|---|
เก็บ index กลางไว้ที่ BookShelf |
ตัวเดินหลายตัวรบกวนตำแหน่งกัน | เก็บตำแหน่งใน Iterator แต่ละตัว |
| คืน Iterator ตัวเดิมทุกครั้งโดยไม่ได้ตั้งใจ | รอบใหม่เริ่มต่อจากตำแหน่งเดิม | สร้างตัวใหม่เมื่อแหล่งข้อมูลรองรับการอ่านซ้ำ |
เลื่อนตำแหน่งทุกครั้งที่เรียก hasNext() |
เรียกตรวจซ้ำแล้วข้ามสมาชิก | รักษารายการที่จะคืนจนกว่า next() จะอ่าน |
คืน null เมื่อเดินจบ |
ผู้ใช้แยกไม่ออกจากสมาชิกที่เป็น null |
โยน NoSuchElementException |
| เปลี่ยนโครงสร้าง collection ระหว่างเดินโดยไม่ดูนโยบาย | ข้อยกเว้นหรือผลลัพธ์ไม่เป็นไปตามคาด | ใช้วิธีที่ implementation รองรับ |
สมมติว่า remove() ใช้ได้กับทุกตัว |
UnsupportedOperationException |
ตรวจสัญญาของตัวเดินนั้น |
Iterator บางแบบอาจอ่านล่วงหน้าและเก็บรายการไว้ใน hasNext() เช่น ตัวกรองข้อมูล แต่การตรวจซ้ำต้องไม่ทำให้ผู้ใช้พลาดสมาชิกที่ยังไม่ได้รับจาก next()
#13. แบบฝึกหัดต่อยอด
- สร้าง
new BookShelf()ที่ไม่มีหนังสือ อธิบายผลของhasNext()และnext() - เรียก
hasNext()สามครั้งก่อนอ่านเล่มแรก แล้วตรวจว่ายังได้Effective Java - ทดลองแก้ช่องในอาร์เรย์ต้นฉบับหลังสร้างชั้นหนังสือ ตรวจว่าชั้นหนังสือยังอ่านข้อมูลเดิมเพราะมีการ clone
- เพิ่ม
reverseIterator()ให้เริ่มจากเล่มสุดท้าย โดยไม่เปลี่ยนลำดับการเดินปกติของiterator() - สร้างตัวเดินที่คืนเฉพาะหนังสือซึ่งชื่อมีคำว่า
JavaโดยระวังการเรียกhasNext()ซ้ำ และกรณีไม่พบหนังสือเลย
เกณฑ์ตรวจงาน: ผลลัพธ์ต้องตรงตามลำดับที่กำหนด ตัวเดินหลายตัวต้องไม่แชร์ตำแหน่งโดยไม่ตั้งใจ และการอ่านหลังหมดข้อมูลต้องเป็นไปตามสัญญา Iterator
#14. ประเด็นสำหรับใช้สอน
Iterator Pattern ทำให้ผู้ใช้สั่งอ่านข้อมูลได้ผ่านสัญญาเดียว แม้รายละเอียดการจัดเก็บและวิธีเดินจะต่างกัน ใน Java ให้เริ่มจากแยกบทบาท Iterable กับ Iterator แล้วตรวจว่า ใครเก็บข้อมูล ใครเก็บตำแหน่ง และใครกำหนดลำดับการเดิน
เมื่อใช้ collection มาตรฐาน ส่วนใหญ่สามารถใช้ Iterator ที่มีอยู่ได้ทันที การสร้างตัวเดินเองเหมาะเมื่อโครงสร้างหรือนโยบายการเดินมีลักษณะเฉพาะ และต้องอธิบายสัญญาของมันให้ชัดเจน
#เอกสารอ้างอิง
ใช้เอกสาร Java SE 21 สำหรับตรวจสัญญา API ที่กล่าวถึง โค้ดตัวอย่างใช้เฉพาะความสามารถที่มีใน Java 17