CodeGym /Các khóa học /JAVA 25 SELF /Iterable và Iterator: duyệt collection

Iterable và Iterator: duyệt collection

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giao diện Iterable

Trong Java hầu hết các collection (trừ Map) đều triển khai giao diện Iterable. Điều này có nghĩa là bạn có thể duyệt chúng tuần tự — từng phần tử một, mà không cần quan tâm đến cấu trúc bên trong. Đối với lập trình viên, điều này giống như “collection có sẵn cách để đi qua tất cả phần tử”.

Giao diện Iterable định nghĩa đúng một phương thức:

Iterator<E> iterator();

Phương thức iterator() trả về một đối tượng kiểu Iterator — “trợ thủ” biết cách đi qua collection từng bước. Nhờ vậy mà vòng lặp for-each quen thuộc hoạt động:

for (ElementType e : collection) {
    // ...
}

— “đằng sau hậu trường” chính là Iterator đó. Một điểm cộng nữa: bạn có thể xóa phần tử an toàn trong lúc duyệt bằng phương thức remove(). Nếu thử làm điều này với cách duyệt thông thường, rất dễ gặp ConcurrentModificationException.

2. Giao diện Iterator

Iterator là “người dẫn đường” có thể đi qua collection của bạn, không bỏ sót phần tử và tuân theo thứ tự duyệt của nó.

Phương thức Mô tả
boolean hasNext()
Còn phần tử nào để duyệt không?
E next()
Trả về phần tử tiếp theo và chuyển con trỏ tới nó
void remove()
Xoá phần tử hiện tại một cách an toàn, không gây lỗi

Ví dụ duyệt collection bằng Iterator

import java.util.*;

public class IteratorDemo {
    public static void main(String[] args) {
        List<String> tasks = new ArrayList<>();
        tasks.add("Vuốt ve con mèo");
        tasks.add("Làm bài tập về nhà");
        tasks.add("Xem phim bộ");

        Iterator<String> it = tasks.iterator();
        while (it.hasNext()) {
            String task = it.next();
            System.out.println("Công việc: " + task);
        }
    }
}

Ở đây có gì diễn ra?

  • Lấy iterator qua tasks.iterator().
  • Khi còn phần tử tiếp theo (hasNext() trả về true), lấy nó qua next() và in ra.
  • Iterator tự theo dõi thứ tự duyệt — bạn không cần biết collection lưu trữ bên trong ra sao.

3. Tại sao cần Iterator khi đã có vòng lặp?

Với Iterator, bạn có thể duyệt bất kỳ collection nào, kể cả không có chỉ số (ví dụ Set). Đây là cách làm thống nhất, không phụ thuộc vào kiểu collection cụ thể.

Xoá phần tử an toàn

Nhiệm vụ thường gặp: đi qua collection và xoá một số phần tử. Nếu làm bằng for-each, bạn có thể gặp lỗi:

for (String task : tasks) {
    if (task.contains("mèo")) {
        tasks.remove(task); // BOOM! ConcurrentModificationException
    }
}

Vì sao lại như vậy? Collection không mong đợi cấu trúc của nó bị thay đổi trực tiếp trong lúc đang được iterator duyệt.

Cách đúng:

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("mèo")) {
        it.remove(); // Mọi thứ sẽ êm xuôi!
    }
}

Tại sao không chỉ dùng chỉ số (index)?

Bởi không phải collection nào cũng có chỉ số. Ví dụ, HashSet hay TreeSet không có khái niệm “phần tử thứ năm”. Iterator luôn hoạt động — đó là sức mạnh của nó.

4. Chi tiết về for-each

Vòng lặp for nâng cao (for-each) có từ Java 5. Về bản chất đây là cú pháp đường mật (syntactic sugar) giúp duyệt phần tử một cách tối giản:

for (String task : tasks) {
    System.out.println("Công việc: " + task);
}

Bên dưới, trình biên dịch gọi iterator(), kiểm tra phần tử qua hasNext() và lấy chúng bằng next(). Đọc lên giống như một câu: “đối với mỗi công việc trong danh sách”.

Khi nào for-each không phù hợp?

  • Cần xoá phần tử trong lúc duyệt (for-each không cho gọi trực tiếp remove()).
  • Cần truy cập chỉ số để, ví dụ, thay thế phần tử theo vị trí.
  • Bạn làm việc với Map — nó có cặp “key-value”, cần logic duyệt riêng.

5. Duyệt Map: mẹo và lưu ý

Giao diện Map không triển khai Iterable trực tiếp vì đó là tập các cặp “key-value”. Tuy vậy, Map cung cấp các view tiện lợi để duyệt.

Duyệt theo key

Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");

for (String login : users.keySet()) {
    System.out.println("Tên đăng nhập: " + login);
}

Duyệt theo value

for (String email : users.values()) {
    System.out.println("Email: " + email);
}

Duyệt theo cặp (key-value)

Cách tổng quát nhất — duyệt theo entrySet():

for (Map.Entry<String, String> entry : users.entrySet()) {
    System.out.println("Tên đăng nhập: " + entry.getKey() + ", Email: " + entry.getValue());
}

Thông tin thú vị: Entry là một giao diện nội bộ (inner interface) của Map với các phương thức getKey()getValue(). Nhờ đó bạn có ngay cả hai phần của cặp.

Duyệt bằng Iterator

Iterator<Map.Entry<String, String>> it = users.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
    // Thậm chí có thể xóa phần tử một cách an toàn:
    if (entry.getKey().startsWith("v")) {
        it.remove();
    }
}

6. Ví dụ thực tế: duyệt collection giúp gì cho ứng dụng

Ví dụ: in tất cả công việc của người dùng

List<String> tasks = new ArrayList<>();
tasks.add("Làm bài tập về nhà");
tasks.add("Vuốt ve con mèo");
tasks.add("Xem phim bộ");

System.out.println("Các công việc của bạn cho hôm nay:");
for (String task : tasks) {
    System.out.println("- " + task);
}

Bây giờ hãy xoá hết các công việc chứa từ "mèo":

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("mèo")) {
        it.remove();
    }
}
System.out.println("Còn lại các công việc:");
for (String task : tasks) {
    System.out.println("- " + task);
}

Ví dụ: duyệt các tên đăng nhập duy nhất qua Set

Set<String> logins = new HashSet<>();
logins.add("vasya");
logins.add("petya");
logins.add("masha");

for (String login : logins) {
    System.out.println("Người dùng: " + login);
}

Lưu ý: thứ tự in ra của Set có thể là bất kỳ!

Ví dụ: duyệt Map để hiển thị người dùng

Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");

for (Map.Entry<String, String> entry : users.entrySet()) {
    System.out.println("Tên đăng nhập: " + entry.getKey() + ", Email: " + entry.getValue());
}

7. Iterator.remove(): xóa phần tử an toàn

Một trong những lỗi phổ biến nhất của người mới — cố gắng xoá phần tử của collection trong lúc duyệt bằng for-each. Iterator giải quyết vấn đề này bằng remove().

Nó hoạt động thế nào?

  • Khi gọi it.remove(), phần tử hiện tại — phần tử vừa được next() trả về lần gần nhất — sẽ bị xóa.
  • Đây là cách an toàn: collection sẽ không ném ConcurrentModificationException.

Ví dụ:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) {
        it.remove(); // Xóa tất cả số chẵn
    }
}
System.out.println(numbers); // [1, 3, 5]

Sơ đồ duyệt collection

+---------+     +---------+     +---------+
| Element | --> | Element | --> | Element | ...
+---------+     +---------+     +---------+
     ^               ^
     |               |
   next()         next()

Iterator “bước” qua các phần tử cho đến khi hasNext() trả về false.

9. Những lỗi điển hình khi làm việc với Iterator và duyệt collection

Lỗi số 1: chỉnh sửa collection trong lúc duyệt bằng for-each.
Cố gắng xoá phần tử ngay trong for-each sẽ dẫn đến ConcurrentModificationException:

for (String task : tasks) {
    if (task.contains("mèo")) {
        tasks.remove(task); // BOOM! ConcurrentModificationException
    }
}

Hãy dùng Iterator và phương thức remove() của nó.

Lỗi số 2: gọi remove() trước next().
Trước hết cần lấy phần tử hiện tại qua next(), nếu không iterator không biết phải xóa gì.

Iterator<String> it = tasks.iterator();
it.remove(); // Lỗi! Cần gọi next() trước

Lỗi số 3: cố gắng duyệt Map trực tiếp trong for-each.
Map không triển khai Iterable trực tiếp — hãy dùng keySet(), values() hoặc entrySet().

Map<String, String> users = new HashMap<>();
// for (String entry : users) { ... } // Lỗi: không thể làm như vậy
for (Map.Entry<String, String> e : users.entrySet()) {
    // đúng
}

Lỗi số 4: thay đổi collection bên ngoài iterator trong khi đang duyệt bằng iterator.
Trong lúc duyệt, chỉ xoá phần tử qua it.remove(), không dùng các phương thức của collection.

Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
    String task = it.next();
    if (task.contains("mèo")) {
        tasks.remove(task); // Lỗi! Cần it.remove()
    }
}
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION