Command Palette

Search for a command to run...

[Java Basics] Vòng lặp trong Java: for, while và do-while

Mọi chương trình từ đầu series tới giờ đều chạy từ dòng đầu tới dòng cuối, đúng một lần. Vòng lặp là thứ phá vỡ quy tắc đó: nó lặp lại một block cho tới khi condition không còn đúng nữa. Java có ba vòng lặp — for, whiledo-while — cộng thêm enhanced for để duyệt qua một tập giá trị.

Cả ba đều diễn đạt được mọi vòng lặp, nên câu hỏi thú vị không phải là cái nào làm được, mà là cái nào nói rõ ý định và cái nào giấu bug. Bài này mổ xẻ header của for rồi chứng minh thứ tự thực thi bằng cách in ra từ bên trong từng phần, sau đó tái hiện lại mọi lỗi kinh điển: off-by-one, dấu chấm phẩy thừa, loop variable biến mất, và ba cách viết ra một vòng lặp không bao giờ dừng. Mọi chương trình và mọi thông báo lỗi bên dưới đều được compile và chạy thật trên OpenJDK 21.0.6.

Vòng lặp trong Java: for, while và do-while

Bắt đầu với for, vì header của nó nhồi ba việc khác nhau vào chung một dòng.

Vòng lặp for, từng phần một

for (int i = 1; i <= 5; i++) {
    System.out.println("line " + i);
}
line 1
line 2
line 3
line 4
line 5

Header chứa ba statement, ngăn cách bởi hai dấu chấm phẩy:

PhầnTrong ví dụChạy khi nào
initializationint i = 1một lần, trước khi vòng lặp bắt đầu
conditioni <= 5trước mỗi lần lặp, kể cả lần đầu tiên
updatei++sau mỗi lần body chạy xong

Body là block trong ngoặc nhọn. Nó là phần thứ tư, và là thứ mà ba phần kia sinh ra để phục vụ.

Header không hề bị giới hạn ở việc đếm tăng một đơn vị. Update là một statement bình thường, nên nó trừ, nhân hay làm bất cứ việc gì cũng được:

int sum = 0;
for (int i = 1; i <= 100; i++) {
    sum += i;
}
System.out.println("sum 1..100 = " + sum);

for (int i = 5; i >= 1; i--) {
    System.out.print(i + " ");
}
System.out.println();

for (int i = 0; i < 10; i += 3) {
    System.out.print(i + " ");
}
System.out.println();
sum 1..100 = 5050
5 4 3 2 1 
0 3 6 9 

Thứ tự chạy chính xác của các phần

Phần lớn tài liệu dừng lại ở bảng phía trên, và bỏ sót đúng cái chi tiết quan trọng nhất: update chạy sau body, còn condition được kiểm tra lại trước mỗi lần lặp, kể cả lần đầu. Thay vì khẳng định suông, hãy in ra từ bên trong từng phần. Ở đây mỗi phần của header là một lời gọi method, nên phần nào cũng tự khai báo khi nó chạy:

public class ForOrder {
    static int init() {
        System.out.println("init");
        return 0;
    }

    static boolean condition(int i) {
        System.out.println("  condition: i < 3 -> " + (i < 3));
        return i < 3;
    }

    static void body(int i) {
        System.out.println("    body: i = " + i);
    }

    static int update(int i) {
        System.out.println("  update: i becomes " + (i + 1));
        return i + 1;
    }

    public static void main(String[] args) {
        for (int i = init(); condition(i); i = update(i)) {
            body(i);
        }
        System.out.println("loop finished");
    }
}
init
  condition: i < 3 -> true
    body: i = 0
  update: i becomes 1
  condition: i < 3 -> true
    body: i = 1
  update: i becomes 2
  condition: i < 3 -> true
    body: i = 2
  update: i becomes 3
  condition: i < 3 -> false
loop finished

Bốn phần của header for và thứ tự JVM chạy chúng

