Command Palette

Search for a command to run...

[Java Basics] Bài tập tổng hợp về câu lệnh điều kiện và vòng lặp

Ba bài trước đã giới thiệu ifswitch, ba dạng vòng lặp, cùng vòng lặp lồng nhau với breakcontinue. Bài này không thêm cú pháp mới nào cả. Nó là mười sáu bài tập dựng hoàn toàn từ những thứ đó, cộng với variable, operator, String và Scanner ở các bài trước.

Mọi lời giải bên dưới đều được compile và chạy thật trên OpenJDK 21.0.6, và mọi khối output đều là output thật của chương trình, dán nguyên văn. Ở những bài có một lời giải sai nổi tiếng, phiên bản sai cũng được chạy thật, nên cả phần thất bại cũng là output thật chứ không phải mô tả suông.

Mười sáu bài tập Java về điều kiện và vòng lặp

Bốn mức, khó dần về sau. Mỗi bài gồm đề bài, output chính xác cần đạt được, một gợi ý, lời giải, và lỗi mà đa số người mắc phải đúng ở bài đó.

Khởi động: các điều kiện

Năm bài ngắn. Bài nào cũng xoay quanh việc viết đúng điều kiện và đặt đúng thứ tự các nhánh. Hãy tự viết trước khi đọc lời giải — giá trị nằm ở lần thử, không nằm ở đáp án.

Bài 1 — Chẵn hay lẻ, tính cả số âm

Đề bài. Cho int n = -7;, in ra số đó chẵn hay lẻ, rồi in -7 % 2 và kết quả so sánh nó với 1. Chương trình phải đúng với cả số âm.

Output mong đợi

-7 is odd
-7 % 2 = -1
-7 % 2 == 1 ? false

Gợi ý. Trong Java, kết quả của % mang dấu của operand bên trái.

Lời giải

public class EvenOdd {
    public static void main(String[] args) {
        int n = -7;

        if (n % 2 == 0) {
            System.out.println(n + " is even");
        } else {
            System.out.println(n + " is odd");
        }

        System.out.println("-7 % 2 = " + (-7 % 2));
        System.out.println("-7 % 2 == 1 ? " + (-7 % 2 == 1));
    }
}

Lỗi thường gặp. Viết if (n % 2 == 1) cho trường hợp lẻ. -7 % 2-1 chứ không phải 1, nên mọi số âm lẻ đều bị báo là chẵn. Hãy test == 0 và để else gánh trường hợp lẻ.

Bài 2 — FizzBuzz

Đề bài. Với i chạy từ 1 tới 15: in Fizz khi i chia hết cho 3, Buzz khi chia hết cho 5, FizzBuzz khi chia hết cho cả hai, còn lại thì in chính con số đó.

Output mong đợi

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

Gợi ý. 15 chia hết cho 3, cho 5 và cho 15, mà trong một chuỗi if / else if chỉ một nhánh được chạy.

Hai thứ tự test của FizzBuzz và output của từng cách

Lời giải — đặt điều kiện hẹp nhất lên trước

public class FizzBuzz {
    public static void main(String[] args) {
        for (int i = 1; i <= 15; i++) {
            if (i % 15 == 0) {
                System.out.println("FizzBuzz");
            } else if (i % 3 == 0) {
                System.out.println("Fizz");
            } else if (i % 5 == 0) {
                System.out.println("Buzz");
            } else {
                System.out.println(i);
            }
        }
    }
}

Lời giải — ghép String từ hai test độc lập

public class FizzBuzzBuild {
    public static void main(String[] args) {
        for (int i = 1; i <= 15; i++) {
            String out = "";
            if (i % 3 == 0) out += "Fizz";
            if (i % 5 == 0) out += "Buzz";
            if (out.isEmpty()) out = "" + i;
            System.out.println(out);
        }
    }
}

Cả hai in ra đúng mười lăm dòng như nhau. Cách thứ hai không có thứ tự nào để đặt sai, vì hai test là hai lệnh if độc lập chứ không phải một chuỗi, và 15 thoả cả hai.

Lỗi thường gặp. Đặt i % 3 == 0 lên đầu. Khi đó 15 rơi vào nhánh này, in ra Fizz, còn nhánh i % 15 == 0 bên dưới không bao giờ chạy tới — nó chết đúng với những input mà nó được viết ra để xử lý:

java FizzBuzzWrong | tail -3
13
14
Fizz

Bài 3 — Số lớn nhất trong ba số

Đề bài. Cho ba giá trị int, in ra số lớn nhất. Phải xử lý được trường hợp bằng nhau. Dùng a = 17, b = 42, c = 42.

Output mong đợi

a=17 b=42 c=42 -> largest = 42

Gợi ý. Trường hợp bằng nhau cần >= chứ không phải >, và nhánh else cuối cùng phải phủ được một trường hợp thật.

Lời giải

public class Largest {
    public static void main(String[] args) {
        int a = 17, b = 42, c = 42;

        int max;
        if (a >= b && a >= c) {
            max = a;
        } else if (b >= c) {
            max = b;
        } else {
            max = c;
        }

        System.out.printf("a=%d b=%d c=%d -> largest = %d%n", a, b, c, max);
    }
}

Lỗi thường gặp. Viết chuỗi điều kiện bằng > và bỏ luôn else cuối. Với b = 42, c = 42 thì không nhánh nào đúng, max không bao giờ được gán, và compiler từ chối chương trình:

LargestBad.java:12: error: variable max might not have been initialized
        System.out.println(max);
                           ^
1 error

Lỗi đó là một ưu ái. Cũng đoạn code ấy mà có else thì đã compile được và trả về một giá trị cũ. (Math.max(a, Math.max(b, c)) làm xong việc trong một dòng, nhưng cấu trúc các nhánh mới là nội dung của bài tập.)

Bài 4 — Xếp loại điểm

Đề bài. Đổi điểm số thành chữ: từ 90 trở lên là A, 80 là B, 70 là C, 60 là D, dưới 60 là F. In kết quả cho các điểm 95, 85, 75, 65, 55, 45 và 35.

Output mong đợi

score  95 -> A
score  85 -> B
score  75 -> C
score  65 -> D
score  55 -> F
score  45 -> F
score  35 -> F

Gợi ý. Trong chuỗi else if, mỗi mốc chỉ cần một phép so sánh, vì đi tới được một nhánh nghĩa là mọi nhánh phía trên đều đã sai. Điều đó chỉ đúng nếu bạn xếp thứ tự từ trên xuống.

Lời giải

public class Grade {
    public static void main(String[] args) {
        for (int score = 95; score >= 35; score -= 10) {
            String grade;
            if (score >= 90) {
                grade = "A";
            } else if (score >= 80) {
                grade = "B";
            } else if (score >= 70) {
                grade = "C";
            } else if (score >= 60) {
                grade = "D";
            } else {
                grade = "F";
            }
            System.out.printf("score %3d -> %s%n", score, grade);
        }
    }
}

Lỗi thường gặp. Vẫn là thứ tự, đúng cái bẫy của FizzBuzz. Bắt đầu chuỗi bằng score >= 60 thì mọi điểm trên 60 đều thành D, vì đó là test đầu tiên đúng:

java GradeWrong | head -4
score  95 -> D
score  85 -> D
score  75 -> D
score  65 -> D

Bài 5 — Năm nhuận

Đề bài. Một năm là năm nhuận khi nó chia hết cho 4, trừ các năm chia hết cho 100 — những năm này chỉ nhuận khi đồng thời chia hết cho 400. In kết quả cho 1900, 2000, 2023, 2024 và 2100.

Output mong đợi

1900 -> false
2000 -> true
2023 -> false
2024 -> true
2100 -> false

Gợi ý. Ba test, hẹp nhất trước: 400, rồi 100, rồi 4.

Lời giải

public class LeapYear {
    public static void main(String[] args) {
        report(1900);
        report(2000);
        report(2023);
        report(2024);
        report(2100);
    }

    static boolean isLeap(int year) {
        if (year % 400 == 0) return true;
        if (year % 100 == 0) return false;
        return year % 4 == 0;
    }

    static void report(int year) {
        System.out.printf("%d -> %b%n", year, isLeap(year));
    }
}

Lỗi thường gặp. Dừng lại ở year % 4 == 0. Phiên bản đó đúng với 2023 và 2024, và sai với mọi năm tròn thế kỷ:

1900 -> true
2000 -> true
2023 -> false
2024 -> true
2100 -> true

1900 và 2100 không phải năm nhuận. Quy tắc chỉ lộ ra ở các năm tròn thế kỷ, nên đó chính là những input phải đem ra test.

Vòng lặp trên các con số

Năm bài mà vòng lặp chính là toàn bộ chương trình. Hai trong số đó cũng là ví dụ kinh điển về việc int hết chỗ chứa.

Bài 6 — Tổng và trung bình của 1..n

Đề bài. Với n = 10, tính tổng từ 1 tới n bằng vòng lặp, rồi in trung bình theo ba cách: chia int thuần, cast sang double, và qua printf với hai chữ số thập phân.

Output mong đợi

sum     = 55
average = 5
average = 5.5
average = 5.50

Gợi ý. Hãy nhìn kỹ dòng thứ hai trước khi kết luận chương trình bị hỏng.

Lời giải

public class SumAverage {
    public static void main(String[] args) {
        int n = 10;
        int sum = 0;

        for (int i = 1; i <= n; i++) {
            sum += i;
        }

        System.out.println("sum     = " + sum);
        System.out.println("average = " + (sum / n));           // int / int
        System.out.println("average = " + ((double) sum / n));  // one side promoted
        System.out.printf("average = %.2f%n", (double) sum / n);
    }
}

Lỗi thường gặp. double avg = sum / n;. Phép chia chạy trước, chạy trong int, ra 5; việc mở rộng sang double xảy ra sau đó và cho bạn 5.0. Phải cast một operand, trước khi chia: (double) sum / n.

Bài 7 — Giai thừa, và chỗ int bó tay

Đề bài. Tính n! cho n từ 1 tới 21 song song trong một int và một long, rồi in cả hai cột. Tìm n đầu tiên mà cột int sai, và n đầu tiên mà cột long sai.

Output mong đợi

  n            int                   long
  1              1                      1
  2              2                      2
  3              6                      6
  4             24                     24
  5            120                    120
  6            720                    720
  7           5040                   5040
  8          40320                  40320
  9         362880                 362880
 10        3628800                3628800
 11       39916800               39916800
 12      479001600              479001600
 13     1932053504             6227020800
 14     1278945280            87178291200
 15     2004310016          1307674368000
 16     2004189184         20922789888000
 17     -288522240        355687428096000
 18     -898433024       6402373705728000
 19      109641728     121645100408832000
 20    -2102132736    2432902008176640000
 21    -1195114496   -4249290049419214848

Integer.MAX_VALUE = 2147483647
Long.MAX_VALUE    = 9223372036854775807

Gợi ý. In cả hai cột rồi tìm dòng đầu tiên mà chúng khác nhau.

Lời giải

public class Factorial {
    public static void main(String[] args) {
        int intFact = 1;
        long longFact = 1L;

        System.out.printf("%3s %14s %22s%n", "n", "int", "long");
        for (int n = 1; n <= 21; n++) {
            intFact *= n;
            longFact *= n;
            System.out.printf("%3d %14d %22d%n", n, intFact, longFact);
        }

        System.out.println();
        System.out.println("Integer.MAX_VALUE = " + Integer.MAX_VALUE);
        System.out.println("Long.MAX_VALUE    = " + Long.MAX_VALUE);
    }
}

Đọc bảng: hai cột trùng nhau tới 12! = 479001600. Tới n = 13 thì cột int ra 1932053504 trong khi giá trị thật là 6227020800, lớn hơn Integer.MAX_VALUE (2147483647). Vậy 12 là giai thừa cuối cùng còn vừa một int. Chuyển sang long mua thêm được tám bậc: 20! = 2432902008176640000 vẫn nằm dưới Long.MAX_VALUE, còn 21! quay về -4249290049419214848.

Lỗi thường gặp. Tưởng rằng overflow sẽ tự báo. Không có exception, không có warning nào cả — phép tính wrap lại và chương trình chạy tiếp với một con số sai.

⚠️ Nếu một giai thừa, một tích đang nhân dồn hay một tổng đang cộng dồn bỗng ra số âm thì bạn đã overflow. Cái dấu âm đó là chữ ký của nó, và thường là triệu chứng duy nhất bạn nhận được.

Bài 8 — Đếm chữ số, rồi đảo ngược chúng

Đề bài. Với n = 90210, in ra nó có bao nhiêu chữ số và giá trị khi đảo ngược các chữ số. Làm bằng số học — không đổi sang String.

Output mong đợi

n        = 90210
digits   = 5
reversed = 1209

Gợi ý. rest % 10 là chữ số cuối và rest /= 10 vứt nó đi. Dựng dần kết quả bằng reversed = reversed * 10 + digit.

Lời giải

public class Digits {
    public static void main(String[] args) {
        int n = 90210;

        int count = 0;
        int reversed = 0;
        int rest = n;

        do {
            int digit = rest % 10;
            reversed = reversed * 10 + digit;
            count++;
            rest /= 10;
        } while (rest != 0);

        System.out.println("n        = " + n);
        System.out.println("digits   = " + count);
        System.out.println("reversed = " + reversed);
    }
}

Lỗi thường gặp. Có hai, và đều nằm ở các trường hợp biên. Dùng while (rest != 0) thay vì do-while sẽ sai với input 0, vì thân vòng lặp không chạy lần nào:

while  loop says 0 has 0 digits
do-while says   0 has 1 digits

Cái thứ hai là mong đợi 01209. Đảo ngược 90210 đúng là ra 1209 — một int không giữ số 0 đứng đầu. Nếu bạn cần giữ, thì việc bạn đang làm là format một String chứ không phải đảo một con số.

Bài 9 — Có phải số nguyên tố không?

Đề bài. Viết isPrime(int n) bằng vòng lặp. In mọi số nguyên tố từ 2 tới 30 trên một dòng, rồi in kết luận cho 1, 2 và 1000003.

Output mong đợi

2 3 5 7 11 13 17 19 23 29 
1 prime? false
2 prime? true
1000003 prime? true

Gợi ý. Nếu n có một ước lớn hơn căn bậc hai của nó thì nó cũng có ước bạn đồng hành nhỏ hơn căn bậc hai — nên một vòng lặp chạy tới căn bậc hai là đã thấy hết. Hãy diễn đạt điều đó bằng i * i <= n thay vì đi tính căn.

Lời giải

public class Prime {
    public static void main(String[] args) {
        for (int n = 2; n <= 30; n++) {
            if (isPrime(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();

        System.out.println("1 prime? " + isPrime(1));
        System.out.println("2 prime? " + isPrime(2));
        System.out.println("1000003 prime? " + isPrime(1000003));
    }

    static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2; i * i <= n; i++) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Cái cận đó không phải chuyện tối ưu vặt. Đếm số vòng lặp mà từng cách thực hiện trên số nguyên tố 1000003:

n = 1000003
i * i <= n  -> 999 iterations
i <= n / 2  -> 500000 iterations
i < n       -> 1000001 iterations

Cùng một kết quả, ít hơn khoảng một nghìn lần số vòng lặp.

Lỗi thường gặp. Quên chốt chặn n < 2. Với 1, 0 và số âm thì thân vòng lặp không chạy lần nào, method rơi thẳng xuống return true và tuyên bố 1 là số nguyên tố.

Còn một lỗi tinh vi hơn ở sát trần của int: chính i * i bị overflow. Tại i = 46341 cái tích không còn vừa nữa, wrap thành số âm, và điều kiện vòng lặp vẫn đúng trong khi lẽ ra phải dừng:

46341 * 46341 as int = -2147479015
(i * i <= n) = true
(i <= n / i) = false

Với các giá trị gần Integer.MAX_VALUE, hãy viết cận là i <= n / i.

Bài 10 — Fibonacci bằng vòng lặp

Đề bài. In 15 số Fibonacci đầu tiên bắt đầu từ 0, trên một dòng, chỉ dùng một vòng lặp và không dùng đệ quy.

Output mong đợi

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 

Gợi ý. Hai variable là đủ. Hãy tính giá trị kế tiếp trước khi ghi đè lên bất kỳ cái nào.

Lời giải

public class Fibonacci {
    public static void main(String[] args) {
        int n = 15;
        long a = 0, b = 1;

        for (int i = 1; i <= n; i++) {
            System.out.print(a + " ");
            long next = a + b;
            a = b;
            b = next;
        }
        System.out.println();
    }
}

Lỗi thường gặp. Gán sai thứ tự: a = b; b = a + b; ghi đè a trước, nên dòng sau cộng b với chính nó và bạn in ra luỹ thừa của hai chứ không phải dãy Fibonacci. Hãy tính next vào một variable riêng trước.

int cũng hụt hơi ở đây, và sớm hơn nhiều người tưởng — F(47) là số hạng đầu tiên không vừa:

F(45) int =  1134903170   long =  1134903170
F(46) int =  1836311903   long =  1836311903
F(47) int = -1323752223   long =  2971215073
F(48) int =   512559680   long =  4807526976

Vòng lặp lồng nhau và các pattern

Vòng lặp ngoài là dòng, vòng lặp trong là những gì nằm trên dòng đó. Mọi bài ở đây thực chất chỉ hỏi một câu: chỉ số dòng phải được biến thành cái gì?

Bài 11 — Tam giác sao vuông góc

Đề bài. In một tam giác vuông bằng dấu * canh trái, 5 dòng: dòng đầu một sao, dòng cuối năm sao.

Output mong đợi

*
**
***
****
*****

Gợi ý. Cận của vòng lặp trong chính là counter của vòng lặp ngoài.

Lời giải

public class Triangle {
    public static void main(String[] args) {
        int n = 5;

        for (int row = 1; row <= n; row++) {
            for (int col = 1; col <= row; col++) {
                System.out.print("*");
            }
            System.out.println();
        }
    }
}

Lỗi thường gặp. Gọi System.out.println("*") trong vòng lặp trong, khiến mỗi dấu sao rơi xuống một dòng riêng. Vòng lặp trong dùng print; việc xuống dòng thuộc về vòng lặp ngoài, sau khi vòng lặp trong kết thúc.

Bài 12 — Pyramid cân

Đề bài. In một pyramid 5 dòng cân trên bề rộng 9 cột: dòng 1 có một sao, dòng 5 có chín sao, và giữa của mọi dòng thẳng hàng nhau.

Output mong đợi

    *
   ***
  *****
 *******
*********

Gợi ý. Mỗi dòng cần hai vòng lặp trong, một cho phần đệm và một cho phần sao. Hãy tính cả hai số lượng đó theo chỉ số dòng trước khi viết dòng code nào.

Chỉ số dòng ánh xạ sang số space và số sao trong một pyramid cân

Lời giải

public class Pyramid {
    public static void main(String[] args) {
        int n = 5;

        for (int row = 1; row <= n; row++) {
            for (int s = 1; s <= n - row; s++) {
                System.out.print(" ");
            }
            for (int star = 1; star <= 2 * row - 1; star++) {
                System.out.print("*");
            }
            System.out.println();
        }
    }
}

Phần số học mới là toàn bộ bài tập, nên đáng để in nó ra một lần cho rõ:

row=1  spaces=4  stars=1  centre column=5
row=2  spaces=3  stars=3  centre column=5
row=3  spaces=2  stars=5  centre column=5
row=4  spaces=1  stars=7  centre column=5
row=5  spaces=0  stars=9  centre column=5

Số sao tăng hai mỗi dòng trong khi phần đệm giảm một, và đó chính là thứ ghim dấu sao ở giữa vào đúng cột 5.

Lỗi thường gặp. Dùng 2 * row sao thay vì 2 * row - 1. Khi đó mọi dòng đều rộng một số chẵn ký tự, không có ký tự nào nằm chính giữa để mà cân, và hình bị lệch:

    **
   ****
  ******
 ********
**********

Lỗi phổ biến còn lại là lệch một đơn vị ở phần đệm: s < n - row cho thiếu đúng một space mỗi dòng và làm pyramid nghiêng về phía ngược lại.

Bài 13 — Bảng cửu chương với các cột thẳng hàng

Đề bài. In bảng nhân 9 x 9 có một dòng tiêu đề và một cột tiêu đề, các cột phải thẳng hàng.

Output mong đợi

   x   1   2   3   4   5   6   7   8   9
   1   1   2   3   4   5   6   7   8   9
   2   2   4   6   8  10  12  14  16  18
   3   3   6   9  12  15  18  21  24  27
   4   4   8  12  16  20  24  28  32  36
   5   5  10  15  20  25  30  35  40  45
   6   6  12  18  24  30  36  42  48  54
   7   7  14  21  28  35  42  49  56  63
   8   8  16  24  32  40  48  56  64  72
   9   9  18  27  36  45  54  63  72  81

Gợi ý. %4d in một số nguyên canh phải trong ô rộng bốn ký tự. %4s làm y hệt cho chữ x ở góc.

Lời giải

public class Grid {
    public static void main(String[] args) {
        int n = 9;

        System.out.printf("%4s", "x");
        for (int col = 1; col <= n; col++) {
            System.out.printf("%4d", col);
        }
        System.out.println();

        for (int row = 1; row <= n; row++) {
            System.out.printf("%4d", row);
            for (int col = 1; col <= n; col++) {
                System.out.printf("%4d", row * col);
            }
            System.out.println();
        }
    }
}

Lỗi thường gặp. Canh cột bằng tab hoặc bằng số space đếm tay — print(row * col + "\t") trông ổn cho tới khi một cột chứa cả 9 lẫn 81, và lúc đó việc canh hàng phụ thuộc vào tab stop của terminal. Chỉ định độ rộng ngay trong format specifier mới là cách thật sự thẳng hàng.

Bài 14 — Mọi cặp trong một khoảng thoả điều kiện

Đề bài. Trong khoảng 1 tới 20, in mọi cặp a + b bằng 24, mỗi cặp đúng một lần, với a < b. In số lượng cặp ở cuối.

Output mong đợi

4 + 20 = 24
5 + 19 = 24
6 + 18 = 24
7 + 17 = 24
8 + 16 = 24
9 + 15 = 24
10 + 14 = 24
11 + 13 = 24
pairs found: 8

Gợi ý. Cho vòng lặp trong bắt đầu từ a + 1, không phải từ 1.

Lời giải

public class Pairs {
    public static void main(String[] args) {
        int n = 20, target = 24;
        int found = 0;

        for (int a = 1; a <= n; a++) {
            for (int b = a + 1; b <= n; b++) {
                if (a + b == target) {
                    System.out.println(a + " + " + b + " = " + target);
                    found++;
                }
            }
        }

        System.out.println("pairs found: " + found);
    }
}

Lỗi thường gặp. Cho vòng lặp trong bắt đầu từ 1. Khi đó mỗi cặp bị tìm thấy hai lần, mỗi chiều một lần, và cặp có hai giá trị bằng nhau cũng bị tính:

pairs found: 17

Tám cặp thành mười bảy: tám cặp mỗi chiều, cộng thêm 12 + 12. Hễ một vòng lặp lồng nhau đang liệt kê các cặp không phân biệt thứ tự thì vòng trong phải bắt đầu ngay sau vòng ngoài.

Đọc input trong vòng lặp

Hai bài dùng Scanner. Cả hai đều được nạp input bằng pipe ở dưới, nên thứ bạn nhìn thấy là output của chính chương trình — pipe không lặp lại các giá trị đã gõ như bàn phím.

Bài 15 — Menu lặp lại cho tới khi chọn thoát

Đề bài. In một menu ba lựa chọn, xử lý lựa chọn đó, và tiếp tục hiện menu cho tới khi người dùng chọn thoát. Lựa chọn không hợp lệ thì in một thông báo rồi hiện lại menu. Dùng do-while.

Output mong đợi với input 1, 7, 2, 100, 9, 3:

1) square a number
2) sum 1..n
3) quit
choice: n: 7 squared = 49
1) square a number
2) sum 1..n
3) quit
choice: n: sum 1..100 = 5050
1) square a number
2) sum 1..n
3) quit
choice: no such option
1) square a number
2) sum 1..n
3) quit
choice: bye

Gợi ý. Menu phải hiện ít nhất một lần trước khi có gì để kiểm tra, và đó đúng là việc của do-while.

Lời giải

import java.util.Scanner;

public class Menu {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int choice;

        do {
            System.out.println("1) square a number");
            System.out.println("2) sum 1..n");
            System.out.println("3) quit");
            System.out.print("choice: ");
            choice = Integer.parseInt(sc.nextLine().trim());

            if (choice == 1) {
                System.out.print("n: ");
                int n = Integer.parseInt(sc.nextLine().trim());
                System.out.println(n + " squared = " + n * n);
            } else if (choice == 2) {
                System.out.print("n: ");
                int n = Integer.parseInt(sc.nextLine().trim());
                int sum = 0;
                for (int i = 1; i <= n; i++) {
                    sum += i;
                }
                System.out.println("sum 1.." + n + " = " + sum);
            } else if (choice != 3) {
                System.out.println("no such option");
            }
        } while (choice != 3);

        System.out.println("bye");
    }
}

Chạy thử:

printf '1\n7\n2\n100\n9\n3\n' | java Menu

Lỗi thường gặp. Khai báo int choice bên trong thân vòng lặp. Phần while ở cuối một do-while nằm ngoài scope của thân, nên variable mà nó kiểm tra phải được khai báo trước vòng lặp:

MenuScope.java:10: error: cannot find symbol
        } while (choice != 3);
                 ^
  symbol:   variable choice
  location: class MenuScope
1 error

Bài 16 — Vòng lặp validate từ chối input sai

Đề bài. Hỏi tuổi cho tới khi input là một số nguyên trong khoảng 1 tới 120. Từ chối thứ không phải số, từ chối số ngoài khoảng, và hỏi lại mỗi lần.

Output mong đợi với input abc, -5, 200, 34:

age (1-120): not a whole number: abc
age (1-120): out of range: -5
age (1-120): out of range: 200
age (1-120): accepted age = 34

Gợi ý. hasNextInt() cho biết token kế tiếp có parse được hay không, và không tiêu thụ gì cả. Vế sau chính là cái bẫy.

Lời giải

import java.util.Scanner;

public class Validate {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int age;

        while (true) {
            System.out.print("age (1-120): ");
            if (!sc.hasNextInt()) {
                System.out.println("not a whole number: " + sc.next());
                continue;
            }
            age = sc.nextInt();
            if (age >= 1 && age <= 120) {
                break;
            }
            System.out.println("out of range: " + age);
        }

        System.out.println("accepted age = " + age);
    }
}

Chạy thử:

printf 'abc\n-5\n200\n34\n' | java Validate

Lỗi thường gặp. Kiểm tra bằng hasNextInt() rồi không tiêu thụ cái token hỏng. hasNextInt() chỉ nhìn; token hỏng vẫn nằm nguyên đó, lần kiểm tra sau nhìn đúng token đó, và vòng lặp in lời than phiền của nó mãi mãi. Chính sc.next() nằm trong câu thông báo mới là thứ tạo ra tiến triển — nó không nằm đó cho đẹp.

Cách trace tay một vòng lặp chạy sai

Khi một vòng lặp cho ra con số sai, cách thoát nhanh nhất không phải là ngồi nhìn nó. Mà là dựng một bảng trace: mỗi vòng lặp một dòng, mỗi variable một cột, cộng thêm một cột cho điều kiện của vòng lặp. Điền tay vào đó, và cái dòng mà bảng của bạn bắt đầu lệch khỏi mong đợi chính là chỗ lỗi.

Đây là vòng lặp cần trace. Nó lẽ ra phải tính tổng 1 tới 5, tức là 15:

int n = 5;
int sum = 0;

for (int i = 1; i < n; i++) {
    sum += i;
}

System.out.println(sum);

Trace từng vòng một. Ghi lại i, điều kiện có đúng không, và sum sau khi thân vòng lặp chạy xong:

vòngii < 5sum sau thân vòng lặp
11true1
22true3
33true6
44true10
55false— thoát vòng lặp, sum giữ nguyên 10

Bảng tự nó trả lời. Dòng thứ năm không bao giờ chạy, nên 5 không bao giờ được cộng vào, và vòng lặp trả về 10 thay vì 15. Đổi điều kiện thành i <= n là bảng trace đó có thêm một dòng:

Cùng một vòng lặp tính tổng, trace với điều kiện nhỏ hơn và với điều kiện nhỏ hơn hoặc bằng

Bạn không nhất thiết phải làm trên giấy. Hai lệnh printf biến chương trình thành bảng trace của chính nó:

for (int i = 1; i < n; i++) {
    System.out.printf("body starts: i=%d sum=%d%n", i, sum);
    sum += i;
    System.out.printf("body ends:   i=%d sum=%d%n", i, sum);
}
System.out.printf("after loop:  sum=%d (expected 15)%n", sum);
body starts: i=1 sum=0
body ends:   i=1 sum=1
body starts: i=2 sum=1
body ends:   i=2 sum=3
body starts: i=3 sum=3
body ends:   i=3 sum=6
body starts: i=4 sum=6
body ends:   i=4 sum=10
after loop:  sum=10 (expected 15)

Bốn vòng, không phải năm. Với i <= n thì cũng đoạn in đó cho ra thêm cặp dòng còn thiếu và kết quả đúng:

body starts: i=5 sum=10
body ends:   i=5 sum=15
after loop:  sum=15 (expected 15)

Có hai điều làm cách này hiệu quả. In cả counter chứ không chỉ giá trị bạn quan tâm, vì một nửa số lỗi kiểu này nằm ở counter chứ không nằm ở phép tính. Và in giá trị trước lẫn sau thân vòng lặp, để thấy được lệnh nào đã thay đổi nó. Tìm ra lỗi rồi thì xoá hai dòng đó đi.

Những lỗi mà các bài tập này nhắm tới

Năm kiểu sai chiếm phần lớn những gì hỏng trong một trăm vòng lặp đầu tiên của người mới.

LỗiTriệu chứngCách sửa
= ở chỗ đáng ra là ==lỗi compile, hoặc điều kiện không bao giờ đúngso sánh bằng ==, gán bằng =
int / int khi tính trung bìnhra số nguyên trong khi cần số thập phâncast một operand: (double) sum / n
lệch một đơn vị ở số space hay số saohình bị nghiêng, hoặc thiếu một dòngviết ra số lượng theo từng dòng trước
int overflow trong một tíchgiá trị dương lớn bỗng thành âmdùng long, và biết luôn giới hạn của nó
== trên Stringso sánh ra false dù nội dung giống nhaudùng equals()

===. Trong C đây là lỗi âm thầm kinh điển; Java bắt được phần lớn ngay lúc compile, vì một int không phải là boolean:

