Mini-challenge: FizzBuzz với switch expression
Bài tập khép lại Module 3 — viết FizzBuzz kết hợp for, if/else, switch expression để rèn tư duy chọn cấu trúc điều khiển phù hợp.
Đâ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 ý
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
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.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên