Menu

Duyệt collection trong Java: for-each, Iterator, forEach

Các cách duyệt collection trong Java: vòng lặp for-each, Iterator, vòng lặp theo chỉ số và phương thức forEach, cùng cách xóa phần tử an toàn khi đang duyệt.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một việc, nhiều công cụ

Bạn đã gom dữ liệu vào một ArrayList, một HashSet hay một HashMap, và giờ muốn đi qua từng phần tử. Java có vài cách để làm việc này. Chọn cách nào tùy vào việc bạn có cần chỉ số không, có cần xóa phần tử giữa vòng lặp không, và bạn thích cú pháp kiểu phương thức hay kiểu vòng lặp.

Tin vui là mọi Collection (list, set, queue) đều hỗ trợ cùng một vòng lặp for nâng cao, nên học một lần là bạn duyệt được tất cả theo cùng một cách.

Hãy đọc dấu hai chấm là "trong": "với mỗi lang trong langs". Bạn không bao giờ đụng đến chỉ số, nên không có gì để viết sai.

Vòng lặp for-each

Vòng lặp for nâng cao là lựa chọn mặc định khi bạn chỉ cần "làm gì đó với từng phần tử". Nó dễ đọc và hoạt động giống hệt nhau trên mọi loại collection. Ví dụ dưới đây dùng HashSet, loại hoàn toàn không có chỉ số:

Một điều cần nhớ về HashSet: nó không có thứ tự xác định, nên các phần tử có thể được in ra theo bất kỳ thứ tự nào. Dù vậy, for-each vẫn đi qua mỗi phần tử đúng một lần.

Khi bạn cần chỉ số

For-each cho bạn phần tử nhưng không cho vị trí của nó. Nếu bạn thật sự cần chỉ số, chẳng hạn để đánh số dòng hoặc để xem các phần tử kế bên, hãy dùng vòng lặp đếm với size() và get(i). Cách này dùng được với List vì list có vị trí; set và map không có chỉ số nên không áp dụng được.

Đừng dùng cách này chỉ vì thói quen. Nếu bạn không dùng i vào việc gì ngoài get(i), phiên bản for-each ngắn hơn và khó viết sai hơn.

Duyệt một Map

Map không phải là Collection, nên bạn không thể dùng for-each trực tiếp trên nó. Thay vào đó, bạn duyệt qua một trong ba view của nó. Phổ biến nhất là entrySet(), trả về từng cặp khóa và giá trị cùng lúc:

Nếu chỉ cần khóa, hãy duyệt ages.keySet(); nếu chỉ cần giá trị, hãy duyệt ages.values(). Nên dùng entrySet() khi cần cả hai: duyệt khóa rồi gọi ages.get(key) bên trong sẽ tra cứu thêm một lần ở mỗi lần lặp mà chẳng được lợi gì.

Iterator

For-each thực ra là cú pháp rút gọn (syntactic sugar) của một Iterator, một đối tượng đi qua collection từng phần tử một bằng hasNext() và next(). Bạn hiếm khi tự viết vòng lặp này, trừ một trường hợp quan trọng: đây là cách an toàn để xóa phần tử trong lúc duyệt.

it.remove() xóa phần tử mà next() vừa trả về, và iterator vẫn dùng tiếp được. Đây là cách duy nhất được cho phép để thay đổi collection trong một vòng lặp tự viết.

Cái bẫy: ConcurrentModificationException

Nếu bạn gọi add hoặc remove trên chính collection bên trong vòng lặp for-each, bạn sẽ gặp ConcurrentModificationException: iterator nhận ra collection đã bị thay đổi bên dưới nó và từ chối chạy tiếp. Đây là một trong những lỗi phổ biến nhất của người mới, và như mọi ngoại lệ lúc chạy, nó làm phương thức dừng lại nếu bạn không tránh được nó.

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
    if (n % 2 == 0) {
        nums.remove(Integer.valueOf(n));   // throws ConcurrentModificationException
    }
}

Cách sửa hầu như luôn là removeIf, vừa thể hiện ý định trong một dòng vừa lo phần duyệt giúp bạn:

removeIf dùng được với mọi Collection, nên cùng lời gọi đó cũng dọn dẹp được một HashSet.

Phương thức forEach

Mọi collection còn có phương thức forEach nhận một lambda và chạy nó trên từng phần tử. Đây là lựa chọn theo phong cách hàm, dạng biểu thức, thay cho vòng lặp, rất tiện cho những câu lệnh ngắn một dòng:

Lưu ý rằng Map.forEach nhận trực tiếp một lambda hai tham số (key, value), không cần entrySet(). Dùng forEach cho những tác vụ phụ nhanh gọn; quay lại vòng lặp for thông thường khi phần thân dài ra hoặc bạn cần break thoát sớm, điều mà lambda không làm được.

Tiếp theo: phương thức

Giờ bạn đã gói dữ liệu vào collection và duyệt qua nó bằng mọi cách Java hỗ trợ. Bước tiếp theo là đóng gói hành vi: tự viết phương thức để đặt tên cho một khối logic, truyền dữ liệu vào và dùng lại nó. Đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

Làm sao để duyệt một list trong Java?

Cách gọn nhất là vòng lặp for nâng cao (for-each): for (String s : list) { ... }. Cách này dùng được với mọi Collection, như ArrayList, HashSet và các loại khác. Chỉ dùng vòng lặp theo chỉ số với get(i) khi bạn thật sự cần vị trí, và dùng Iterator khi cần xóa phần tử trong lúc lặp.

Làm sao để duyệt một HashMap trong Java?

Map không phải là Collection, nên bạn duyệt qua một trong các view của nó. Lựa chọn thường dùng là for (Map.Entry<K, V> e : map.entrySet()), cho bạn cả khóa (e.getKey()) lẫn giá trị (e.getValue()) trong một lượt duyệt. Bạn cũng có thể duyệt map.keySet() để lấy khóa hoặc map.values() để lấy giá trị.

Vì sao lại gặp ConcurrentModificationException khi lặp?

Bạn đã gọi add hoặc remove trên collection trong khi vòng lặp for-each đang duyệt nó. Bên dưới, vòng lặp này dùng một Iterator, và iterator này phát hiện collection đã bị thay đổi cấu trúc. Cách sửa: xóa thông qua phương thức remove() của chính Iterator, hoặc gọi removeIf(...) thay vì tự viết vòng lặp.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu