Khi chỉ số chỉ làm vướng víu
Vòng lặp for có biến đếm cho bạn một biến đếm, một điều kiện và một bước cập nhật. Nhưng rất nhiều khi bạn thật ra không quan tâm vị trí của phần tử: bạn chỉ muốn làm gì đó với từng phần tử, theo thứ tự, từ đầu đến cuối. Quản lý chỉ số cho việc đó là việc thừa, và đó cũng chính là chỗ lỗi lệch một đơn vị (off-by-one) hay chui vào.
Vòng lặp for-each (Java gọi là for nâng cao, enhanced for) bỏ hẳn biến đếm. Bạn đặt tên một biến, trỏ nó vào một collection, và vòng lặp lần lượt đưa cho bạn từng phần tử.
Cú pháp cơ bản
Dạng của nó là for (Type element : collection). Hãy đọc dấu hai chấm là chữ "trong":
Không có i, không có scores.length, không có scores[i]. Ở mỗi lần lặp, score chính là phần tử tiếp theo. Vòng lặp chạy một lần cho mỗi phần tử và tự dừng khi hết phần tử: bạn không thể chạy quá cuối hay bắt đầu sớm một phần tử.
Duyệt một list
Cùng vòng lặp đó dùng được với mọi thứ có thể duyệt (iterable), gồm List, Set và các kiểu collection khác. Kiểu của phần tử đứng trước tên biến:
Để ý rằng bạn không cần biết hay bận tâm langs dựa trên mảng, danh sách liên kết hay thứ gì khác: for-each hoạt động như nhau với tất cả. Đó là thế mạnh thật sự của nó: một cú pháp dễ đọc cho mọi collection.
var giúp bạn khỏi viết tên kiểu
Nếu kiểu của phần tử dài hoặc đã quá rõ, var cho phép trình biên dịch tự suy ra để bạn không phải lặp lại:
Không có var, biến lặp kia sẽ phải viết dài dòng là Map.Entry<String, Integer>. var giúp code dễ đọc, và kiểu vẫn được kiểm tra đầy đủ lúc biên dịch: đây không phải kiểu động lỏng lẻo.
Cái bẫy khi sửa collection
Đây là quy tắc ai cũng từng vấp: bạn không thể thêm hoặc xóa phần tử của một collection trong lúc vòng lặp for-each đang duyệt nó. Làm vậy sẽ ném ConcurrentModificationException:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // throws ConcurrentModificationException
}
}
Vòng lặp nhận ra list đã bị thay đổi bên dưới nó và dừng lại, thay vì lặng lẽ bỏ qua hoặc lặp lại phần tử. Để xóa an toàn, hãy chuyển sang dùng Iterator tường minh, vì nó có phương thức remove() mà vòng lặp biết đến:
Một cách viết tắt phổ biến là items.removeIf(item -> item.equals("b")), dùng biểu thức lambda để làm điều tương tự chỉ trong một dòng.
Chỉ đọc, không gán lại
Một giới hạn tinh tế khác: gán giá trị cho biến lặp chỉ thay đổi bản sao cục bộ, không thay đổi collection. Điều này khiến những người quen với ngôn ngữ mà biến lặp là tham chiếu trực tiếp bất ngờ:
Nếu cần ghi ngược vào mảng, bạn cần chỉ số, và như thế nghĩa là dùng vòng lặp for có biến đếm cổ điển: for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;. Với phần tử là đối tượng, bạn có thể thay đổi đối tượng mà biến trỏ tới (chẳng hạn gọi một setter), nhưng không thể thay thế nó trong collection.
break và continue vẫn hoạt động
for-each là một vòng lặp thật sự, nên break và continue hoạt động y như ở nơi khác: break thoát khỏi vòng lặp, continue nhảy sang phần tử tiếp theo:
Đoạn code này in ra keep rồi keep: nó bỏ qua "skip" và dừng ở "stop" trước khi tới "never". Vậy là bạn không bị buộc phải duyệt mọi phần tử; bạn chỉ đổi chỉ số lấy code gọn gàng hơn.
Tiếp theo: mảng
Bạn đã duyệt mảng vài lần mà chưa dừng lại tìm hiểu mảng thực sự là gì: những vùng chứa có kích thước cố định, đánh chỉ số, với thuộc tính .length. Trang tiếp theo quay lại từ đầu và nói kỹ về mảng: cách khai báo, giá trị mặc định ban đầu, và kích thước cố định của mảng khác với ArrayList có thể mở rộng như thế nào.
Câu hỏi thường gặp
Vòng lặp for-each trong Java là gì?
Vòng lặp for-each (còn gọi là for nâng cao, enhanced for) duyệt qua từng phần tử của mảng hoặc collection mà không cần biến đếm: for (Type item : collection) { ... }. Hãy đọc nó là "với mỗi item trong collection". Nó gọn hơn vòng lặp for có biến đếm khi bạn chỉ cần từng phần tử và không bao giờ cần chỉ số.
Vòng lặp for và foreach trong Java khác nhau thế nào?
Vòng lặp for cổ điển dùng biến đếm tường minh (for (int i = 0; i < arr.length; i++)), nên bạn điều khiển được chỉ số và chiều duyệt. Vòng lặp for-each, for (Type x : arr), không có chỉ số: nó đi qua mọi phần tử theo thứ tự. Dùng for-each khi chỉ đọc và duyệt từ đầu đến cuối; dùng vòng lặp có biến đếm khi bạn cần chỉ số, muốn bỏ qua phần tử, hoặc thay đổi cấu trúc của collection.
Vì sao vòng lặp for-each của tôi ném ConcurrentModificationException?
Vì bạn đã gọi add() hoặc remove() trên collection trong lúc đang duyệt nó bằng for-each. Vòng lặp phát hiện thay đổi cấu trúc và ném ngoại lệ để bảo vệ bạn khỏi hành vi không xác định. Để xóa phần tử an toàn, hãy dùng Iterator tường minh và phương thức remove() của nó, hoặc gom các phần tử cần xóa lại rồi xóa sau vòng lặp.