Java — Từ Zero đến Senior/Điều kiện & Vòng lặp/Mini-challenge: FizzBuzz với switch expression
7/7
~22 phútĐiều kiện & Vòng lặp

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 35 → 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 35.

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

ConceptBài đã họcDùng ở đây
Vòng lặp forModule 3, bài 4Duyệt từ 1 đến n
Toán tử % (modulo)Module 2, bài 4Kiểm tra chia hết
if/else if/elseModule 3, bài 1Phiên bản 1
switch expressionModule 3, bài 2Phiên bản 2, 3 — tận dụng case gộp
yield trong switch blockModule 3, bài 2Bonus

▶️ 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 keykey = 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 expression trả trực tiếp String vào biến out — không cần khai báo out trướ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ừ println sang return, 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/else nế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ảnDễ đọcMở rộngIdiomatic 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 for duyệt range với if/else if rẽ nhánh và switch expression trả 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 % 15 trước i % 3i % 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.