Đây là mini-challenge khép lại Module 3. Không có lý thuyết mới — chỉ bài toán cổ điển bạn có thể đã nghe: FizzBuzz. Nhưng thay vì if/else if kiểu junior, bạn sẽ viết theo 3 phiên bản để so sánh cấu trúc điều khiển: if/else, switch expression (Java 14+), và bonus nâng cao.
FizzBuzz nổi tiếng vì được Jeff Atwood dùng làm bài lọc interview — thống kê cho thấy nhiều lập trình viên "có kinh nghiệm" vẫn làm không xong. Bạn sẽ làm được — không chỉ làm xong, mà làm theo cách idiomatic Java 21.
🎯 Đề bài
In các số từ 1 đến n. Với mỗi số:
- Chia hết cho 3 và 5 → in
"FizzBuzz". - Chỉ chia hết cho 3 → in
"Fizz". - Chỉ chia hết cho 5 → in
"Buzz". - Không chia hết cả 2 → in chính số đó.
Ví dụ với n = 15:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
🔍 Phân tích I-P-O
Input: Số nguyên dương n.
Processing: Duyệt i từ 1 đến n, với mỗi i xác định output theo modulo 3 và 5.
Output: In lần lượt từng dòng tương ứng.
Key insight
Điểm "bẫy" của FizzBuzz với người mới: thứ tự kiểm tra điều kiện.
// SAI — khong bao gio in "FizzBuzz"
if (i % 3 == 0) System.out.println("Fizz");
else if (i % 5 == 0) System.out.println("Buzz");
else if (i % 15 == 0) System.out.println("FizzBuzz"); // unreachable
else System.out.println(i);
Lý do: i = 15 khớp i % 3 == 0 trước → in "Fizz" → skip nhánh dưới. Phải kiểm tra case đặc biệt nhất trước.
📦 Concept dùng trong bài
| Concept | Bài đã học | Dùng ở đây |
|---|---|---|
Vòng lặp for | Module 3, bài 4 | Duyệt từ 1 đến n |
Toán tử % (modulo) | Module 2, bài 4 | Kiểm tra chia hết |
if/else if/else | Module 3, bài 1 | Phiên bản 1 |
switch expression | Module 3, bài 2 | Phiên bản 2, 3 — tận dụng case gộp |
yield trong switch block | Module 3, bài 2 | Bonus |
▶️ Phần 1 — Starter với if/else if
Viết phiên bản đầu với kiểu "cổ điển". Copy file, điền vào TODO:
public class FizzBuzz {
public static void main(String[] args) {
int n = 15;
for (int i = 1; i <= n; i++) {
// TODO: kiem tra i chia het 15 (hoac 3 va 5) -> in "FizzBuzz"
// TODO: chi chia het 3 -> in "Fizz"
// TODO: chi chia het 5 -> in "Buzz"
// TODO: else -> in i
}
}
}
javac FizzBuzz.java
java FizzBuzz
▶️ Phần 2 — Refactor thành switch expression
Sau khi phần 1 chạy đúng, refactor: biến "FizzBuzz" thành expression nội suy từ cặp remainder (i%3, i%5).
Gợi ý: có 4 trạng thái khác biệt:
(0, 0)→ "FizzBuzz"(0, *)(không 0) → "Fizz"(*, 0)→ "Buzz"(*, *)→ số
Có nhiều cách encode. Một cách gọn: tạo 1 int key từ 2 remainder:
int key = (i % 3 == 0 ? 1 : 0) * 2 + (i % 5 == 0 ? 1 : 0);
// key = 3 -> div both, key = 2 -> div 3, key = 1 -> div 5, key = 0 -> neither
Rồi dùng switch expression:
String out = switch (key) {
case 3 -> "FizzBuzz";
case 2 -> "Fizz";
case 1 -> "Buzz";
default -> String.valueOf(i);
};
System.out.println(out);
Dành 15–20 phút thử tự làm cả 2 phần trước khi xem lời giải.
💡 Gợi ý
💡 💡 Gợi ý — đọc khi bị kẹt
Toán tử modulo %: kiểm tra chia hết — x % y == 0 nghĩa là x chia hết y.
Chia hết cho cả 3 và 5 tương đương chia hết cho 15 (LCM), không cần kiểm tra (i%3==0 && i%5==0) — ngắn hơn viết i%15==0.
Thứ tự if/else if: kiểm tra case cụ thể nhất trước, tổng quát nhất sau. Hoặc reverse: kiểm tra "chia 15" trước khi kiểm tra "chia 3" và "chia 5".
Với switch expression: case gộp bằng dấu phẩy: case 3, 15 -> ....
Đừng gọi System.out.println trong từng nhánh — tính output thành String rồi in 1 chỗ. Tách "quyết định output" và "in output" là design tốt hơn.
✅ Lời giải
ℹ️ ✅ Lời giải — xem sau khi đã thử
Phiên bản 1: if/else if cổ điển
public class FizzBuzz {
public static void main(String[] args) {
int n = 15;
for (int i = 1; i <= n; 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);
}
}
}
}
Điểm đáng nhớ: check i % 15 trước, không phải i % 3. Lý do đã giải thích ở phần "Key insight".
Phiên bản 2: switch expression (Java 14+)
public class FizzBuzzSwitch {
public static void main(String[] args) {
int n = 15;
for (int i = 1; i <= n; i++) {
int key = (i % 3 == 0 ? 1 : 0) * 2 + (i % 5 == 0 ? 1 : 0);
String out = switch (key) {
case 3 -> "FizzBuzz";
case 2 -> "Fizz";
case 1 -> "Buzz";
default -> String.valueOf(i);
};
System.out.println(out);
}
}
}
Giải thích từng phần:
- Encode 2 remainder thành 1 key —
key = div3 * 2 + div5. 4 tổ hợp cho 4 giá trị 0..3. Đây là kỹ thuật quen thuộc khi cần dispatch theo nhiều dimension. switch expressiontrả trực tiếpStringvào biếnout— không cần khai báoouttrước rồi gán trong từng case.- Tách "quyết định output" và "in output" — hàm sau có thể đổi từ
printlnsangreturn, ghi file, gọi API... mà không đụng switch. - Ternary bên trong expression —
(i % 3 == 0 ? 1 : 0)ngắn gọn, có thể lùi vềif/elsenếu team không thích ternary.
Phiên bản 3 (bonus): switch expression với block + yield
Nếu bạn muốn pattern "tính gì đó trong case rồi trả", dùng block { ... yield ... }:
for (int i = 1; i <= n; i++) {
boolean div3 = (i % 3 == 0);
boolean div5 = (i % 5 == 0);
String out = switch (Boolean.compare(div3, div5) + (div3 && div5 ? 10 : 0)) {
case 10 -> "FizzBuzz"; // div3 && div5
case 1 -> "Fizz"; // div3 only (true > false = 1)
case -1 -> "Buzz"; // div5 only
default -> {
// khong chia het ca 2 -> tra ve so
yield String.valueOf(i);
}
};
System.out.println(out);
}
Hơi rối — chỉ để minh họa yield. Code production nên giữ phiên bản 2 cho dễ đọc.
Phiên bản 4 (bonus khác): string concatenation
Không dùng switch, dùng thẳng pattern "lắp chuỗi":
for (int i = 1; i <= n; i++) {
String out = "";
if (i % 3 == 0) out += "Fizz";
if (i % 5 == 0) out += "Buzz";
if (out.isEmpty()) out = String.valueOf(i);
System.out.println(out);
}
Ưu điểm: không cần biết trước các tổ hợp. Muốn thêm rule "chia 7 in Bazz" chỉ cần thêm 1 dòng. Mở rộng tốt nhất. Điểm trừ: thêm 1 biến local và kiểm tra isEmpty() ở cuối.
So sánh 4 phiên bản
| Phiên bản | Dễ đọc | Mở rộng | Idiomatic Java 21 |
|---|---|---|---|
| 1. if/else if | ✅ | ❌ (phải sửa 4 chỗ) | ✅ |
| 2. switch + key | ✅ | ⚠️ (phải đổi encoding) | ✅ (dùng switch expression) |
| 3. switch + yield | ❌ (rối) | ❌ | ⚠️ |
| 4. concat | ✅ | ✅ | ✅ |
Không có "cách đúng duy nhất". Phiên bản 1 và 4 thường xuất hiện trong code production.
🎓 Mở rộng
Đã hoàn thành bài cơ bản? Thử tiếp:
Mức 1 — Nhập n từ console:
try (Scanner sc = new Scanner(System.in)) {
System.out.print("Nhap n: ");
int n = sc.nextInt();
// ... FizzBuzz voi n
}
Mức 2 — FizzBuzzFooBar (3, 5, 7):
Thêm rule: chia hết cho 7 in "Foo". Kiểm tra tất cả tổ hợp: "FizzFoo" (3 và 7), "BuzzFoo" (5 và 7), "FizzBuzzFoo" (3,5,7). Phiên bản 4 (concat) giải quyết dễ nhất:
for (int i = 1; i <= n; i++) {
String out = "";
if (i % 3 == 0) out += "Fizz";
if (i % 5 == 0) out += "Buzz";
if (i % 7 == 0) out += "Foo";
if (out.isEmpty()) out = String.valueOf(i);
System.out.println(out);
}
Mức 3 — Trả về List<String> thay vì in:
static List<String> fizzBuzz(int n) {
List<String> result = new ArrayList<>(n);
for (int i = 1; i <= n; i++) {
// tinh out nhu cu
result.add(out);
}
return result;
}
Tách logic khỏi I/O → testable. Unit test với JUnit: gọi fizzBuzz(15), assert kết quả với list mong đợi.
Mức 4 — Stream version (Java 8+):
import java.util.stream.IntStream;
IntStream.rangeClosed(1, n)
.mapToObj(i -> {
int key = (i % 3 == 0 ? 1 : 0) * 2 + (i % 5 == 0 ? 1 : 0);
return switch (key) {
case 3 -> "FizzBuzz";
case 2 -> "Fizz";
case 1 -> "Buzz";
default -> String.valueOf(i);
};
})
.forEach(System.out::println);
Stream API sẽ học ở module sau. Đây là teaser.
✨ Điều bạn vừa làm được
Hoàn thành mini-challenge này, bạn đã:
- Kết hợp
forduyệt range vớiif/else ifrẽ nhánh vàswitch expressiontrả giá trị — 3 cấu trúc điều khiển chính của Module 3. - Hiểu tại sao thứ tự kiểm tra điều kiện quan trọng —
i % 15trướci % 3vài % 5. - Thấy rõ lợi ích switch expression trả giá trị so với switch statement cổ điển: ngắn, an toàn, không fall-through.
- Áp dụng pattern "tách quyết định output và in output" — design nhỏ nhưng làm code testable và đổi destination dễ (console → file → API).
- So sánh 4 cách giải cùng một bài — hiểu không có "cách đúng duy nhất", chọn theo context.
Chúc mừng — bạn đã hoàn thành Module 3! Module 4 sẽ bước vào phương thức (method): tham số, return, overloading, varargs, recursion. Từ đây bạn sẽ viết chương trình chia nhỏ thành nhiều hàm, thay vì dồn tất cả vào main.