Java Foundations/Mini-challenge: FizzBuzz với switch expression
22/35
Bài 22 / 35~22 phútĐiều kiện & Vòng lặpMiễn phí lượt xem

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.

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

Đặt 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