- Viết
if/else if/elsegọn gàng; biết dùng "trả về sớm" để giảm lồng nhau. - Phân biệt
switchcâu lệnh (cũ, có fall-through) vàswitchbiểu thức (Java 14+, an toàn hơn). - Dùng pattern matching cho
switch(Java 21) — thay thế chuỗiinstanceof. - 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
Scannervà tránh bẫynextInt()+nextLine(). - Định dạng đầu ra bằng
printf/String.format— bảng chỉ định định dạng.
1. if / else if / else
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()) { } // ✅
if// ① 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.
// ❌ 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ó
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
break là chạy tiếp nhánh sauint 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.
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 switch | Từ phiên bản |
|---|---|
byte, short, char, int (và wrapper của chúng) | Java 1.0 |
enum | Java 5 |
String | Java 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ị.
// ✅ 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ánh | case x: … break; | case x -> …; |
| Fall-through | ✅ Có (nguồn bug) | ❌ Không |
| Trả giá trị | ❌ Không | ✅ Có |
| Gộp nhãn | Xếp chồng case | case 1, 2, 3 -> |
| Nhánh nhiều dòng | Viế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ến | Chung cho cả switch | Riê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.
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.
// ❌ 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";
};
}
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
// ① 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ại | Kiểm tra điều kiện | Chạy tối thiểu | Dùng khi |
|---|---|---|---|
for | Trước mỗi vòng | 0 lần | Biết số lần lặp, cần chỉ số |
for-each ⭐ | Ngầm (còn phần tử không) | 0 lần | Mặc định — duyệt mảng/collection |
while | Trước mỗi vòng | 0 lần | Không biết trước số lần |
do-while | Sau mỗi vòng | 1 lần | Menu, đọc input, thử lại |
for-each// ① 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
// 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 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
// --- 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.
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
// 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]]
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
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ôngint[][] 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
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();
}
}
nextInt() rồi nextLine()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:
// ✅ 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();
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
// --- 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ỉ định | Kiểu | Ví dụ | Kết quả |
|---|---|---|---|
%d | Số nguyên | printf("%d", 42) | 42 |
%5d | Rộng 5, căn phải | printf("%5d", 42) | 42 |
%-5d | Rộng 5, căn trái | printf("%-5d|", 42) | 42 | |
%05d | Đệm số 0 | printf("%05d", 42) | 00042 |
%,d | Phân cách hàng nghìn | printf("%,d", 1500000) | 1,500,000 |
%f | Số thực (6 chữ số) | printf("%f", 3.14) | 3.140000 |
%.2f | 2 chữ số thập phân | printf("%.2f", 3.14159) | 3.14 |
%,.2f | Kết hợp cả hai | printf("%,.2f", 1234.5) | 1,234.50 |
%e | Ký hiệu khoa học | printf("%e", 1234.5) | 1.234500e+03 |
%s | Chuỗi (gọi toString()) | printf("%s", obj) | tuỳ object |
%-10s | Chuỗi rộng 10, căn trái | printf("%-10s|", "An") | An | |
%b | Boolean | printf("%b", true) | true |
%c | Ký tự | printf("%c", 'A') | A |
%x / %o | Hệ 16 / hệ 8 | printf("%x", 255) | ff |
%n | Xuống dòng (đa nền tảng) | — | \n hoặc \r\n |
%% | Dấu phần trăm | printf("%d%%", 50) | 50% |
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đ
%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ống | Cá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 enum | Liệt kê đủ hằng số, bỏ default để có kiểm tra biên dịch |
| Duyệt mảng/collection | for (var x : ds) |
| Cần chỉ số | for (int i = 0; i < a.length; i++) |
| Chạy ít nhất một lần | do { … } while (dk); |
| Thoát nhiều vòng lặp | Nhãn: ngoai: for(…) { … break ngoai; } |
| Độ dài | Mảng: a.length · Chuỗi: s.length() · List: l.size() |
| In mảng | Arrays.toString(a) · nhiều chiều: Arrays.deepToString(a) |
| So sánh mảng | Arrays.equals(a, b) — không a.equals(b) |
| Sắp xếp | Arrays.sort(a); giảm dần cần Integer[] + Comparator |
| Sao chép mảng | Arrays.copyOf(a, n) hoặc a.clone() (nông!) |
| Đọc input | Chỉ dùng sc.nextLine() + Integer.parseInt() |
| In bảng thẳng hàng | printf("%-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.
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
switchbiểu thức, thêm nhánhdefaultnémIllegalArgumentException. - Nâng cấp: đổi
String vungthànhenum 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:
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ệ:
nhanphải némIllegalArgumentExceptionnếu số cột củaakhác số hàng củab. laDoiXungvàtongDuongCheochỉ áp dụng cho ma trận vuông — kiểm tra trước.xoanOcphả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:
=== 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-whilecho vòng lặp menu,switchbiể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
Fall-through trong switch là gì? Vì sao nó nguy hiểm và khi nào nó hữu ích?
Xem đá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.
switch biểu thức hơn switch câu lệnh ở những điểm nào?
Xem đá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ọiswitchliê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 —
switchcũ 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; }.
Khi nào dùng for cổ điển, khi nào dùng for-each?
Xem đá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).
Vì sao arr.equals(arr2) cho false dù hai mảng cùng nội dung?
Xem đá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.
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.
Bẫy Scanner.nextInt() + nextLine() là gì? Sửa thế nào?
Xem đá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ằngInteger.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ằngtry/catch NumberFormatException. - Gọi thêm một
sc.nextLine()ngay saunextInt()để "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 để.
Java có mảng hai chiều thật không? int[][] thực chất là gì?
Xem đá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ảiclone()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.
break ngoai; (nhãn) làm gì? Java có goto không?
Xem đá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.
%n và \n khác nhau thế nào? Khi nào khác biệt đó quan trọng?
Xem đá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
\nthì 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.switchcũ: fall-through và nguy cơ của nó; gộp nhãn là cách dùng hợp lý duy nhất.switchbiể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,
lengthlà 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ẫynextInt+nextLine, mẫu đọc số an toàn.printf: bảng chỉ định định dạng, in bảng thẳng hàng,%nthay\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) —
enumkết hợpswitchđể có kiểm tra đủ nhánh;sealedclass mở rộng ý tưởng đó cho class. - Chương 6 (Ngoại lệ) —
ArrayIndexOutOfBoundsException,NumberFormatException,InputMismatchException. - Chương 7 (Collections) —
ArrayListthay 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/casetừ 3.10 với ý tưởng tương tự pattern matching, vàfor…elsemà Java không có.