Bốn điều rút ra từ đoạn transcript đó:

  • init in ra đúng một lần, ở trên cùng, trước lần kiểm tra condition đầu tiên.
  • Thứ tự bên trong vòng lặp là condition, body, update — không bao giờ là body, condition, update. Cái update mà bạn tưởng là "đầu vòng sau" thực ra xảy ra ở cuối vòng này.
  • Condition được tính bốn lần cho ba lần lặp. Lần cuối cùng chính là lần kết thúc vòng lặp, và lúc đó counter đã được update lên 3 rồi.
  • Body không bao giờ nhìn thấy i = 3. Khi counter chạm tới giá trị biên thì vòng lặp đã xong.

Điểm cuối cùng chính là lý do for (int i = 0; i < n; i++) chạy đúng n lần với i nhận các giá trị từ 0 tới n - 1, và cũng là nền tảng của phần off-by-one bên dưới.

Cả ba phần trong header đều có thể bỏ trống

Hai dấu chấm phẩy là bắt buộc. Những gì nằm giữa chúng thì không. Bỏ trống cả ba, bạn có vòng lặp vô hạn kinh điển:

public class Forever {
    public static void main(String[] args) {
        int tick = 0;
        for (;;) {
            System.out.println("tick " + tick);
            tick++;
        }
    }
}
tick 0
tick 1
tick 2
tick 3
tick 4

Đó là năm dòng đầu tiên. Process bị kill sau hai giây, và tới lúc đó nó đã in ra hơn một triệu dòng — con số mang tính tham khảo và phụ thuộc vào máy, không phải đặc tính của vòng lặp. Condition bỏ trống được hiểu là true, nên không có gì dừng vòng lặp lại được. for (;;)while (true) compile ra cùng một thứ, và javac coi cả hai là vô hạn chắc chắn — statement viết sau chúng bị từ chối:

while (true) {
    System.out.println("x");
}
System.out.println("after");
Unreachable.java:6: error: unreachable statement
        System.out.println("after");
        ^
1 error

for (;;) cho ra thông báo y hệt. Bỏ trống từng phần riêng lẻ cũng được, chỉ là counter chuyển ra chỗ khác sống:

int i = 0;
for (; i < 3; ) {
    System.out.println("i = " + i);
    i++;
}
i = 0
i = 1
i = 2

Theo chiều ngược lại, phần initialization và phần update đều nhận một danh sách ngăn cách bởi dấu phẩy, nên một vòng lặp có thể điều khiển hai counter cùng lúc:

for (int i = 0, j = 10; i < j; i++, j--) {
    System.out.println("i=" + i + " j=" + j);
}
i=0 j=10
i=1 j=9
i=2 j=8
i=3 j=7
i=4 j=6

Có hai giới hạn với dấu phẩy đó. Type chỉ được viết một lần và áp dụng cho mọi tên trong danh sách, nên int i = 0, long j = 10 không compile được. Và dấu phẩy trong header for không phải comma operator tổng quát như trong C — Java chỉ cho phép nó ở đúng hai vị trí này, và chỉ dùng để khai báo trong initializer hoặc để viết statement expression trong update.

Counter sống ở đâu

int i = 0 viết trong header sẽ khai báo i trong một scope chỉ phủ header và body, không hơn. Qua dấu ngoặc nhọn đóng là cái tên đó biến mất:

public class ScopeError {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        System.out.println("after the loop, i = " + i);
    }
}
ScopeError.java:6: error: cannot find symbol
        System.out.println("after the loop, i = " + i);
                                                    ^
  symbol:   variable i
  location: class ScopeError
1 error

Nếu bạn cần counter sau vòng lặp — ví dụ để báo lại chỗ mà một phép tìm kiếm đã dừng — hãy khai báo nó trước vòng lặp và trong header chỉ gán giá trị:

int i;
for (i = 0; i < 3; i++) {
    System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
i = 0
i = 1
i = 2
after the loop, i = 3

Để ý giá trị: 3, không phải 2. Counter giữ lại đúng giá trị đầu tiên làm condition sai. Khai báo ngay trong header vẫn là lựa chọn mặc định vì nó giữ một cái tên dùng xong bỏ đi ra khỏi scope bên ngoài, và compiler chặn bạn đọc nhầm nó.

while: kiểm tra trước, và có thể không chạy lần nào

while chính là for với phần initialization và phần update bị lấy ra khỏi header, đem trả về chỗ mà các statement bình thường vẫn nằm:

int i = 1;
while (i <= 3) {
    System.out.println("line " + i);
    i++;
}
line 1
line 2
line 3

Về cấu trúc thì giống hệt for (int i = 1; i <= 3; i++), và tệ hơn cho đúng công việc này: ba mảnh vốn thuộc về nhau nay nằm rải ở ba chỗ, và quên mất i++ thì bạn nhận một vòng lặp vô hạn chứ không phải một lỗi compile.

while phát huy giá trị khi không có counter nào cả — khi số lần lặp là bao nhiêu tuỳ vào dữ liệu:

int n = 90210;
int digits = 0;
while (n > 0) {
    n = n / 10;
    digits++;
}
System.out.println("digits = " + digits);
digits = 5

Không có gì ở đây biết trước đáp án là năm. Condition kiểm tra chính cái trạng thái mà body vừa tạo ra, và đó đúng là hình dạng mà while sinh ra để làm. Vì condition được kiểm tra trước lần chạy đầu tiên, một vòng while có thể chạy không lần nào — while (n > 0) với n = 0 không làm gì cả, và thường thì đó chính là điều bạn muốn.

do-while: chạy trước, kiểm tra sau

do-while chuyển condition xuống dưới đáy, nên body luôn chạy ít nhất một lần:

while chạy body không lần nào còn do-while chạy một lần

int a = 10;
while (a < 5) {
    System.out.println("body");
    a++;
}
System.out.println("a = " + a);

int b = 10;
do {
    System.out.println("body");
    b++;
} while (b < 5);
System.out.println("b = " + b);
a = 10
body
b = 11

Cùng một condition, cùng một giá trị khởi đầu, và 10 < 5 sai ngay từ lần kiểm tra đầu tiên. Vòng while không in gì và để a nguyên ở 10. Vòng do-while in body một lần và để b ở 11, vì nó chưa hề hỏi tới condition cho tới khi body đã chạy xong.

Dấu chấm phẩy đứng sau while (b < 5) là một phần của statement chứ không phải gõ thừa. Thiếu nó là lỗi cú pháp:

do {
    System.out.println(n);
    n++;
} while (n < 3)
System.out.println("done");
DoNoSemi.java:7: error: ';' expected
        } while (n < 3)
                       ^
1 error

Hình dạng mà do-while sinh ra để phục vụ là "hỏi đã, rồi mới quyết định có hỏi lại không" — một menu, hoặc một lời nhắc phải hiện ra trước khi có gì đó để kiểm tra:

import java.util.Scanner;

public class AgePrompt {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int age;
        do {
            System.out.print("Age (1-129): ");
            age = Integer.parseInt(sc.nextLine().trim());
        } while (age < 1 || age > 129);
        System.out.println("accepted: " + age);
    }
}
printf '0\n200\n25\n' | java AgePrompt
Age (1-129): Age (1-129): Age (1-129): accepted: 25

Viết bằng while, chương trình đó buộc phải hoặc lặp lại lời nhắc ở phía trên vòng lặp, hoặc khởi đầu bằng một giá trị cố tình không hợp lệ. do-while nói đúng điều nó muốn nói.

Nên dùng vòng lặp nào?

Tình huốngVòng lặpVì sao
Số lần lặp đã biết trướcforcounter, giới hạn và bước nhảy nằm chung trong header
Duyệt mọi phần tử của array hoặc của một Iterableenhanced forkhông có index để mà làm sai
Số lần lặp chưa biết và có thể bằng khôngwhilecondition được kiểm tra trước lần chạy đầu
Việc phải xảy ra ít nhất một lần — menu, lời nhắc nhập liệu, thử lạido-whilebody chạy trước khi condition được hỏi tới

Hãy đọc bảng đó như một quyết định chứ không phải một bảng xếp hạng: dùng for khi có counter, dùng enhanced for khi đang duyệt một tập giá trị, dùng while khi điều kiện dừng phụ thuộc vào dữ liệu, và dùng do-while khi lần chạy đầu tiên phải xảy ra trước lúc có gì đó để kiểm tra.

Enhanced for (for-each)

Khi vòng lặp chỉ tồn tại để ghé qua từng phần tử của một array thì counter chỉ là việc sổ sách. Enhanced for bỏ hẳn nó đi:

for có index và for-each trên cùng một array

int[] scores = {70, 85, 90};

for (int s : scores) {
    System.out.println("  value " + s);
}
  value 70
  value 85
  value 90

Đọc for (int s : scores) là "với mỗi int s trong scores". Type đứng trước tên là type của phần tử, còn biểu thức sau dấu hai chấm là thứ đang được duyệt. Không có counter để khởi tạo, không có giới hạn để so sánh, và vì thế không có off-by-one nào để mà mắc.

Cái bạn đánh đổi là index. Nó không tồn tại bên trong vòng lặp, và đòi nó là lỗi compile chứ không phải một bất ngờ lúc runtime:

int[] scores = {70, 85, 90};
for (int s : scores) {
    System.out.println(i + ": " + s);
}
ForEachNoIndex.java:5: error: cannot find symbol
            System.out.println(i + ": " + s);
                               ^
  symbol:   variable i
  location: class ForEachNoIndex
1 error

Giới hạn thứ hai mới là cái làm nhiều người vấp. s là một bản copy của phần tử, không phải ô nhớ trong array. Gán cho nó chỉ đổi bản copy và không đổi gì khác:

int[] scores = {70, 85, 90};

for (int s : scores) {
    s = s * 2;
}
System.out.println("after 's = s * 2' in a for-each: " + Arrays.toString(scores));

for (int i = 0; i < scores.length; i++) {
    scores[i] = scores[i] * 2;
}
System.out.println("after 'scores[i] = scores[i] * 2': " + Arrays.toString(scores));
after 's = s * 2' in a for-each: [70, 85, 90]
after 'scores[i] = scores[i] * 2': [140, 170, 180]

Vòng lặp đầu chạy ba lần và không làm được gì. Vòng thứ hai ghi qua index và nhân đôi mọi phần tử. Vậy nên: for-each để đọc, for có index để ghi.

Với một array các object thì quy tắc vẫn thế nhưng đọc ra hơi khác, vì thứ được copy là reference. Gọi method trên loop variable là chạm tới đúng object mà array đang giữ; còn gán một object mới cho loop variable thì không:

StringBuilder[] names = { new StringBuilder("An"), new StringBuilder("Binh") };

for (StringBuilder sb : names) {
    sb.append("!");
}
System.out.println("after append: " + Arrays.toString(names));

for (StringBuilder sb : names) {
    sb = new StringBuilder("replaced");
}
System.out.println("after assignment: " + Arrays.toString(names));
after append: [An!, Binh!]
after assignment: [An!, Binh!]

Enhanced for không chỉ dùng cho array. Nó nhận mọi thứ implement java.lang.Iterable, tức là mọi collection trong java.util — một bài sau trong series sẽ giới thiệu chúng đàng hoàng. Thứ nó không nhận là String:

for (char c : "abc") {
    System.out.println(c);
}
StringForEach.java:3: error: for-each not applicable to expression type
        for (char c : "abc") {
                      ^
  required: array or java.lang.Iterable
  found:    String
1 error

Thông báo nói ra đúng quy tắc: array hoặc Iterable, mà String thì không phải cái nào. Hãy duyệt "abc".toCharArray() nếu bạn cần từng ký tự.

Off-by-one: < hay <= khi so với length

Index của array chạy từ 0 tới length - 1. Chính length đã nằm ngoài phần tử cuối cùng, nên một vòng lặp viết với <= sẽ bước ra ngoài array ở lần chạy cuối:

Duyệt dải index với i nhỏ hơn length so với i nhỏ hơn hoặc bằng length

int[] a = {10, 20, 30};

System.out.println("i < a.length");
for (int i = 0; i < a.length; i++) {
    System.out.println("  a[" + i + "] = " + a[i]);
}

System.out.println("i <= a.length");
for (int i = 0; i <= a.length; i++) {
    System.out.println("  a[" + i + "] = " + a[i]);
}
i < a.length
  a[0] = 10
  a[1] = 20
  a[2] = 30
i <= a.length
  a[0] = 10
  a[1] = 20
  a[2] = 30
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at OffByOne.main(OffByOne.java:12)

Ba lần chạy đầu tiên của hai vòng lặp không thể phân biệt được. Lần thứ tư mới là chỗ chúng tách nhau, và thông báo exception nói thẳng vấn đề ra: Index 3 out of bounds for length 3. Nó gọi tên cả index bị đòi lẫn length khiến index đó không hợp lệ, đủ để tìm ra bug mà không cần mở debugger.

Đây là lý do i < array.length được viết y như vậy ở khắp nơi trong Java, và là lý do đi chệch khỏi idiom đó thì đáng phải nhìn lại lần nữa:

  • i < length ghé qua mọi phần tử đúng một lần.
  • i <= length ném exception ở lần chạy cuối.
  • i < length - 1 bỏ qua phần tử cuối trong im lặng, và cái này còn tệ hơn — không có gì sập, chỉ là kết quả sai.
  • Bắt đầu từ 1 thay vì 0 thì bỏ qua phần tử đầu, cũng trong im lặng.

Hai biến thể im lặng đó chính là lý do nên ưu tiên enhanced for mỗi khi bạn chỉ cần đọc từng phần tử: một vòng lặp không có index thì không thể làm sai index của nó.

Vòng lặp vô hạn và ba nguyên nhân thường gặp

Một vòng lặp kết thúc khi condition của nó thành sai. Bất cứ thứ gì ngăn điều đó xảy ra đều cho bạn một vòng lặp chạy tới khi bạn kill process.

Condition không bao giờ thay đổi

Nguyên nhân phổ biến nhất là một body quên đụng vào thứ mà condition đang kiểm tra:

int n = 3;
while (n > 0) {
    System.out.println("n is still " + n);
    // forgot n--;
}
System.out.println("done");
n is still 3
n is still 3
n is still 3
n is still 3

Bốn dòng trong hơn một triệu dòng mà process đã in ra khi bị kill sau hai giây; con số đó mang tính tham khảo và phụ thuộc vào máy, còn việc nó không dừng thì không. for chống được đúng lỗi này vì update nằm trong header, chỗ mà sự vắng mặt của nó lộ ra ngay; while thì không, và đó là lập luận chính cho việc dùng for mỗi khi có counter.

Update đi sai hướng

Nguyên nhân thứ hai là update đẩy counter ra xa giới hạn thay vì tiến về phía nó. Cái này thú vị vì trong Java nó không phải lúc nào cũng vô hạn:

long iterations = 0;
int i;
for (i = 10; i > 0; i++) {
    iterations++;
}
System.out.println("the loop ended after " + iterations + " iterations");
System.out.println("i is now " + i);
the loop ended after 2147483638 iterations
i is now -2147483648

Vòng lặp có dừng thật, sau 2.147.483.638 lần chạy. i leo tới Integer.MAX_VALUE, rồi i++ kế tiếp cuốn nó về Integer.MIN_VALUE — đúng cái int overflow im lặng mà bài 6 đã nói — và giá trị đó không lớn hơn không, nên condition cuối cùng cũng thành sai. Một vòng lặp kết thúc nhờ counter của nó bị tràn không phải là một vòng lặp chạy đúng; đó là một bug tình cờ có điểm dừng.

Cộng dồn số thực không bao giờ rơi đúng vào giá trị cần

Nguyên nhân thứ ba làm bất ngờ cả những người không làm gì sai rõ ràng. Một counter kiểu double với phép so sánh == trông rất hợp lý mà lại không hợp lý:

double x = 0.0;
int guard = 0;
while (x != 1.0 && guard < 12) {
    x += 0.1;
    guard++;
    System.out.printf("step %2d  x = %.17f  x == 1.0 ? %b%n", guard, x, x == 1.0);
}
System.out.println("guard stopped the loop at step " + guard);
step  1  x = 0.10000000000000000  x == 1.0 ? false
step  2  x = 0.20000000000000000  x == 1.0 ? false
step  3  x = 0.30000000000000004  x == 1.0 ? false
step  4  x = 0.40000000000000000  x == 1.0 ? false
step  5  x = 0.50000000000000000  x == 1.0 ? false
step  6  x = 0.60000000000000000  x == 1.0 ? false
step  7  x = 0.70000000000000000  x == 1.0 ? false
step  8  x = 0.79999999999999990  x == 1.0 ? false
step  9  x = 0.89999999999999990  x == 1.0 ? false
step 10  x = 0.99999999999999990  x == 1.0 ? false
step 11  x = 1.09999999999999990  x == 1.0 ? false
step 12  x = 1.20000000000000000  x == 1.0 ? false

Bước 10 lẽ ra là chỗ vòng lặp phải kết thúc. x đang là 0.99999999999999990, không bằng 1.0, nên phép kiểm tra sai và bước 11 phóng thẳng qua đích. Nếu không có vế guard < 12, vòng lặp này không bao giờ dừng. 0.1 không có biểu diễn chính xác trong hệ nhị phân dấu chấm động, và mười phép cộng không chính xác thì không cộng lại thành đúng 1.0.

⚠️ Đừng bao giờ điều khiển vòng lặp bằng == hay != trên double hoặc float. Hãy đếm bằng int rồi tính ra giá trị thực bên trong body — double x = i / 10.0; — hoặc so sánh bằng < và chấp nhận một sai số.

Dấu chấm phẩy thừa

Một dấu chấm phẩy đứng một mình là một statement hợp lệ của Java và nó không làm gì cả. Đặt một cái ngay sau header của vòng lặp thì nó trở thành toàn bộ body, còn block bạn định lặp lại chỉ chạy một lần, sau khi vòng lặp đã xong. Đôi khi compiler bắt được nó một cách tình cờ:

int sum = 0;
for (int i = 1; i <= 3; i++);
    sum += i;
System.out.println("sum = " + sum);
StraySemicolon.java:5: error: cannot find symbol
            sum += i;
                   ^
  symbol:   variable i
  location: class StraySemicolon
1 error

Dòng sum += i; nằm ngoài vòng lặp, nên i khai báo trong header không còn trong scope ở đó. Chẩn đoán đó đúng nhưng nó chỉ vào triệu chứng chứ không phải nguyên nhân. Khai báo counter ra ngoài header thì đúng bug đó compile sạch sẽ:

int sum = 0;
int i;
for (i = 1; i <= 3; i++);
    sum += i;
System.out.println("i   = " + i);
System.out.println("sum = " + sum);
i   = 4
sum = 4

Đáp án mong đợi là 6. Vòng lặp chạy ba lần mà không làm gì, để i ở 4, rồi sum += i chạy đúng một lần. Cách thụt lề nói một đằng còn dấu chấm phẩy nói một nẻo; Java nghe theo dấu chấm phẩy. Ngay cả javac -Xlint:all cũng compile file đó mà không cảnh báo gì.

Trên while thì cùng lỗi gõ đó không chỉ cho kết quả sai, mà treo luôn:

int n = 0;
while (n < 3);
{
    System.out.println("n = " + n);
    n++;
}
System.out.println("done");

Đoạn đó compile sạch, và chạy lên thì cho ra đúng 0 byte output và không bao giờ trả về. Statement rỗng chính là body, nên n không bao giờ tăng và n < 3 đúng mãi mãi. Block bên dưới giờ chỉ là một block bình thường mà chương trình không bao giờ chạm tới. Nếu chương trình của bạn treo mà không in ra gì cả, dấu chấm phẩy thừa sau header while là thứ đáng kiểm tra đầu tiên.

Scope và shadowing của loop variable

Counter khai báo trong header for chỉ có scope trong vòng lặp, nghĩa là hai vòng lặp liền nhau đều dùng được tên i mà không đụng nhau. Quy tắc đó cũng cắt theo chiều ngược lại: bạn không dùng lại được một cái tên mà một local variable bao ngoài đang giữ, vì Java không cho một local shadow một local khác:

int i = 99;
for (int i = 0; i < 2; i++) {
    System.out.println(i);
}
Redeclare.java:4: error: variable i is already defined in method main(String[])
        for (int i = 0; i < 2; i++) {
                 ^
1 error

Với một field thì lại khác — một local variable được phép shadow field, và bên trong vòng lặp thì local thắng:

public class Shadow {
    static int i = 99;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            System.out.println("inside the loop, i = " + i);
        }
        System.out.println("the field i is still " + i);
    }
}
inside the loop, i = 0
inside the loop, i = 1
the field i is still 99

Nó compile được, chạy được, và là cách chắc chắn để làm người đọc rối. Hãy đặt cho counter một cái tên mà class bao ngoài chưa dùng.

Đọc cho tới khi gặp sentinel

Đây là pattern nối while với input thật: đọc một giá trị trước vòng lặp, rồi đọc giá trị kế tiếp ở cuối body, để condition lúc nào cũng có sẵn thứ để kiểm tra.

import java.util.Scanner;

public class Sentinel {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int total = 0;
        int count = 0;

        System.out.print("score (or 'done'): ");
        String line = sc.nextLine().trim();

        while (!line.equals("done")) {
            total += Integer.parseInt(line);
            count++;
            System.out.print("score (or 'done'): ");
            line = sc.nextLine().trim();
        }

        System.out.println();
        System.out.println("read " + count + " scores, total " + total);
    }
}
printf '7\n8\n9\ndone\n' | java Sentinel
score (or 'done'): score (or 'done'): score (or 'done'): score (or 'done'): 
read 3 scores, total 24

Các giá trị gõ vào không hiện ra vì input đến từ pipe chứ không phải bàn phím. Thứ bạn thấy là output của chính chương trình: bốn lời nhắc — ba cho ba điểm số cộng thêm một cái nhận done — và phần tổng kết.