AssignInIf.java:4: error: incompatible types: int cannot be converted to boolean
        if (x = 5) {
              ^
1 error

Phiên bản Java không bắt được là khi variable có type boolean, ở đó while (done = false) là một phép gán hợp lệ có giá trị false. Nó compile được, và vòng lặp đơn giản là không chạy lần nào:

boolean done = false;
int guard = 0;
while (done = false) {          // BUG: assignment, not comparison
    guard++;
    if (guard > 3) break;
}
System.out.println("loop body ran " + guard + " times, done = " + done);
loop body ran 0 times, done = false

Chia số nguyên. Đã gặp ở bài 6, và nó cũng chẳng báo gì: 55 / 10 ra 5, và phần .5 biến mất trước khi có bất kỳ double nào tham gia.

Lệch một đơn vị khi vẽ hình. Bài 12. Cách chữa không phải là ngồi đoán trong editor, mà là viết ra bảng số lượng theo từng dòng rồi đối chiếu với hình mình muốn.

Overflow. Bài 7 và bài 10. 12! là giai thừa cuối cùng còn vừa một int, và F(46) là số Fibonacci cuối cùng còn vừa một int. Cả hai đều wrap trong im lặng.

So sánh String bằng ==. == hỏi hai reference có trỏ vào cùng một object hay không, chứ không hỏi nội dung có giống nhau hay không. Hai String literal thì dùng chung một object, và đó là lý do cái sai này có vẻ chạy được lúc test rồi hỏng khi gặp input thật:

Scanner sc = new Scanner(System.in);
String answer = sc.nextLine().trim();

System.out.println("answer            = [" + answer + "]");
System.out.println("answer == \"quit\"    -> " + (answer == "quit"));
System.out.println("answer.equals(\"quit\") -> " + answer.equals("quit"));

String literal = "quit";
System.out.println("literal == \"quit\"   -> " + (literal == "quit"));
printf 'quit\n' | java StringEquals
answer            = [quit]
answer == "quit"    -> false
answer.equals("quit") -> true
literal == "quit"   -> true

Cùng bốn ký tự, hai câu trả lời khác nhau. Một menu viết bằng == trên input của người dùng thì không bao giờ thoát được.

FAQ

Vì sao trung bình của tôi ra số nguyên?

Vì cả hai operand đều là int, nên Java thực hiện phép chia nguyên và vứt phần thập phân đi trước khi bất cứ chuyện gì khác xảy ra. Gán kết quả vào một double là đã quá muộn. Hãy cast một operand trước: (double) sum / n, hoặc sum / (double) n.

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

Dùng for khi số vòng lặp đã biết từ một counter — mọi bài ở mức hai và mức ba trong bài viết này. Dùng while khi vòng lặp chạy tới lúc một điều kiện thay đổi và có thể bạn không vào vòng lặp lần nào. Dùng do-while khi thân vòng lặp phải chạy ít nhất một lần trước khi có gì để kiểm tra, trên thực tế là menu và các câu hỏi nhập liệu. Ba dạng có sức mạnh tương đương, nên hãy chọn dạng làm ý định lộ ra rõ nhất.

Làm sao dừng một vòng lặp chạy mãi không hết?

Ctrl-C trong terminal. Rồi đi tìm thứ mà vòng lặp lẽ ra phải thay đổi nhưng đã không: một counter không bao giờ được tăng, một lần kiểm tra hasNextInt() mà token hỏng không bao giờ bị tiêu thụ, hoặc một điều kiện kiểm tra variable mà thân vòng lặp không hề đụng tới. Thêm một printf trace ở đầu thân vòng lặp là biết ngay đó là cái nào chỉ sau một lần chạy.

Có nhất thiết phải dùng printf để canh cột không?

Với bất cứ thứ gì rộng hơn một cột thì có. %4d giữ sẵn một ô rộng cố định bất kể con số có bao nhiêu chữ số, nên 9 và 81 rơi vào cùng một cột. Tab thì phụ thuộc tab stop của terminal, còn đệm space bằng tay thì hỏng ngay khi một giá trị dài thêm một chữ số.

Lời giải có dùng vài method nhỏ. Như vậy có được không?

Được, và isPrime là chỗ tự nhiên nhất để dùng — một method trả lời một câu hỏi về một giá trị thì dễ đọc hơn là nhét cùng vòng lặp đó vào giữa dòng. Method có bài riêng ở phần sau của series; bây giờ cứ chép theo dáng của static boolean isPrime(int n) và đừng bận tâm về cơ chế bên trong.

Kết luận

Không bài nào trong mười sáu bài này cần cú pháp gì ngoài if, else if, ba dạng vòng lặp và một tầng lồng nhau. Cái làm chúng khó là thứ tự các điều kiện, phép số học biến chỉ số dòng thành số lượng, trường hợp biên ở số 0, và ranh giới chính xác trong điều kiện của vòng lặp. Đó mới là những phần đáng luyện, vì chúng không tự dễ đi nhờ việc đọc.

Có hai thói quen đáng mang theo. Hãy trace vòng lặp trước khi viết lại nó: một bảng ghi i và giá trị tích luỹ theo từng vòng tìm ra lỗi lệch một đơn vị trong chưa tới một phút. Và hãy cố tình chạy thử phiên bản sai một lần — nhìn thấy Fizz ở chỗ đáng ra là FizzBuzz dạy quy tắc thứ tự tốt hơn mọi lời giải thích.

Mọi bài tập ở đây đều làm việc với một giá trị tại một thời điểm. Bài tiếp theo giới thiệu công cụ để giữ nhiều giá trị cùng lúc: array — cách khai báo, cách khởi tạo, và cách duyệt bằng vòng lặp.

Bài viết liên quan

[Java Basics] Dự án tổng hợp: app quản lý sinh viên trên console

Một ứng dụng console hoàn chỉnh viết bằng Java, chỉ dùng những gì cả khóa đã dạy: class domain bất biến tự kiểm tra dữ liệu, một cây exception nhỏ, repository trong bộ nhớ dựa trên list và map, lưu trữ ra file text sau một interface, và menu loop sống sót trước mọi input sai.

[Java Basics] Toán tử trong Java: số học, so sánh, logic, gán và toán tử ba ngôi

Hướng dẫn đầy đủ về toán tử trong Java — số học, so sánh, logic, gán, bitwise và ba ngôi — với mọi kết quả đều được compile và chạy thật trên JDK 21, kèm các bẫy về chia số nguyên, số dư, số thực và short-circuit.

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

Vòng lặp trong Java giải thích bằng cách chạy thật: thứ tự thực thi chính xác của header for, while so với do-while, enhanced for và lý do nó không ghi ngược vào array, lỗi off-by-one với length, và ba cách tạo ra vòng lặp vô hạn.

[Java Basics] Mảng nhiều chiều trong Java

Mảng nhiều chiều trong Java thực chất là array của array chứ không phải hình chữ nhật - cách tạo và duyệt grid, jagged array, deepToString, deepEquals, shallow vs deep copy, mảng 3 chiều và ví dụ matrix, mọi output đều compile và chạy thật trên JDK 21.