Ba bài trước đã giới thiệu if và switch, ba dạng vòng lặp, cùng vòng lặp lồng nhau với break
và continue. 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.
![]()
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 là -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.

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.

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òng | i | i < 5 | sum sau thân vòng lặp |
|---|---|---|---|
| 1 | 1 | true | 1 |
| 2 | 2 | true | 3 |
| 3 | 3 | true | 6 |
| 4 | 4 | true | 10 |
| 5 | 5 | false | — 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:

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ỗi | Triệu chứng | Cách sửa |
|---|---|---|
= ở chỗ đáng ra là == | lỗi compile, hoặc điều kiện không bao giờ đúng | so sánh bằng ==, gán bằng = |
int / int khi tính trung bình | ra số nguyên trong khi cần số thập phân | cast một operand: (double) sum / n |
| lệch một đơn vị ở số space hay số sao | hình bị nghiêng, hoặc thiếu một dòng | viết ra số lượng theo từng dòng trước |
int overflow trong một tích | giá trị dương lớn bỗng thành âm | dùng long, và biết luôn giới hạn của nó |
== trên String | so sánh ra false dù nội dung giống nhau | dùng equals() |
= và ==. 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.