Đọc cùng một biến ở hai chỗ, một lần trước vòng lặp và một lần ở cuối body, là cái giá phải trả để giữ phép kiểm tra sentinel nằm đúng chỗ của nó là condition. Cách viết còn lại, đọc một lần ở giữa vòng lặp rồi thoát ra sớm khi gặp sentinel, cần một statement mà bài này chưa nói tới.

FAQ

Vì sao vòng lặp của tôi chạy dư một lần?

Gần như luôn là một dấu <= nằm ở chỗ đáng lẽ phải là <. for (int i = 0; i <= n; i++) chạy n + 1 lần, và trên một array thì nó ném ArrayIndexOutOfBoundsException ở lần chạy cuối. Cứ tính số lần lặp của for (int i = a; i < b; i++) bằng b - a thì phép tính đó thôi không còn là đoán mò.

Tôi có thể đổi counter ngay trong body không?

Được, và update trong header vẫn chạy chồng lên thay đổi của bạn:

for (int i = 0; i < 5; i++) {
    System.out.println("i = " + i);
    i++;
}
i = 0
i = 2
i = 4

Cả i++ trong body lẫn i++ trong header đều nổ, nên counter tiến hai đơn vị mỗi vòng. Nó hợp lệ và nó là cách chắc chắn làm vòng lặp trở nên khó đọc. Nếu bước nhảy thật sự là hai thì hãy viết i += 2 trong header, chỗ mà người đọc nhìn thấy.

for có nhanh hơn while không?

Không — hai cái không chỉ tương đương, chúng compile ra bytecode giống hệt nhau. Hai method sau cộng cùng một dãy số:

static int f() { int s = 0; for (int i = 0; i < 10; i++) { s += i; } return s; }
static int w() { int s = 0; int i = 0; while (i < 10) { s += i; i++; } return s; }
javac Shapes.java && javap -c Shapes
  static int f();
    Code:
       0: iconst_0
       1: istore_0
       2: iconst_0
       3: istore_1
       4: iload_1
       5: bipush        10
       7: if_icmpge     20
      10: iload_0
      11: iload_1
      12: iadd
      13: istore_0
      14: iinc          1, 1
      17: goto          4
      20: iload_0
      21: ireturn

w() dịch ngược ra đúng từng đó lệnh, khớp từng offset. do-while mới là cái khác biệt: vì phép kiểm tra nằm dưới đáy, nó chỉ cần một lệnh nhảy có điều kiện mỗi vòng thay vì một lệnh so sánh rồi nhảy cộng thêm một goto vô điều kiện — và đó không phải lý do để chọn nó. Chi phí cấu trúc của một vòng lặp là số lần body của nó chạy, chứ không phải keyword nào mở đầu: duyệt n phần tử là n đơn vị công việc, viết kiểu gì cũng thế.

Vì sao tôi không dùng được counter sau vòng lặp?

for (int i = ...) khai báo i trong một scope kết thúc ở dấu ngoặc nhọn đóng, nên qua đó cái tên không còn tồn tại — javac báo cannot find symbol. Hãy khai báo nó trước vòng lặp và chỉ gán trong header. Nó sẽ giữ giá trị đầu tiên làm condition sai, tức là quá một bước so với giá trị cuối cùng mà body nhìn thấy.

Có viết được for-each trên String không?

Không viết trực tiếp được. Enhanced for nhận một array hoặc một thứ implement Iterable, mà String thì không phải cái nào, nên for (char c : "abc") hỏng với for-each not applicable to expression type. Hãy dùng for (char c : "abc".toCharArray()), hoặc một vòng lặp có index với charAt(i) khi bạn cần cả vị trí.

Kết luận

Một vòng lặp gồm bốn phần — initialization, condition, body, update — và for, while, do-while chỉ khác nhau ở chỗ viết bốn phần đó ở đâu và kiểm tra condition vào lúc nào. for gom cả ba phần header vào một chỗ, nên nó là lựa chọn đúng mỗi khi có counter. while kiểm tra trước và có thể chạy không lần nào. do-while chạy trước và luôn chạy ít nhất một lần. Enhanced for vứt bỏ index, và vứt luôn cả họ hàng lỗi off-by-one, đổi lại là không ghi ngược được vào array.

Mọi vòng lặp trong bài này đều chạy body từ trên xuống dưới, lần nào cũng thế. Bài kế tiếp thêm vào những statement thay đổi điều đó: vòng lặp lồng nhau, cùng với breakcontinue để thoát sớm khỏi một vòng lặp hoặc bỏ qua phần còn lại của một lượt chạy.

Bài viết liên quan

[Java Basics] Phương thức trong Java: khai báo và gọi method

Cách khai báo và gọi method trong Java: các thành phần của một declaration, static method so với instance method và lỗi non-static method cannot be referenced from a static context, return statement, call stack và StackOverflowError, cách đọc stack trace, Javadoc, cùng những lỗi compiler thật mà người mới hay gặp.

[Java Basics] Tính đa hình trong Java: overriding và overloading

Tính đa hình trong Java trên OpenJDK 21: quy tắc method overriding, @Override, dynamic dispatch chứng minh bằng javap, overriding và overloading, field hiding, static hiding, upcasting và downcasting, cùng cái bẫy trong constructor.

[Java Basics] Mảng một chiều trong Java: khai báo, khởi tạo và duyệt mảng

Hướng dẫn đầy đủ về mảng một chiều trong Java - mọi cách khai báo, giá trị mặc định, field length, index và ArrayIndexOutOfBoundsException, duyệt mảng bằng for và for-each, cái bẫy reference, copy thật với Arrays.copyOf và System.arraycopy, cùng Arrays.equals, mọi ví dụ đều compile và chạy trên JDK 21.

[Java Basics] Biến và kiểu dữ liệu trong Java: primitive vs reference

Hướng dẫn đầy đủ về biến và kiểu dữ liệu trong Java - 8 kiểu primitive cùng range thật, reference type, stack vs heap, literal, integer overflow, var, final và autoboxing, mọi ví dụ đều được compile và chạy trên JDK 21.