Chương 03 · Điều khiển luồng & mảng

Điều khiển luồng, mảng và nhập xuất

Chương này là bộ khung của mọi chương trình: rẽ nhánh, lặp, và lưu nhiều giá trị cùng kiểu. Điểm đáng chú ý nhất là switch — nó đã thay đổi hoàn toàn từ Java 14, từ một câu lệnh dễ gây bug (quên break) thành một biểu thức an toàn, và tới Java 21 thì trở thành công cụ khớp mẫu mạnh mẽ. Chúng ta học cả cách cũ (vì bạn sẽ gặp trong code có sẵn) lẫn cách mới (vì bạn nên viết như vậy).

Độ dài: ~1500 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-2
🎯 Mục tiêu chương
  • Viết if/else if/else gọn gàng; biết dùng "trả về sớm" để giảm lồng nhau.
  • Phân biệt switch câu lệnh (cũ, có fall-through) và switch biểu thức (Java 14+, an toàn hơn).
  • Dùng pattern matching cho switch (Java 21) — thay thế chuỗi instanceof.
  • Nắm bốn kiểu vòng lặp và biết chọn cái nào cho tình huống nào.
  • Dùng break, continue, và nhãn để thoát vòng lặp lồng nhau.
  • Làm chủ mảng: khai báo, khởi tạo, mảng nhiều chiều, mảng răng cưa.
  • Sử dụng lớp tiện ích Arrays: sort, binarySearch, copyOf, fill, equals, toString.
  • Đọc dữ liệu bằng Scanner và tránh bẫy nextInt() + nextLine().
  • Định dạng đầu ra bằng printf / String.format — bảng chỉ định định dạng.

1. if / else if / else

IfElse.java
int diem = 85;

if (diem >= 90) {
    System.out.println("Giỏi");
} else if (diem >= 70) {
    System.out.println("Khá");
} else if (diem >= 50) {
    System.out.println("Trung bình");
} else {
    System.out.println("Yếu");
}

// ⚠️ Điều kiện PHẢI là boolean — Java không có truthy/falsy
int n = 5;
// if (n) { }              // ❌ error: int cannot be converted to boolean
if (n != 0) { }              // ✅

String s = "";
// if (s) { }              // ❌
if (!s.isEmpty()) { }         // ✅
🕳️ Ba cái bẫy của if
IfBay.java
// ① Dấu chấm phẩy lạc — thân if trở thành câu lệnh rỗng
if (x > 0);              // ⚠️ chấm phẩy ở đây!
    System.out.println("dương");   // LUÔN chạy, bất kể x

// ② Bỏ ngoặc nhọn — chỉ câu lệnh ĐẦU TIÊN thuộc về if
if (x > 0)
    System.out.println("a");
    System.out.println("b");      // LUÔN chạy — thụt lề đánh lừa mắt

// ③ Dùng = thay ==  (chỉ lọt được với boolean)
boolean xong = false;
if (xong = true) { }      // ⚠️ GÁN rồi kiểm tra → luôn true
if (xong == true) { }     // đúng, nhưng thừa
if (xong) { }             // ✅ cách viết chuẩn

Bẫy ② là nguyên nhân của lỗi bảo mật nổi tiếng "goto fail" trong SSL của Apple năm 2014. Luôn dùng ngoặc nhọn, kể cả khi thân chỉ có một dòng — mọi hướng dẫn code style nghiêm túc đều yêu cầu điều này.

💡 Trả về sớm — giảm lồng nhau
TraVeSom.java
// ❌ Kim tự tháp — khó đọc, khó thêm điều kiện
public String xuLy(DonHang don) {
    if (don != null) {
        if (don.daThanhToan()) {
            if (don.conHang()) {
                return "Giao hàng";
            } else {
                return "Hết hàng";
            }
        } else {
            return "Chưa thanh toán";
        }
    } else {
        return "Đơn không hợp lệ";
    }
}

// ✅ Xử lý trường hợp loại trừ trước, thoát sớm
public String xuLy(DonHang don) {
    if (don == null)           return "Đơn không hợp lệ";
    if (!don.daThanhToan())   return "Chưa thanh toán";
    if (!don.conHang())       return "Hết hàng";
    return "Giao hàng";      // đường đi chính, nằm phẳng ở cuối
}

Kiểu viết thứ hai gọi là guard clause. Mỗi điều kiện loại trừ được xử lý và thoát ngay, để "đường đi chính" (happy path) nằm phẳng ở cuối — dễ đọc và dễ thêm điều kiện mới.

2. switch câu lệnh — cách cũ và cái bẫy của nó

SwitchCu.java
int thu = 3;
String ten;

switch (thu) {
    case 2: ten = "Thứ hai"; break;
    case 3: ten = "Thứ ba";  break;
    case 4: ten = "Thứ tư";  break;
    default: ten = "Không rõ";
}
System.out.println(ten);   // Thứ ba
⚠️ Fall-through — quên break là chạy tiếp nhánh sau
FallThrough.java
int thu = 3;
switch (thu) {
    case 2: System.out.println("Thứ hai");
    case 3: System.out.println("Thứ ba");    // ← bắt đầu ở đây
    case 4: System.out.println("Thứ tư");    // ← rồi chạy tiếp!
    default: System.out.println("Không rõ");  // ← và tiếp nữa!
}
// In ra: Thứ ba / Thứ tư / Không rõ

Đây là hành vi cố ý của thiết kế (kế thừa từ C), nhưng trong 99% trường hợp nó là bug. Quên một chữ break ở giữa 20 nhánh là lỗi cực kỳ khó phát hiện khi đọc code.

Fall-through có một chỗ dùng hợp lý: gộp nhiều nhãn cùng xử lý một cách.

GopNhan.java
switch (thang) {
    case 1: case 3: case 5: case 7:
    case 8: case 10: case 12:
        soNgay = 31; break;
    case 4: case 6: case 9: case 11:
        soNgay = 30; break;
    case 2:
        soNgay = namNhuan ? 29 : 28; break;
    default:
        throw new IllegalArgumentException("Tháng không hợp lệ: " + thang);
}
Kiểu dữ liệu dùng được trong switchTừ phiên bản
byte, short, char, int (và wrapper của chúng)Java 1.0
enumJava 5
StringJava 7
Bất kỳ object nào (qua pattern matching)Java 21
long, float, double, boolean❌ Không bao giờ — dùng if

3. switch biểu thức (Java 14+) — cách nên viết

Java 14 chính thức đưa vào switch biểu thức: dùng mũi tên ->, không có fall-through, và trả về một giá trị.

SwitchMoi.java
// ✅ Cách hiện đại — ngắn hơn, an toàn hơn
String ten = switch (thu) {
    case 2 -> "Thứ hai";
    case 3 -> "Thứ ba";
    case 4 -> "Thứ tư";
    default -> "Không rõ";
};                       // ⚠️ có dấu chấm phẩy — đây là BIỂU THỨC

// Gộp nhiều nhãn bằng dấu phẩy — không cần fall-through
int soNgay = switch (thang) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;
    case 4, 6, 9, 11             -> 30;
    case 2                        -> namNhuan ? 29 : 28;
    default -> throw new IllegalArgumentException("Tháng: " + thang);
};

// Nhánh nhiều dòng: dùng khối và yield để trả giá trị
String mota = switch (diem / 10) {
    case 10, 9 -> "Giỏi";
    case 8, 7 -> {
        System.out.println("Ghi log: học sinh khá");
        yield "Khá";             // yield trả giá trị cho khối
    }
    default -> "Cần cố gắng";
};

// Vẫn dùng được như CÂU LỆNH (không trả giá trị) — vẫn không fall-through
switch (lenh) {
    case "start" -> khoiDong();
    case "stop"  -> dungLai();
    default     -> System.out.println("Lệnh lạ");
}
switch câu lệnh (cũ)switch biểu thức (Java 14+)
Cú pháp nhánhcase x: … break;case x -> …;
Fall-through✅ Có (nguồn bug)❌ Không
Trả giá trị❌ Không✅ Có
Gộp nhãnXếp chồng casecase 1, 2, 3 ->
Nhánh nhiều dòngViết thẳng{ … yield giaTri; }
Bắt buộc đủ nhánh❌ Không✅ Có — thiếu default là lỗi biên dịch
Phạm vi biếnChung cho cả switchRiêng từng nhánh

"Bắt buộc đủ nhánh" — lợi ích lớn nhất mà ít người để ý

Với switch biểu thức, trình biên dịch bắt buộc mọi trường hợp phải được xử lý. Kết hợp với enum (hoặc sealed ở chương 5), điều này trở nên rất mạnh: khi bạn thêm một hằng số mới vào enum, mọi switch trên enum đó sẽ báo lỗi biên dịch cho tới khi bạn xử lý trường hợp mới.

DuNhanh.java
enum TrangThai { MOI, DANG_XU_LY, HOAN_THANH }

String mota = switch (tt) {
    case MOI         -> "Vừa tạo";
    case DANG_XU_LY  -> "Đang làm";
    case HOAN_THANH  -> "Xong";
};   // ✅ đủ mọi hằng số → KHÔNG cần default

// Mai sau thêm HUY vào enum → dòng trên báo lỗi biên dịch NGAY:
// error: the switch expression does not cover all possible input values

Đây là lý do không nên viết default khi switch trên enum: có default thì trình biên dịch im lặng, và bạn mất đi tấm lưới an toàn này.

4. Pattern matching cho switch (Java 21)

Java 21 cho phép switch khớp theo kiểu, không chỉ theo giá trị. Đây là thay đổi lớn nhất của switch kể từ khi ngôn ngữ ra đời.

PatternMatching.java
// ❌ Cách cũ — chuỗi instanceof kèm ép kiểu thủ công
static String motaCu(Object o) {
    if (o instanceof Integer) {
        Integer i = (Integer) o;          // phải ép kiểu tay
        return "Số nguyên: " + i;
    } else if (o instanceof String) {
        String s = (String) o;
        return "Chuỗi dài " + s.length();
    } else if (o == null) {
        return "Rỗng";
    }
    return "Không rõ";
}

// ✅ Java 16+: pattern matching cho instanceof — biến tự khai báo
if (o instanceof String s && s.length() > 3) {
    System.out.println(s.toUpperCase());   // s đã sẵn kiểu String
}

// ✅ Java 21: pattern matching cho switch — gọn nhất
static String mota(Object o) {
    return switch (o) {
        case null          -> "Rỗng";          // switch xử lý được null!
        case Integer i     -> "Số nguyên: " + i;
        case Long l        -> "Số dài: " + l;
        case String s      -> "Chuỗi dài " + s.length();
        case int[] arr     -> "Mảng " + arr.length + " phần tử";
        default            -> "Kiểu khác: " + o.getClass().getSimpleName();
    };
}

// Có thêm điều kiện với "when" (guard)
static String phanLoai(Object o) {
    return switch (o) {
        case Integer i when i < 0      -> "Số âm";
        case Integer i when i == 0     -> "Số không";
        case Integer i                -> "Số dương";
        case String s when s.isBlank() -> "Chuỗi trắng";
        case String s                 -> "Chuỗi: " + s;
        default                        -> "Khác";
    };
}
⚠️ Thứ tự nhánh quan trọng — cụ thể trước, tổng quát sau

Giống catch hay if/else if, các nhánh case được kiểm tra từ trên xuống. Nếu đặt case Object o lên trước case String s, trình biên dịch báo lỗi ngay: "this case label is dominated by a preceding case label". Tương tự, case Integer i không có when phải đứng sau các nhánh Integer i when …. Java bắt được lỗi này lúc biên dịch — điểm cộng lớn so với chuỗi if/else nơi bạn phải tự để ý.

5. Bốn kiểu vòng lặp

VongLap.java
// ① for cổ điển — khi cần CHỈ SỐ
for (int i = 0; i < 5; i++) {
    System.out.println(i);        // 0 1 2 3 4
}
// Ba phần: khởi tạo ; điều kiện ; bước nhảy — đều có thể để trống
for (;;) { break; }            // vòng lặp vô hạn

// Nhiều biến, đếm ngược, bước nhảy khác 1
for (int i = 0, j = 10; i < j; i++, j--) { ... }
for (int i = 10; i > 0; i -= 2) { ... }


// ② for-each (enhanced for) — khi CHỈ CẦN GIÁ TRỊ ⭐ mặc định
int[] diem = {8, 9, 7};
for (int d : diem) {
    System.out.println(d);
}
for (String ten : danhSachTen) { ... }   // mọi thứ cài Iterable đều dùng được
for (var sv : danhSachSinhVien) { ... }   // var cho gọn


// ③ while — khi KHÔNG BIẾT TRƯỚC số lần lặp
int n = 100;
while (n > 1) {
    n = n % 2 == 0 ? n / 2 : 3 * n + 1;
}


// ④ do-while — chạy ÍT NHẤT MỘT LẦN rồi mới kiểm tra
int luaChon;
do {
    hienThiMenu();
    luaChon = docLuaChon();
} while (luaChon != 0);      // ⚠️ có dấu chấm phẩy cuối
LoạiKiểm tra điều kiệnChạy tối thiểuDùng khi
forTrước mỗi vòng0 lầnBiết số lần lặp, cần chỉ số
for-each ⭐Ngầm (còn phần tử không)0 lầnMặc định — duyệt mảng/collection
whileTrước mỗi vòng0 lầnKhông biết trước số lần
do-whileSau mỗi vòng1 lầnMenu, đọc input, thử lại
🕳️ Hai giới hạn của for-each
ForEachGioiHan.java
// ① Không sửa được phần tử của mảng nguyên thủy
int[] a = {1, 2, 3};
for (int x : a) {
    x = x * 2;          // ⚠️ x chỉ là BẢN SAO — mảng không đổi
}
System.out.println(a[0]);  // 1

for (int i = 0; i < a.length; i++) {
    a[i] = a[i] * 2;     // ✅ phải dùng chỉ số
}

// ② Không xoá được phần tử khi đang duyệt collection
List<String> ds = new ArrayList<>(List.of("a", "b", "c"));
for (String s : ds) {
    if (s.equals("b")) ds.remove(s);   // 💥 ConcurrentModificationException
}

// ✅ Cách đúng (chương 7 sẽ nói kỹ):
ds.removeIf(s -> s.equals("b"));

6. break, continue và nhãn

BreakContinue.java
// break — thoát HẲN vòng lặp
for (int i = 0; i < 10; i++) {
    if (i == 5) break;
    System.out.print(i);      // 01234
}

// continue — bỏ qua phần còn lại, sang vòng tiếp theo
for (int i = 0; i < 10; i++) {
    if (i % 2 != 0) continue;
    System.out.print(i);      // 02468
}

// ⚠️ break/continue chỉ tác động vòng lặp TRONG CÙNG
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) break;    // chỉ thoát vòng j, vòng i vẫn chạy
    }
}

// ⭐ NHÃN — thoát nhiều vòng lặp cùng lúc
ngoai:
for (int i = 0; i < matran.length; i++) {
    for (int j = 0; j < matran[i].length; j++) {
        if (matran[i][j] == canTim) {
            System.out.printf("Thấy tại (%d,%d)%n", i, j);
            break ngoai;         // thoát CẢ HAI vòng
        }
    }
}

// continue với nhãn: sang vòng tiếp theo của vòng NGOÀI
ngoaiCont:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) continue ngoaiCont;
        System.out.print(i + "" + j + " ");   // 00 10 20
    }
}
💡 Nhãn hay tách hàm?

Nhãn là công cụ hợp pháp và đôi khi là cách rõ ràng nhất, nhưng nếu vòng lặp lồng nhau quá ba cấp hoặc logic bên trong dài, hãy cân nhắc tách thành phương thức riêng và dùng return — thường dễ đọc và dễ test hơn nhiều. Java không có goto (đây là từ khoá được giữ lại nhưng không dùng được); nhãn là thứ gần nhất, và nó bị giới hạn chỉ dùng với vòng lặp nên không thể tạo ra "mã spaghetti" như goto thật.

7. Mảng

Mang.java
// --- Khai báo và khởi tạo ---
int[] a = new int[5];              // 5 phần tử, tất cả = 0
int[] b = {1, 2, 3};                // khởi tạo trực tiếp
int[] c = new int[]{1, 2, 3};      // dạng đầy đủ (cần khi truyền tham số)

String[] ten = new String[3];      // tất cả = null (kiểu tham chiếu)

// Cú pháp dấu ngoặc sau tên biến — hợp lệ nhưng KHÔNG nên dùng
int d[] = new int[5];              // di sản từ C, khó đọc

// --- Truy cập ---
b[0] = 99;
System.out.println(b[0]);            // 99
System.out.println(b.length);         // 3 — THUỘC TÍNH, không phải method!
// (so với String.length() và List.size() — ba cách viết khác nhau)

// --- Lỗi ngoài phạm vi ---
// System.out.println(b[3]);
//   ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3

// --- Kích thước CỐ ĐỊNH sau khi tạo ---
// b.length = 10;   // ❌ không gán được
// Muốn thay đổi kích thước → dùng ArrayList (chương 7)

Mảng là object — và điều đó có hệ quả

Mảng trong Java là kiểu tham chiếu: nó nằm trên heap, có thể là null, và == so địa chỉ chứ không so nội dung. Nó cũng là kiểu duy nhất được tạo mà không cần class khai báo trước — int[], String[][] đều là kiểu hợp lệ mà JVM tự sinh.

MangLaObject.java
int[] x = {1, 2, 3};
int[] y = {1, 2, 3};
System.out.println(x == y);                // false — hai object khác nhau
System.out.println(x.equals(y));           // false ⚠️ mảng KHÔNG override equals!
System.out.println(Arrays.equals(x, y));    // true  ✅ cách đúng

System.out.println(x);                     // [I@1b6d3586  — vô dụng
System.out.println(Arrays.toString(x));   // [1, 2, 3]    ✅

Chuỗi [I@1b6d3586 đọc là: [ nghĩa là mảng, I là int, sau @ là mã băm dạng thập lục phân. Đây là toString() mặc định của Object — mảng không override nó.

7.1. Mảng nhiều chiều và mảng răng cưa

MangHaiChieu.java
// Mảng hai chiều "chữ nhật"
int[][] mt = new int[3][4];        // 3 hàng, 4 cột
mt[0][0] = 1;

int[][] bang = {
    {1, 2, 3},
    {4, 5, 6},
};

// Duyệt hai chiều
for (int i = 0; i < bang.length; i++) {          // số HÀNG
    for (int j = 0; j < bang[i].length; j++) {   // số CỘT của hàng i
        System.out.printf("%4d", bang[i][j]);
    }
    System.out.println();
}

// Với for-each
for (int[] hang : bang) {
    for (int o : hang) System.out.printf("%4d", o);
    System.out.println();
}

// ⭐ MẢNG RĂNG CƯA (jagged) — mỗi hàng độ dài KHÁC nhau
int[][] rangCua = new int[3][];      // chỉ định số hàng
rangCua[0] = new int[2];
rangCua[1] = new int[5];
rangCua[2] = new int[1];

// Tam giác Pascal — ứng dụng tự nhiên của mảng răng cưa
int n = 5;
int[][] pascal = new int[n][];
for (int i = 0; i < n; i++) {
    pascal[i] = new int[i + 1];       // hàng i có i+1 phần tử
    pascal[i][0] = pascal[i][i] = 1;
    for (int j = 1; j < i; j++) {
        pascal[i][j] = pascal[i-1][j-1] + pascal[i-1][j];
    }
}
System.out.println(Arrays.deepToString(pascal));
// [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
📌 Java không có mảng hai chiều thật

int[][] thực chất là "mảng của các mảng" — mỗi hàng là một object riêng nằm rải rác trên heap. Điều này khác C, nơi mảng hai chiều là một khối bộ nhớ liên tục. Hệ quả: (1) mảng răng cưa là chuyện tự nhiên trong Java; (2) duyệt theo hàng nhanh hơn duyệt theo cột (do cache CPU); (3) tốn thêm bộ nhớ cho các con trỏ hàng.

7.2. Lớp tiện ích Arrays

ArraysUtil.java
import java.util.Arrays;

int[] a = {5, 2, 8, 1, 9};

// --- Hiển thị ---
Arrays.toString(a);                 // "[5, 2, 8, 1, 9]"
Arrays.deepToString(mangHaiChieu);   // cho mảng lồng nhau

// --- Sắp xếp (tại chỗ) ---
Arrays.sort(a);                     // [1, 2, 5, 8, 9]
Arrays.sort(a, 1, 4);               // chỉ sắp xếp [1..4)

Integer[] w = {5, 2, 8};
Arrays.sort(w, Comparator.reverseOrder());   // giảm dần — cần WRAPPER
// ⚠️ Không sắp xếp giảm dần trực tiếp trên int[] được — phải dùng Integer[]

// --- Tìm kiếm nhị phân (mảng PHẢI đã sắp xếp!) ---
int vt = Arrays.binarySearch(a, 5);   // 2
// Không thấy → trả về (-(điểm chèn) - 1), luôn là số âm

// --- Sao chép ---
int[] b = Arrays.copyOf(a, 3);         // 3 phần tử đầu
int[] c = Arrays.copyOf(a, 10);        // dài hơn → phần thừa = 0
int[] d = Arrays.copyOfRange(a, 1, 4);  // [1, 4)
int[] e = a.clone();                 // bản sao NÔNG (shallow)

// --- Điền, so sánh ---
Arrays.fill(a, 0);                   // đặt tất cả về 0
Arrays.equals(a, b);                  // so từng phần tử (1 chiều)
Arrays.deepEquals(x2d, y2d);          // cho mảng nhiều chiều

// --- Chuyển sang Stream / List (chương 7, 10) ---
int tong = Arrays.stream(a).sum();
List<String> ds = Arrays.asList("a", "b");   // ⚠️ list KÍCH THƯỚC CỐ ĐỊNH
// ds.add("c");  // UnsupportedOperationException
List<String> ds2 = new ArrayList<>(Arrays.asList("a", "b"));  // ✅ sửa được
🕳️ clone() chỉ sao chép nông
SaoChepNong.java
int[][] goc = {{1, 2}, {3, 4}};
int[][] sao = goc.clone();
sao[0][0] = 99;
System.out.println(goc[0][0]);   // 99 ⚠️ các HÀNG vẫn dùng chung!

// ✅ Sao chép sâu: clone từng hàng
int[][] sau = new int[goc.length][];
for (int i = 0; i < goc.length; i++) sau[i] = goc[i].clone();

clone() tạo mảng ngoài mới nhưng chép lại các tham chiếu hàng — nên cả hai mảng trỏ vào cùng những hàng cũ. Đây là biểu hiện trực tiếp của khái niệm "kiểu tham chiếu" ở chương 2.

8. Nhập dữ liệu — Scanner

NhapLieu.java
import java.util.Scanner;

public class NhapLieu {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("Tên: ");
        String ten = sc.nextLine();      // đọc CẢ DÒNG, kể cả dấu cách

        System.out.print("Tuổi: ");
        int tuoi = sc.nextInt();          // đọc MỘT TOKEN số nguyên

        System.out.print("Điểm: ");
        double diem = sc.nextDouble();

        System.out.printf("%s, %d tuổi, %.2f điểm%n", ten, tuoi, diem);
        sc.close();
    }
}
⚠️ Bẫy nổi tiếng nhất Java: nextInt() rồi nextLine()
ScannerBay.java
Scanner sc = new Scanner(System.in);

System.out.print("Tuổi: ");
int tuoi = sc.nextInt();       // người dùng gõ "22" rồi Enter
                                // nextInt() lấy "22", ĐỂ LẠI ký tự xuống dòng

System.out.print("Tên: ");
String ten = sc.nextLine();   // 💥 đọc ngay ký tự xuống dòng còn sót
                                //    → trả về chuỗi RỖNG, không đợi người dùng gõ!

System.out.println("[" + ten + "]");   // []

Ba cách sửa, theo thứ tự ưu tiên:

ScannerSua.java
// ✅ Cách 1 (tốt nhất): CHỈ dùng nextLine(), tự chuyển kiểu
int tuoi = Integer.parseInt(sc.nextLine().trim());
String ten = sc.nextLine();
// Nhất quán, không có ký tự sót, và kiểm soát được lỗi định dạng

// ✅ Cách 2: gọi nextLine() thừa một lần để "nuốt" ký tự sót
int tuoi2 = sc.nextInt();
sc.nextLine();                // bỏ phần còn lại của dòng
String ten2 = sc.nextLine();

// ✅ Cách 3: dùng next() nếu chỉ cần MỘT TỪ (không có dấu cách)
String maSo = sc.next();
NhapAnToan.java — mẫu đọc số an toàn
static int nhapSoNguyen(Scanner sc, String nhac, int min, int max) {
    while (true) {
        System.out.print(nhac);
        String tho = sc.nextLine().trim();
        try {
            int n = Integer.parseInt(tho);
            if (n < min || n > max) {
                System.out.printf("  Phải trong [%d, %d]%n", min, max);
                continue;
            }
            return n;
        } catch (NumberFormatException e) {
            System.out.printf("  '%s' không phải số nguyên%n", tho);
        }
    }
}

// Các method khác của Scanner:
// sc.hasNextInt()   — kiểm tra trước khi đọc, không ném lỗi
// sc.hasNextLine()  — còn dòng nào không (dùng khi đọc từ file)
// sc.nextBoolean(), sc.nextLong(), sc.nextDouble()
// new Scanner(new File("data.txt"))  — đọc từ tệp (chương 11)

9. Xuất dữ liệu — println, printf, format

XuatLieu.java
// --- Ba cách in cơ bản ---
System.out.println("Có xuống dòng");
System.out.print("Không xuống dòng");
System.out.printf("Có định dạng: %d%n", 42);

// --- Luồng lỗi chuẩn: dùng cho thông báo, không phải kết quả ---
System.err.println("Lỗi: không tìm thấy tệp");

// --- String.format: trả về chuỗi thay vì in ra ---
String s = String.format("%s có %d điểm", "An", 9);
String t = "%s có %d điểm".formatted("An", 9);   // Java 15+, gọn hơn
Chỉ địnhKiểuVí dụKết quả
%dSố nguyênprintf("%d", 42)42
%5dRộng 5, căn phảiprintf("%5d", 42)   42
%-5dRộng 5, căn tráiprintf("%-5d|", 42)42   |
%05dĐệm số 0printf("%05d", 42)00042
%,dPhân cách hàng nghìnprintf("%,d", 1500000)1,500,000
%fSố thực (6 chữ số)printf("%f", 3.14)3.140000
%.2f2 chữ số thập phânprintf("%.2f", 3.14159)3.14
%,.2fKết hợp cả haiprintf("%,.2f", 1234.5)1,234.50
%eKý hiệu khoa họcprintf("%e", 1234.5)1.234500e+03
%sChuỗi (gọi toString())printf("%s", obj)tuỳ object
%-10sChuỗi rộng 10, căn tráiprintf("%-10s|", "An")An        |
%bBooleanprintf("%b", true)true
%cKý tựprintf("%c", 'A')A
%x / %oHệ 16 / hệ 8printf("%x", 255)ff
%nXuống dòng (đa nền tảng)—\n hoặc \r\n
%%Dấu phần trămprintf("%d%%", 50)50%
BangDep.java — in bảng thẳng hàng
String[] ten  = {"Nguyễn Việt Anh", "Trần Bình", "Lê Chi"};
double[] diem = {8.75, 9.5, 7.25};
long[] hocPhi = {15_000_000L, 12_500_000L, 18_000_000L};

System.out.printf("%-20s %6s %15s%n", "HỌ TÊN", "ĐIỂM", "HỌC PHÍ");
System.out.println("-".repeat(43));
for (int i = 0; i < ten.length; i++) {
    System.out.printf("%-20s %6.2f %,15dđ%n", ten[i], diem[i], hocPhi[i]);
}

// HỌ TÊN                 ĐIỂM         HỌC PHÍ
// -------------------------------------------
// Nguyễn Việt Anh        8.75      15,000,000đ
// Trần Bình              9.50      12,500,000đ
// Lê Chi                 7.25      18,000,000đ
💡 Dùng %n thay \n

\n luôn là ký tự LF (mã 10). %n sinh ra ký tự xuống dòng của hệ điều hành hiện tại — LF trên Unix/macOS, CRLF trên Windows. Với tệp văn bản mà người dùng Windows sẽ mở bằng Notepad, khác biệt này quyết định họ thấy các dòng riêng biệt hay một dòng dài liền tù tì.

10. Cheat sheet chương 3

Tình huốngCách viết
Rẽ nhánh trả về giá trịvar x = switch (v) { case A -> …; default -> …; };
Rẽ nhánh theo kiểu (Java 21)case String s -> …, thêm điều kiện với when
Switch trên enumLiệt kê đủ hằng số, bỏ default để có kiểm tra biên dịch
Duyệt mảng/collectionfor (var x : ds)
Cần chỉ sốfor (int i = 0; i < a.length; i++)
Chạy ít nhất một lầndo { … } while (dk);
Thoát nhiều vòng lặpNhãn: ngoai: for(…) { … break ngoai; }
Độ dàiMảng: a.length · Chuỗi: s.length() · List: l.size()
In mảngArrays.toString(a) · nhiều chiều: Arrays.deepToString(a)
So sánh mảngArrays.equals(a, b) — không a.equals(b)
Sắp xếpArrays.sort(a); giảm dần cần Integer[] + Comparator
Sao chép mảngArrays.copyOf(a, n) hoặc a.clone() (nông!)
Đọc inputChỉ dùng sc.nextLine() + Integer.parseInt()
In bảng thẳng hàngprintf("%-20s %6.2f %,12d%n", …)
Xuống dòng đa nền tảng%n thay \n

Bài tập

Chuyển đổi switch cũ sang mới

Đoạn code sau viết theo kiểu Java 8 và có một bug do fall-through. Tìm bug, rồi viết lại bằng switch biểu thức.

CanSua.java
static String tinhPhiVanChuyen(String vung, double kg) {
    double phi = 0;
    switch (vung) {
        case "NOI_THANH":
            phi = 15000;
            break;
        case "NGOAI_THANH":
            phi = 25000;
        case "TINH_KHAC":
            phi = 40000 + kg * 5000;
            break;
        case "QUOC_TE":
            phi = 200000 + kg * 50000;
            break;
    }
    return String.format("%,.0fđ", phi);
}
  • Gọi với ("NGOAI_THANH", 2) trả về gì? Đúng ra phải là gì?
  • Viết lại bằng switch biểu thức, thêm nhánh default ném IllegalArgumentException.
  • Nâng cấp: đổi String vung thành enum Vung, rồi bỏ default. Thêm một hằng số mới vào enum và quan sát trình biên dịch báo lỗi.
Gợi ý

Câu 3 minh hoạ giá trị lớn nhất của switch biểu thức: nó biến "quên xử lý trường hợp mới" từ một bug lúc chạy thành một lỗi biên dịch. Đây là lý do nên tránh default khi switch trên enum — có default thì trình biên dịch không còn cảnh báo được nữa.

Thao tác ma trận

Viết class MaTran với các phương thức static:

API
static void    in(int[][] m);                // in thẳng hàng bằng printf
static int[][] chuyenVi(int[][] m);          // đổi hàng ↔ cột
static int[][] nhan(int[][] a, int[][] b);  // nhân ma trận
static boolean laDoiXung(int[][] m);         // m == chuyenVi(m)?
static int     tongDuongCheo(int[][] m);     // tổng đường chéo chính
static int[]   xoanOc(int[][] m);            // duyệt xoắn ốc từ ngoài vào
  • Kiểm tra hợp lệ: nhan phải ném IllegalArgumentException nếu số cột của a khác số hàng của b.
  • laDoiXung và tongDuongCheo chỉ áp dụng cho ma trận vuông — kiểm tra trước.
  • xoanOc phải chạy đúng với ma trận không vuông (ví dụ 3×5).
Gợi ý

xoanOc là bài phỏng vấn kinh điển. Cách làm: giữ bốn biên tren, duoi, trai, phai; đi phải → xuống → trái → lên, sau mỗi lượt thì thu hẹp biên tương ứng. Điều kiện dừng: tren > duoi || trai > phai. Cẩn thận trường hợp còn đúng một hàng hoặc một cột — cần kiểm tra lại biên trước hai lượt cuối để không đi hai lần.

Máy tính bỏ túi dòng lệnh

Viết ứng dụng menu vòng lặp, dùng Scanner:

Giao diện mong muốn
=== MÁY TÍNH ===
1. Cộng      2. Trừ
3. Nhân      4. Chia
5. Luỹ thừa  6. Căn bậc hai
7. Xem lịch sử
0. Thoát
Chọn: _
  • Dùng do-while cho vòng lặp menu, switch biểu thức để điều phối.
  • Không được sập với bất kỳ input nào: chữ cái, chuỗi rỗng, số quá lớn, chia cho 0, căn số âm.
  • Lưu lịch sử 10 phép tính gần nhất vào mảng và in ra khi chọn mục 7.
  • Kết quả in với 4 chữ số thập phân, có phân cách hàng nghìn.
Gợi ý

Lịch sử: dùng mảng String[10] kiểu đệm vòng — biến viTri tăng dần và lấy modulo 10, cùng biến soLuong đếm tổng để biết đã đầy chưa. Đây chính là ý tưởng của cấu trúc hàng đợi vòng mà bạn sẽ gặp lại ở Chương 7 với ArrayDeque.
Chia cho 0 với double cho Infinity chứ không ném lỗi — nhớ kiểm tra bằng Double.isInfinite() hoặc chặn từ đầu.

Trò chơi đoán số với thống kê

Máy chọn số ngẫu nhiên 1-100, người chơi đoán. Yêu cầu:

  • Gợi ý "lớn hơn" / "nhỏ hơn" sau mỗi lần đoán sai.
  • Giới hạn 7 lần đoán (đủ cho thuật toán nhị phân — hãy tự chứng minh vì sao 7 là đủ).
  • Ghi lại toàn bộ các lần đoán vào mảng, cuối ván in ra kèm nhận xét: người chơi có chơi tối ưu không?
  • Hỏi "chơi tiếp?" và cho phép chơi nhiều ván; cuối cùng in thống kê: số ván thắng/thua, số lần đoán trung bình, ván tốt nhất.
Gợi ý

int bimat = new Random().nextInt(100) + 1; hoặc ThreadLocalRandom.current().nextInt(1, 101).
Vì sao 7 lần là đủ: mỗi lần đoán giữa khoảng chia đôi không gian tìm kiếm, nên cần ⌈log₂(100)⌉ = 7 lần. Để nhận xét người chơi có tối ưu không, hãy so mỗi lần đoán với điểm giữa của khoảng còn lại tại thời điểm đó.

Phân tích văn bản

Viết chương trình nhận một đoạn văn bản (nhiều dòng, kết thúc bằng dòng chỉ có END) và in báo cáo:

  • Số dòng, số từ, số ký tự (kể cả và không kể khoảng trắng).
  • Từ dài nhất và từ ngắn nhất.
  • Tần suất từng chữ cái a-z (không phân biệt hoa thường) — vẽ biểu đồ cột bằng ký tự █.
  • 10 từ xuất hiện nhiều nhất, kèm số lần.

Ràng buộc: chỉ dùng mảng, String, vòng lặp — chưa dùng HashMap hay Stream (sẽ làm lại bằng công cụ đó ở chương 8 và 10 để so sánh).

Gợi ý

Đếm chữ cái: mảng int[26], chỉ số là c - 'a' sau khi Character.toLowerCase(c). Nhớ kiểm tra c >= 'a' && c <= 'z' để bỏ qua dấu và ký tự đặc biệt.
Đếm từ mà không có HashMap: tách từ vào một mảng, rồi với mỗi từ duyệt mảng "từ duy nhất" đã thu thập — đây là O(n²), và chính sự chậm chạp đó là động lực để bạn học HashMap ở chương 8. Hãy đo thời gian với văn bản 10.000 từ để cảm nhận rõ.

Quiz

Q1

Fall-through trong switch là gì? Vì sao nó nguy hiểm và khi nào nó hữu ích?

Xem đáp án
✓ Đáp án

Fall-through là hành vi của switch câu lệnh (kiểu cũ, dùng dấu hai chấm): sau khi khớp một case, chương trình chạy tiếp mọi nhánh phía dưới cho tới khi gặp break hoặc hết khối.

Nguy hiểm vì quên một chữ break giữa hàng chục nhánh là lỗi rất khó phát hiện khi đọc code — nó không phải lỗi cú pháp, chương trình vẫn chạy và chỉ cho kết quả sai.

Hữu ích khi cố ý gộp nhiều nhãn cùng xử lý: case 1: case 3: case 5: soNgay = 31; break;.

switch biểu thức (Java 14+, dùng ->) loại bỏ hoàn toàn fall-through, và cho phép gộp nhãn gọn hơn bằng dấu phẩy: case 1, 3, 5 -> 31;. Đây là lý do nên chuyển sang cú pháp mới.

Q2

switch biểu thức hơn switch câu lệnh ở những điểm nào?

Xem đáp án
✓ Đáp án

Năm điểm, theo thứ tự quan trọng:

  • Bắt buộc đủ nhánh — trình biên dịch yêu cầu mọi trường hợp phải được xử lý. Với enum, thêm một hằng số mới sẽ làm mọi switch liên quan báo lỗi biên dịch. Đây là lợi ích lớn nhất.
  • Không có fall-through — loại bỏ hẳn một lớp bug.
  • Là biểu thức — trả về giá trị, gán thẳng vào biến, không cần khai báo biến trước rồi gán trong từng nhánh.
  • Gộp nhãn gọn — case 1, 2, 3 ->.
  • Phạm vi biến riêng từng nhánh — switch cũ dùng chung một phạm vi cho cả khối, dễ gây xung đột tên.

Nhánh nhiều dòng dùng khối và yield để trả giá trị: case A -> { … yield giaTri; }.

Q3

Khi nào dùng for cổ điển, khi nào dùng for-each?

Xem đáp án
✓ Đáp án

for-each là mặc định — ngắn hơn, không thể sai chỉ số, không thể gây ArrayIndexOutOfBoundsException, và diễn đạt đúng ý "duyệt qua mọi phần tử".

Dùng for cổ điển khi:

  • Cần chỉ số (in số thứ tự, so sánh phần tử i với i-1).
  • Cần sửa phần tử của mảng — trong for-each, biến lặp chỉ là bản sao (với kiểu nguyên thủy) nên gán vào nó không đổi mảng.
  • Cần duyệt ngược, hoặc bước nhảy khác 1.
  • Cần duyệt một phần của mảng.

Một giới hạn nữa của for-each: không được thêm/xoá phần tử của collection khi đang duyệt — sẽ ném ConcurrentModificationException. Dùng Iterator.remove() hoặc removeIf() thay thế (chương 7).

Q4

Vì sao arr.equals(arr2) cho false dù hai mảng cùng nội dung?

Xem đáp án
✓ Đáp án

Vì mảng không override equals(). Nó dùng bản cài đặt mặc định của Object, mà bản đó chỉ so sánh địa chỉ — hệt như ==. Hai mảng khác nhau luôn có địa chỉ khác nhau, nên kết quả luôn false.

SoSanhMang.java
int[] a = {1, 2}, b = {1, 2};
a == b                      // false
a.equals(b)                  // false ⚠️
Arrays.equals(a, b)          // true  ✅ 1 chiều
Arrays.deepEquals(x, y)      // ✅ nhiều chiều

Tương tự, System.out.println(arr) in ra [I@1b6d3586 (mã băm) chứ không phải nội dung — phải dùng Arrays.toString(arr) hoặc Arrays.deepToString(arr). Đây là hai trong số những nhầm lẫn phổ biến nhất của người mới học Java.

Q5

Bẫy Scanner.nextInt() + nextLine() là gì? Sửa thế nào?

Xem đáp án
✓ Đáp án

nextInt() chỉ đọc token số nguyên và để lại ký tự xuống dòng trong bộ đệm. Lần nextLine() tiếp theo gặp ngay ký tự đó, hiểu là "dòng đã hết", và trả về chuỗi rỗng mà không hề đợi người dùng gõ gì.

Cách sửa, theo thứ tự nên dùng:

  • Chỉ dùng nextLine() cho mọi thứ, rồi tự chuyển kiểu bằng Integer.parseInt(s.trim()). Nhất quán, không có ký tự sót, và bạn kiểm soát được lỗi định dạng bằng try/catch NumberFormatException.
  • Gọi thêm một sc.nextLine() ngay sau nextInt() để "nuốt" ký tự sót.
  • Dùng next() nếu chỉ cần một từ không có dấu cách.

Bẫy này tồn tại vì Scanner trộn hai mô hình đọc: theo token (next*) và theo dòng (nextLine). Chọn một mô hình và dùng nhất quán là cách phòng tránh triệt để.

Q6

Java có mảng hai chiều thật không? int[][] thực chất là gì?

Xem đáp án
✓ Đáp án

Không. int[][] là "mảng của các mảng int" — mảng ngoài chứa các tham chiếu, mỗi tham chiếu trỏ tới một mảng một chiều riêng biệt nằm rải rác trên heap. Điều này khác C, nơi mảng hai chiều là một khối bộ nhớ liên tục.

Ba hệ quả:

  • Mảng răng cưa là tự nhiên — mỗi hàng có độ dài riêng: new int[3][] rồi gán từng hàng. Tam giác Pascal là ví dụ điển hình.
  • Duyệt theo hàng nhanh hơn theo cột — các phần tử trong một hàng nằm liền nhau nên tận dụng được cache CPU; nhảy giữa các hàng thì không.
  • clone() chỉ sao chép nông — tạo mảng ngoài mới nhưng dùng chung các hàng cũ. Muốn bản sao thật phải clone() từng hàng.

Cũng vì vậy, m.length là số hàng, còn m[i].length là số cột của riêng hàng i — luôn dùng dạng thứ hai khi duyệt, để code chạy đúng cả với mảng răng cưa.

Q7

break ngoai; (nhãn) làm gì? Java có goto không?

Xem đáp án
✓ Đáp án

Nhãn cho phép break hoặc continue tác động lên một vòng lặp bên ngoài, thay vì chỉ vòng lặp trong cùng. Đặt nhãn trước vòng lặp (ngoai:) rồi dùng break ngoai; để thoát cả cụm.

  • break nhan; — thoát hẳn vòng lặp mang nhãn đó.
  • continue nhan; — sang lần lặp tiếp theo của vòng lặp mang nhãn đó.

Java không có goto. Từ khoá goto được giữ lại trong danh sách từ khoá (cùng với const) nhưng không dùng được — đây là quyết định thiết kế có chủ đích để tránh mã spaghetti. Nhãn là thứ gần goto nhất, nhưng nó bị giới hạn chỉ dùng với vòng lặp và chỉ nhảy ra ngoài, nên không tạo ra luồng điều khiển rối rắm được.

Thực tế: nếu thấy cần nhãn, hãy cân nhắc tách phần đó thành phương thức riêng và dùng return — thường dễ đọc và dễ test hơn.

Q8

%n và \n khác nhau thế nào? Khi nào khác biệt đó quan trọng?

Xem đáp án
✓ Đáp án

\n là một ký tự cố định — LF (mã 10), luôn luôn như vậy trên mọi hệ điều hành. %n là một chỉ định định dạng, được thay bằng ký tự xuống dòng của hệ điều hành đang chạy: LF trên Unix/macOS, CRLF (\r\n) trên Windows.

Khi nào quan trọng:

  • Ghi tệp văn bản mà người dùng Windows sẽ mở bằng Notepad hoặc Excel — dùng \n thì họ thấy một dòng dài liền tù tì.
  • Sinh tệp CSV — nhiều công cụ yêu cầu CRLF theo chuẩn RFC 4180.
  • Giao tiếp qua giao thức mạng — HTTP và SMTP quy định rõ CRLF.

Khi in ra terminal thì thường không khác biệt vì terminal hiện đại xử lý được cả hai. Lưu ý %n chỉ dùng được trong printf / String.format — trong println thì không cần vì nó đã tự thêm ký tự xuống dòng đúng của hệ thống (System.lineSeparator()).

Tổng kết

Sau chương 3, bạn đã nắm:

  • if: điều kiện phải là boolean; luôn dùng ngoặc nhọn; guard clause để giảm lồng nhau.
  • switch cũ: fall-through và nguy cơ của nó; gộp nhãn là cách dùng hợp lý duy nhất.
  • switch biểu thức (Java 14+): ->, yield, trả giá trị, và bắt buộc đủ nhánh.
  • Pattern matching (Java 21): case String s, when, xử lý null, kiểm tra thứ tự nhánh lúc biên dịch.
  • Bốn vòng lặp và tiêu chí chọn; giới hạn của for-each.
  • Nhãn để thoát vòng lặp lồng nhau; Java không có goto.
  • Mảng: là object, length là thuộc tính, kích thước cố định, int[][] là mảng của mảng.
  • Arrays: toString/deepToString, sort, binarySearch, copyOf, equals — và bẫy sao chép nông.
  • Scanner: bẫy nextInt+nextLine, mẫu đọc số an toàn.
  • printf: bảng chỉ định định dạng, in bảng thẳng hàng, %n thay \n.

Kết nối

  • Chương 4 (OOP cơ bản) — phương thức, tham số, return; mảng đối tượng.
  • Chương 5 (OOP nâng cao) — enum kết hợp switch để có kiểm tra đủ nhánh; sealed class mở rộng ý tưởng đó cho class.
  • Chương 6 (Ngoại lệ) — ArrayIndexOutOfBoundsException, NumberFormatException, InputMismatchException.
  • Chương 7 (Collections) — ArrayList thay mảng khi cần kích thước động; ConcurrentModificationException; removeIf.
  • Chương 10 (Stream API) — thay thế phần lớn vòng lặp bằng filter/map/collect.
  • Chương 11 (Tệp tin) — Scanner đọc từ tệp; Files.readAllLines.
  • Trụ cột DSA — mảng là nền của mọi cấu trúc dữ liệu; xoắn ốc, tìm kiếm nhị phân, hai con trỏ.
  • Python Chương 4 — đối chiếu: Python có match/case từ 3.10 với ý tưởng tương tự pattern matching, và for…else mà Java không có.