Buổi 2: Chuỗi và cấu trúc điều khiển
Hãy hình dung bạn được giao viết phần mềm nhỏ cho phòng đào tạo: nhận một dòng văn bản dạng 20211234,Nguyễn Văn An,8.5, tách ra mã sinh viên, họ tên và điểm, chuẩn hóa tên cho đẹp, xếp loại học lực rồi in bảng điểm của cả lớp. Chỉ với chuỗi (String) để xử lý văn bản và cấu trúc điều khiển (rẽ nhánh, vòng lặp) để quyết định "làm gì" và "lặp bao nhiêu lần", bạn đã viết được rất nhiều chương trình hữu ích. Đây là buổi nền tảng: hầu hết bài tập lập trình sau này đều quay về hai công cụ này.
Mục tiêu buổi học
- Giải thích được vì sao
Stringtrong Java là bất biến và phân biệt được==vớiequals. - Dùng thành thạo các phương thức thường gặp của
String,StringBuildervà định dạng đầu ra bằngString.format/printf. - Xác định được phạm vi (scope) của biến trong khối lệnh và tránh các lỗi do trùng tên, dùng biến ngoài phạm vi.
- Viết được rẽ nhánh
if / else if / elsevàswitchcổ điển (kể cả hiện tượng fall-through) cho bài toán phân loại. - Chọn đúng
for,while,do-while, for-each; dùngbreak,continue, nhãn (label) và vòng lặp lồng nhau để in hình, duyệt bảng, tìm kiếm.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1 | Chuỗi: bất biến, hàm thường dùng, so sánh | 30 phút |
| 2 | StringBuilder và định dạng đầu ra | 25 phút |
| 3 | Khối lệnh và phạm vi biến | 10 phút |
| 4 | Rẽ nhánh: if/else, switch | 35 phút |
| 5 | Vòng lặp: for, while, do-while, for-each | 35 phút |
| 6 | break, continue, nhãn và vòng lặp lồng | 30 phút |
| 7 | Tổng hợp: menu bảng điểm, tổng kết | 15 phút |
Khoảng 55% thời gian dành cho thực hành: sau mỗi phần lý thuyết bạn có một nhóm bài tập trên Codelab để làm ngay.
1. Chuỗi (String)
1.1. String là đối tượng bất biến
Trong Java, chuỗi không phải kiểu nguyên thủy mà là một lớp (java.lang.String). Mỗi chuỗi như "java" là một đối tượng nằm trên heap, và điều quan trọng nhất cần nhớ là: sau khi tạo ra, nội dung của nó không bao giờ thay đổi — gọi là bất biến (immutable). Mọi phương thức "có vẻ như sửa chuỗi" (toUpperCase, replace, trim…) thực chất trả về một chuỗi mới và để nguyên chuỗi cũ.
Đoạn code dưới đây cho thấy cái bẫy kinh điển: gọi toUpperCase() mà không gán kết quả.
String s = "java";
s.toUpperCase(); // kết quả bị bỏ đi
System.out.println(s); // vẫn là java
s = s.toUpperCase(); // gán lại mới có tác dụng
System.out.println(s);Vì sao Java thiết kế như vậy? Bất biến giúp chuỗi an toàn khi chia sẻ giữa nhiều nơi (không ai lén sửa), dùng được làm khóa trong HashMap, và cho phép JVM tái sử dụng chuỗi giống nhau thông qua string pool — vùng nhớ đặc biệt chứa các chuỗi literal.
Hình dưới minh họa hai cách tạo chuỗi: viết literal "java" thì JVM dùng lại đối tượng trong pool, còn new String("java") luôn tạo đối tượng mới trên heap.
String a = "java";
String b = "java";
String c = new String("java");- a == bcùng một tham chiếu (cùng ô trong pool)true
- a == chai tham chiếu khác nhaufalse
- a.equals(c)so sánh nội dung chuỗitrue
- a == c.intern()intern() trả về ô trong pooltrue
Luôn so sánh nội dung chuỗi bằng equals(); == chỉ so sánh địa chỉ nên dễ cho kết quả bất ngờ. intern() đưa/tìm chuỗi trong pool và trả về tham chiếu tới ô đó.
Hãy đọc hình theo thứ tự: hai biến cùng viết literal sẽ trỏ về một đối tượng trong pool; biến tạo bằng new trỏ sang một đối tượng khác dù nội dung giống hệt. Chính vì vậy == (so sánh tham chiếu) và equals (so sánh nội dung) có thể cho kết quả khác nhau — chúng ta sẽ quay lại ở mục 1.3.
Lưu ý: Bất biến không có nghĩa là biến không đổi được.
s = s.toUpperCase();đổi biếnssang trỏ vào đối tượng mới; đối tượng"java"cũ vẫn nguyên vẹn (và sẽ được dọn rác nếu không ai dùng).
1.2. Các phương thức thường dùng
Chuỗi là dãy ký tự đánh chỉ số từ 0. Ký tự cuối có chỉ số length() - 1. Bảng sau liệt kê những phương thức bạn sẽ dùng hằng ngày:
| Phương thức | Ý nghĩa | Ví dụ → kết quả |
|---|---|---|
length() |
Độ dài chuỗi | "abc".length() → 3 |
charAt(i) |
Ký tự tại chỉ số i |
"abc".charAt(1) → 'b' |
substring(a, b) |
Đoạn từ a đến trước b |
"abcdef".substring(1, 4) → "bcd" |
substring(a) |
Từ a đến hết |
"abcdef".substring(4) → "ef" |
indexOf(x) |
Vị trí xuất hiện đầu tiên, -1 nếu không có |
"banana".indexOf("na") → 2 |
lastIndexOf(x) |
Vị trí xuất hiện cuối cùng | "banana".lastIndexOf("na") → 4 |
contains(x) |
Có chứa chuỗi con không | "banana".contains("nan") → true |
startsWith(x) / endsWith(x) |
Kiểm tra tiền tố / hậu tố | "CNTT101".startsWith("CNTT") → true |
toUpperCase() / toLowerCase() |
Đổi hoa / thường | "Java".toLowerCase() → "java" |
trim() / strip() |
Cắt khoảng trắng hai đầu | " a ".trim() → "a" |
isEmpty() / isBlank() |
Rỗng / rỗng hoặc chỉ toàn dấu cách (isBlank từ Java 11) |
" ".isBlank() → true |
replace(a, b) |
Thay mọi lần xuất hiện | "a-b-c".replace("-", "+") → "a+b+c" |
repeat(n) |
Lặp chuỗi n lần (Java 11) |
"ab".repeat(3) → "ababab" |
toCharArray() |
Chuyển thành mảng char |
"hi".toCharArray() → {'h','i'} |
Hãy chạy thử để xem các phương thức này phối hợp với nhau. Chú ý chỉ số bắt đầu từ 0 và substring(a, b) không lấy ký tự ở vị trí b.
String s = " Codelab NEU ";
String t = s.trim();
System.out.println("[" + t + "]");
System.out.println(t.length());
System.out.println(t.charAt(0));
System.out.println(t.substring(0, 7));
System.out.println(t.indexOf("NEU"));
System.out.println(t.toUpperCase());
System.out.println(t.replace(' ', '_'));
System.out.println(t.contains("lab"));Hai phương thức indexOf và substring thường đi cùng nhau để cắt chuỗi theo dấu phân cách. Ví dụ tách phần tên đăng nhập và tên miền của một email:
String email = "[email protected]";
int at = email.indexOf('@');
String user = email.substring(0, at);
String domain = email.substring(at + 1);
System.out.println(user);
System.out.println(domain);
System.out.println(email.endsWith("neu.edu.vn"));Tách và ghép chuỗi: split và join
split(dauPhanCach) cắt một chuỗi thành mảng các chuỗi con; String.join(dauNoi, cacPhanTu) làm ngược lại. Đây là cặp công cụ chính để đọc dữ liệu dạng CSV (các cột cách nhau bằng dấu phẩy) từ một dòng văn bản.
String line = "20211234,Nguyễn Văn An,8.5";
String[] parts = line.split(",");
System.out.println(parts.length);
System.out.println("MSSV : " + parts[0]);
System.out.println("Tên : " + parts[1]);
double score = Double.parseDouble(parts[2]);
System.out.println("Điểm : " + score);
System.out.println(String.join(" | ", parts));Muốn tách theo khoảng trắng (một hay nhiều dấu cách) hãy dùng split("\\s+"); chuỗi này là một biểu thức chính quy (regex) nên dấu \ phải viết đôi trong code Java. Bạn không cần học regex sâu ở buổi này, chỉ cần nhớ đúng hai mẫu "\\s+" (khoảng trắng) và "," (dấu phẩy).
Mẹo: Muốn đổi chuỗi sang số dùng
Integer.parseInt("42")hoặcDouble.parseDouble("3.5"); ngược lại muốn đổi số sang chuỗi dùngString.valueOf(42)hoặc đơn giản là"" + 42. Nếu chuỗi không phải số,parseIntnémNumberFormatException— buổi 4 sẽ học cách xử lý.
Duyệt từng ký tự
Vì charAt(i) lấy được từng ký tự, bạn có thể duyệt chuỗi bằng vòng lặp và dùng các hàm tiện ích của lớp Character như isDigit, isLetter, isUpperCase, toUpperCase. Ví dụ đếm số chữ cái, chữ số và khoảng trắng trong một mã:
String s = "Lop CNTT 01 - Khoa 65";
int letters = 0, digits = 0, spaces = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isLetter(c)) letters++;
else if (Character.isDigit(c)) digits++;
else if (c == ' ') spaces++;
}
System.out.println(letters + " " + digits + " " + spaces);(Đừng lo nếu chưa quen vòng for — mục 5 sẽ giải thích chi tiết. Ở đây chỉ cần hiểu: biến i chạy từ 0 đến length() - 1.)
1.3. So sánh chuỗi
Đây là lỗi số một của người mới học Java. Toán tử == với đối tượng so sánh địa chỉ (hai biến có cùng trỏ vào một đối tượng không), còn equals so sánh nội dung. Với chuỗi, bạn gần như luôn muốn so sánh nội dung.
Mô phỏng sau cho bạn tự tạo chuỗi bằng literal và bằng new, rồi xem == và equals trả về gì.
String: == hay equals()?
Chọn cách tạo hai chuỗi, xem chúng nằm ở đâu trong bộ nhớQuy tắc: so sánh nội dung chuỗi luôn dùng equals(); == chỉ nên dùng để kiểm tra cùng một đối tượng (hoặc null).
Hãy thử cả hai trường hợp trong demo, sau đó kiểm tra lại bằng code thật. Chú ý dòng thứ ba: intern() đưa chuỗi về đối tượng trong pool nên == lại cho true, nhưng đây là chi tiết để hiểu cơ chế, không phải cách so sánh bạn nên dùng.
String a = "java";
String b = "java";
String c = new String("java");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == c.intern());
System.out.println(a.equals(c));
System.out.println("JAVA".equalsIgnoreCase(a));Ngoài equals, bạn có hai công cụ khác:
equalsIgnoreCase(x): so sánh nội dung, bỏ qua hoa/thường.compareTo(x): so sánh thứ tự từ điển, trả về số âm nếu chuỗi đứng trướcx,0nếu bằng, số dương nếu đứng sau. Dùng để sắp xếp.
System.out.println("apple".compareTo("banana") < 0);
System.out.println("b".compareTo("a"));
System.out.println("abc".compareTo("abc"));
System.out.println("Z".compareTo("a") < 0);Dòng cuối cho true vì so sánh dựa trên mã Unicode: mọi chữ hoa ('Z' = 90) đứng trước chữ thường ('a' = 97). Muốn sắp xếp không phân biệt hoa/thường hãy dùng compareToIgnoreCase.
Cảnh báo: Viết
if (name == "An")đôi khi chạy đúng (khi cả hai là literal cùng pool) nhưng sai khi chuỗi đến từScanner,splithay nối chuỗi — một lỗi rất khó tìm. Luôn dùngname.equals("An"); và nếunamecó thể lànullthì viết ngược lại"An".equals(name)để không bịNullPointerException.
| Cần làm | Dùng | Không dùng |
|---|---|---|
| Hai chuỗi có cùng nội dung? | a.equals(b) |
a == b |
| Bỏ qua hoa/thường | a.equalsIgnoreCase(b) |
a.toLowerCase() == b |
| Thứ tự từ điển | a.compareTo(b) |
a < b (không biên dịch) |
| Chuỗi rỗng? | s.isEmpty() hoặc s.length() == 0 |
s == "" |
Sau khi nắm cách thao tác và so sánh chuỗi, bạn đã có đủ công cụ cho nhóm bài đầu tiên.
Vừa học xong các phương thức của String và cách tách chuỗi, hãy làm các bài sau để luyện cắt, đảo và duyệt chuỗi.
2. StringBuilder và định dạng đầu ra
2.1. Vì sao cần StringBuilder
Vì String bất biến, mỗi lần bạn viết s = s + "x" Java phải tạo một chuỗi hoàn toàn mới rồi sao chép toàn bộ nội dung cũ vào. Nối vài lần thì không sao, nhưng nối hàng chục nghìn lần trong vòng lặp thì chi phí tăng theo bình phương số lần nối. Lớp StringBuilder giải quyết bằng cách giữ một bộ đệm có thể thay đổi: thêm, chèn, xóa, đảo ngược ngay trên bộ đệm đó, và chỉ tạo String khi bạn gọi toString().
| Phương thức | Tác dụng |
|---|---|
append(x) |
Thêm x vào cuối (nhận mọi kiểu: chuỗi, số, ký tự…) |
insert(i, x) |
Chèn x vào vị trí i |
deleteCharAt(i) / delete(a, b) |
Xóa một ký tự / một đoạn |
setCharAt(i, c) |
Gán lại ký tự tại i |
reverse() |
Đảo ngược toàn bộ nội dung |
length(), charAt(i) |
Giống String |
toString() |
Lấy ra một String |
Ví dụ sau xây một chuỗi từng bước và đảo ngược nó. Lưu ý các lời gọi có thể nối chuỗi liên tiếp (method chaining) vì append trả về chính StringBuilder.
StringBuilder sb = new StringBuilder();
sb.append("Java").append(' ').append(13);
System.out.println(sb);
sb.insert(0, ">> ");
System.out.println(sb);
sb.setCharAt(3, 'j');
System.out.println(sb);
sb.reverse();
System.out.println(sb.toString());
sb.deleteCharAt(0);
System.out.println(sb.length());Đây là cách chuẩn để đảo một chuỗi: new StringBuilder(s).reverse().toString(). Dưới đây là một bài toán kiểm tra palindrome (chuỗi đọc xuôi ngược giống nhau), kết hợp so sánh nội dung bằng equals:
String word = "level";
String rev = new StringBuilder(word).reverse().toString();
System.out.println(rev);
System.out.println(word.equals(rev));
String other = "codelab";
System.out.println(other.equals(new StringBuilder(other).reverse().toString()));Khi nào dùng cái nào? Quy tắc thực dụng: nếu nối trong vòng lặp hoặc cần sửa tại chỗ thì dùng StringBuilder; còn nối vài mảnh cố định trong một câu lệnh thì cứ dùng toán tử + cho dễ đọc (trình biên dịch tự tối ưu).
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append(i);
if (i < 5) sb.append(", ");
}
System.out.println(sb);
String s = "";
for (int i = 1; i <= 5; i++) s += i + " ";
System.out.println(s.trim());Mẹo:
StringBufferlà "anh em" củaStringBuildernhưng an toàn khi nhiều luồng cùng dùng (và chậm hơn một chút). Trong chương trình một luồng, hãy chọnStringBuilder.
2.2. Định dạng đầu ra với String.format và printf
In bảng điểm cho đẹp cần căn lề, chặn số chữ số thập phân, thêm số 0 đứng đầu… Java có String.format(dinhDang, ...) (trả về chuỗi) và System.out.printf(dinhDang, ...) (in luôn) dùng chung cú pháp: chuỗi định dạng chứa các đặc tả bắt đầu bằng %.
| Đặc tả | Ý nghĩa | Ví dụ |
|---|---|---|
%d |
Số nguyên | %d với 42 → 42 |
%5d |
Số nguyên căn phải, rộng 5 | 42 |
%05d |
Rộng 5, đệm số 0 | 00042 |
%,d |
Có dấu phân cách hàng nghìn | 1,234,567 |
%f / %.2f |
Số thực / làm tròn 2 chữ số thập phân | 3.14 |
%s |
Chuỗi | An |
%-10s |
Chuỗi căn trái, rộng 10 | An |
%10s |
Chuỗi căn phải, rộng 10 | An |
%c |
Một ký tự | A |
%n |
Xuống dòng (đúng theo hệ điều hành) | |
%% |
Dấu phần trăm | % |
Ví dụ dưới in một bảng điểm có đường kẻ. Mỗi cột có độ rộng cố định nên các dòng thẳng hàng:
String[] names = {"An", "Binh", "Cuong"};
double[] gpa = {8.5, 7.25, 9.0};
System.out.println(String.format("%-4s%-10s%6s", "STT", "Ten", "Diem"));
for (int i = 0; i < names.length; i++) {
System.out.printf("%03d %-10s%6.2f%n", i + 1, names[i], gpa[i]);
}
System.out.println(String.format("%,d dong", 1234567));Có một chi tiết dễ gây khó chịu: định dạng số thực phụ thuộc ngôn ngữ (locale) của máy. Trên máy Việt Nam, %.2f có thể in 3,14 (dấu phẩy) thay vì 3.14. Muốn kết quả cố định ở mọi nơi (đặc biệt khi chấm bài tự động), hãy truyền locale rõ ràng:
double pi = 3.14159;
System.out.println(String.format(java.util.Locale.US, "%.2f", pi));
System.out.println(String.format(java.util.Locale.US, "%8.3f|", pi));
System.out.println(String.format("%s có %d ký tự", "Codelab", 7));Ví dụ thực tế: Hóa đơn điện thoại, bảng lương, báo cáo doanh thu… đều cần định dạng tiền tệ có dấu phân cách và hai chữ số thập phân.
printf("%,.2f", amount)giải quyết gọn cả hai việc.
Sau khi biết cách xây dựng và định dạng chuỗi, hãy luyện tập các bài biến đổi chuỗi.
Vừa học xong StringBuilder và String.format, hãy làm các bài sau để thực hành biến đổi, mã hóa chuỗi và định dạng đầu ra.
3. Khối lệnh và phạm vi biến
Một khối lệnh (block) là đoạn code nằm giữa cặp ngoặc nhọn { ... }. Thân của if, vòng lặp, phương thức đều là khối. Quy tắc phạm vi (scope) rất đơn giản: một biến chỉ tồn tại từ chỗ khai báo đến hết khối chứa nó. Ra khỏi khối, biến biến mất và dùng tiếp sẽ bị lỗi biên dịch.
int total = 0; // sống đến hết đoạn code
for (int i = 1; i <= 3; i++) { // i chỉ sống trong vòng for
int square = i * i; // square chỉ sống trong thân vòng
total += square;
}
System.out.println(total);
// System.out.println(i); // lỗi biên dịch: không thấy i ở đây
{
int temp = 10; // khối lệnh "trần": temp chỉ sống trong khối
System.out.println(temp);
}
int temp = 20; // được phép: temp cũ đã hết phạm vi
System.out.println(temp);Có ba điều cần nhớ:
- Biến khai báo trong vòng lặp được tạo lại ở mỗi lượt; muốn giữ giá trị qua các lượt phải khai báo ngoài vòng (như
total). - Java không cho phép khai báo biến cục bộ trùng tên với một biến cục bộ khác còn đang trong phạm vi (khác C++). Viết
int total = 5;bên trong vòng for ở trên sẽ báo lỗivariable total is already defined. - Biến phải được gán giá trị trước khi dùng: trình biên dịch từ chối
int x; System.out.println(x);.
Lưu ý: Khai báo biến ở phạm vi nhỏ nhất có thể. Biến chỉ dùng trong vòng lặp thì khai báo ngay trong vòng lặp (hoặc ngay trong phần khởi tạo của
for) — code dễ đọc hơn và ít lỗi dùng nhầm hơn.
Phạm vi biến còn liên quan đến lỗi thường gặp khi rẽ nhánh: nếu bạn khai báo biến bên trong if rồi dùng bên ngoài, trình biên dịch sẽ báo không tìm thấy biến. Cách đúng là khai báo trước if với giá trị mặc định, rồi gán trong các nhánh — ta sẽ thấy ngay ở phần tiếp theo.
4. Rẽ nhánh: if/else và switch
Chương trình thực tế luôn phải ra quyết định: điểm bao nhiêu thì đạt, năm nào là năm nhuận, hóa đơn trên mức nào thì giảm giá. Java có hai công cụ chính: if / else if / else cho điều kiện bất kỳ, và switch cho việc chọn theo một giá trị cụ thể.
4.1. if, else if, else
Cú pháp cơ bản: điều kiện là một biểu thức kiểu boolean đặt trong ngoặc tròn. Các nhánh được kiểm tra từ trên xuống, nhánh đầu tiên đúng sẽ chạy và các nhánh còn lại bị bỏ qua.
if (dieuKien1) {
...
} else if (dieuKien2) {
...
} else {
... // tất cả điều kiện trên đều sai
}Ví dụ xếp loại học lực theo điểm hệ 10. Vì các nhánh chạy từ trên xuống, ta chỉ cần viết cận dưới của từng loại — điều kiện diem >= 7.0 đã ngầm hiểu là "không đạt 8.5 trở lên" vì nhánh trước đã loại rồi.
double score = 7.5;
String grade;
if (score >= 8.5) {
grade = "Giỏi";
} else if (score >= 7.0) {
grade = "Khá";
} else if (score >= 5.0) {
grade = "Trung bình";
} else {
grade = "Yếu";
}
System.out.println(score + " -> " + grade);Chú ý biến grade được khai báo trước if để dùng được ở bên dưới (đúng như nói ở mục 3), và mọi nhánh đều gán giá trị cho nó nên trình biên dịch yên tâm.
Điều kiện phức hợp dùng && (và), || (hoặc), ! (phủ định). Cả && và || đều đoản mạch (short-circuit): vế phải chỉ được tính khi cần. Ví dụ kinh điển là năm nhuận: chia hết cho 4 mà không chia hết cho 100, hoặc chia hết cho 400.
int[] years = {1900, 2000, 2023, 2024};
for (int y : years) {
boolean leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
System.out.println(y + (leap ? " nhuận" : " không nhuận"));
}Ở ví dụ trên ta dùng luôn toán tử ba ngôi dieuKien ? giaTriNeuDung : giaTriNeuSai (đã học ở buổi 1) để chọn một trong hai giá trị gọn trong một biểu thức. Nên dùng cho lựa chọn đơn giản; chọn nhiều nhánh thì viết if cho dễ đọc.
Những cái bẫy hay gặp khi viết if
- Dấu
=thay cho==.if (x = 5)không biên dịch được vớiint(Java cầnboolean), nhưng với biếnbooleanthì lại biên dịch và gán giá trị — cẩn thận. - Dấu chấm phẩy thừa.
if (x > 0);kết thúc câu lệnhifngay tại đó; khối{ ... }phía sau luôn chạy. - Bỏ ngoặc nhọn. Nếu thân
ifchỉ có một lệnh thì được phép bỏ{}, nhưng khi thêm lệnh thứ hai bạn sẽ quên ngoặc. Hãy luôn viết ngoặc nhọn. elsebám vàoifgần nhất (dangling else): thụt lề không quyết định logic, ngoặc nhọn mới quyết định.- So sánh số thực bằng
==.0.1 + 0.2 == 0.3chofalsevì sai số làm tròn nhị phân. Hãy so sánh bằng ngưỡng sai số:Math.abs(a - b) < 1e-9.
double a = 0.1 + 0.2;
System.out.println(a);
System.out.println(a == 0.3);
System.out.println(Math.abs(a - 0.3) < 1e-9);
int n = 15;
if (n % 3 == 0)
if (n % 5 == 0)
System.out.println("chia hết 15");
else
System.out.println("chia hết 3, không chia hết 5");Ở đây thụt lề tình cờ khớp với ý nghĩa thật: else thuộc về if (n % 5 == 0) gần nhất. Nhưng hãy thử dời else ra ngang hàng với if ngoài cùng — trình biên dịch vẫn gắn nó vào if trong, vì nó chỉ nhìn cấu trúc ngoặc chứ không nhìn khoảng trắng. Luôn dùng ngoặc nhọn để chính bạn và người đọc không nhầm.
Cảnh báo: Thứ tự các nhánh
else ifrất quan trọng. Nếu viếtscore >= 5.0trướcscore >= 8.5, mọi điểm từ 5 trở lên đều rơi vào nhánh "Trung bình". Hãy đi từ điều kiện chặt nhất đến lỏng nhất.
Hãy xem một bài phân loại tam giác theo ba cạnh — ví dụ điển hình cho việc kiểm tra điều kiện hợp lệ trước, phân loại sau:
int a = 3, b = 4, c = 5;
if (a + b <= c || a + c <= b || b + c <= a) {
System.out.println("Không phải tam giác");
} else if (a == b && b == c) {
System.out.println("Đều");
} else if (a == b || b == c || a == c) {
System.out.println("Cân");
} else if (a * a + b * b == c * c || a * a + c * c == b * b || b * b + c * c == a * a) {
System.out.println("Vuông");
} else {
System.out.println("Thường");
}4.2. switch cổ điển
Khi cần chọn nhánh theo giá trị rời rạc của một biểu thức (số nguyên, char, String, hoặc enum), switch gọn hơn một chuỗi else if. Cú pháp:
switch (bieuThuc) {
case giaTri1:
...
break;
case giaTri2:
...
break;
default:
...
}Cơ chế hoạt động: Java so khớp giá trị của bieuThuc với từng case, nhảy tới case khớp và chạy tiếp xuống dưới cho đến khi gặp break hoặc hết switch. Nhánh default chạy khi không case nào khớp (không bắt buộc nhưng nên có).
char rank = 'B';
switch (rank) {
case 'A':
System.out.println("Giỏi");
break;
case 'B':
System.out.println("Khá");
break;
case 'C':
System.out.println("Trung bình");
break;
default:
System.out.println("Không hợp lệ");
}Hiện tượng fall-through (rơi xuống) — quên break — là nguồn lỗi nổi tiếng nhưng cũng là một công cụ hữu ích: ta cố ý bỏ break để gom nhiều case vào cùng một xử lý. Bài "số ngày trong tháng" là ví dụ kinh điển:
int month = 4, year = 2024, days;
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
days = 31;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
case 2:
days = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
break;
default:
days = -1;
}
System.out.println("Tháng " + month + "/" + year + " có " + days + " ngày");switch cũng nhận String (từ Java 7), so khớp bằng equals nên an toàn:
String cmd = "stop";
switch (cmd) {
case "start":
System.out.println("Bắt đầu");
break;
case "stop":
System.out.println("Dừng");
case "pause":
System.out.println("(rơi xuống pause)");
break;
default:
System.out.println("Lệnh lạ");
}Chạy thử bạn sẽ thấy cả hai dòng Dừng và (rơi xuống pause) được in ra vì case "stop" thiếu break. Muốn chọn theo khoảng (ví dụ điểm 0–10) với switch, ta thường biến khoảng thành giá trị rời rạc, chẳng hạn switch ((int) score) hoặc switch (score / 10).
| So sánh | if / else if |
switch |
|---|---|---|
| Kiểu điều kiện | Bất kỳ biểu thức boolean (khoảng, &&, ` |
|
| Kiểu dữ liệu | Mọi kiểu | int, char, byte, short, String, enum (không dùng được double, boolean) |
| Dễ sai | Sai thứ tự nhánh | Quên break |
| Dùng khi | Phân loại theo khoảng | Chọn lệnh, tháng, mã loại… |
Mở rộng (Java 14+): Từ Java 14,
switchcó dạng mũi têncase 1, 2 -> …và dùng được như một biểu thức trả về giá trị, không còn fall-through. Môi trường chạy của khóa học là Java 13 nên ở đây ta chỉ dùngswitchcổ điển.
Đến đây bạn đã đủ kiến thức để làm các bài về rẽ nhánh.
Vừa học xong if/else if và switch, hãy làm các bài sau để luyện phân loại theo khoảng, kiểm tra điều kiện hợp lệ và giải bài toán nhiều trường hợp.
5. Vòng lặp: for, while, do-while, for-each
Vòng lặp cho phép chạy lại một khối lệnh nhiều lần: cộng dồn điểm cả lớp, in bảng cửu chương, đọc dữ liệu cho tới khi hết. Java có bốn dạng; chọn đúng dạng giúp code ngắn và ít lỗi.
| Vòng lặp | Dùng khi | Kiểm tra điều kiện |
|---|---|---|
for |
Biết trước số lượt (đếm từ a đến b) | Trước mỗi lượt |
while |
Không biết trước số lượt, lặp khi còn điều kiện | Trước mỗi lượt (có thể chạy 0 lần) |
do-while |
Phải chạy ít nhất một lần (nhập liệu, menu) | Sau mỗi lượt |
| for-each | Duyệt mọi phần tử của mảng/tập hợp, không cần chỉ số | Tự động |
5.1. Vòng for
Cú pháp gồm ba phần trong ngoặc, cách nhau bằng dấu ;: khởi tạo (chạy một lần), điều kiện (kiểm tra trước mỗi lượt), cập nhật (chạy sau mỗi lượt).
for (khoiTao; dieuKien; capNhat) {
thanLenh;
}Thứ tự thực thi: khởi tạo → kiểm tra điều kiện → (nếu đúng) thân lệnh → cập nhật → kiểm tra điều kiện … cho đến khi điều kiện sai. Bài toán "tổng từ 1 đến n" là ví dụ đầu tiên:
int n = 5, sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
System.out.println("i = " + i + ", sum = " + sum);
}
System.out.println("Tổng = " + sum);Mô phỏng sau chạy vòng for từng bước và hiển thị bảng giá trị các biến sau mỗi lượt. Hãy đoán trước giá trị của sum rồi bấm bước tiếp theo để kiểm chứng; chú ý thời điểm điều kiện sai và vòng lặp dừng.
Theo dõi vòng lặp for từng bước
Khởi tạo → kiểm tra → thân → cập nhật → kiểm tra …Bạn có thể đổi bước nhảy (i += 2), đếm ngược (i--) hoặc bắt đầu từ giá trị khác. Ví dụ in các số chẵn giảm dần:
for (int i = 10; i >= 0; i -= 2) {
System.out.print(i + " ");
}
System.out.println();5.2. Vòng while
while kiểm tra điều kiện trước mỗi lượt, rất hợp khi số lượt phụ thuộc vào dữ liệu. Ví dụ điển hình: tách từng chữ số của một số bằng phép % 10 (lấy chữ số cuối) và / 10 (bỏ chữ số cuối).
int n = 20241;
int sum = 0, count = 0;
while (n > 0) {
sum += n % 10;
n /= 10;
count++;
}
System.out.println("Số chữ số: " + count);
System.out.println("Tổng chữ số: " + sum);Một bài khác: đếm số ước của n. Ta thử mọi i từ 1 đến n và đếm những i chia hết n. (Có thể tối ưu chỉ thử đến sqrt(n), nhưng bản đơn giản này đủ cho bài nhỏ.)
int n = 36, divisors = 0;
int i = 1;
while (i <= n) {
if (n % i == 0) divisors++;
i++;
}
System.out.println(n + " có " + divisors + " ước");Cảnh báo: Vòng lặp vô hạn là lỗi phổ biến nhất của
while: quên câu lệnh làm điều kiện dần trở nên sai (ở trên lài++vàn /= 10). Mỗi khi viếtwhile, hãy tự hỏi "cái gì khiến điều kiện sai đi?".
5.3. Vòng do-while
do-while chạy thân lệnh trước, rồi mới kiểm tra điều kiện — vì vậy luôn chạy ít nhất một lần. Nhớ rằng cuối while(...) phải có dấu ;. Nó hợp với việc nhập đến khi hợp lệ, hoặc đảo số như sau (xây số mới từ các chữ số cuối của số cũ):
int n = 1230, rev = 0;
do {
rev = rev * 10 + n % 10;
n /= 10;
} while (n > 0);
System.out.println(rev);Hãy so sánh với while khi điều kiện sai ngay từ đầu:
int k = 0;
while (k > 0) System.out.println("while chạy");
do System.out.println("do-while chạy một lần"); while (k > 0);Ví dụ thực tế hơn: nhập điểm cho tới khi nằm trong khoảng 0–10. Các dòng // @stdin cung cấp dữ liệu như người dùng gõ vào bàn phím: 15 và -3 bị từ chối, 8 được chấp nhận.
Scanner sc = new Scanner(System.in);
int score;
do {
System.out.print("Nhập điểm (0-10): ");
score = sc.nextInt();
} while (score < 0 || score > 10);
System.out.println();
System.out.println("Đã nhận: " + score);5.4. Vòng for-each (enhanced for)
Khi chỉ cần đọc từng phần tử (không cần biết chỉ số), for-each gọn và an toàn hơn vì không thể sai chỉ số:
for (KieuPhanTu x : mangHoacTapHop) {
...
}int[] scores = {8, 6, 9, 5, 7};
int total = 0, best = scores[0];
for (int x : scores) {
total += x;
if (x > best) best = x;
}
System.out.println("Tổng " + total + ", cao nhất " + best);
for (char c : "Java".toCharArray()) {
System.out.print(c + "-");
}
System.out.println();Hạn chế của for-each: không biết chỉ số hiện tại, và gán x = ... chỉ đổi bản sao cục bộ chứ không sửa phần tử trong mảng. Khi cần chỉ số hoặc cần sửa phần tử, dùng for thường.
Mẹo: Chọn vòng lặp theo câu hỏi: "Tôi biết số lượt?" →
for. "Tôi duyệt hết một tập?" → for-each. "Lặp tới khi điều kiện đổi?" →while. "Phải làm ít nhất một lần?" →do-while.
Bạn đã có đủ bốn dạng vòng lặp để giải các bài đếm, cộng dồn và tách chữ số.
Vừa học xong for, while, do-while, hãy làm các bài sau để luyện cộng dồn, tách chữ số, đếm ước và in bảng.
6. break, continue, nhãn và vòng lặp lồng
6.1. break và continue
Hai câu lệnh này thay đổi luồng chạy bình thường của vòng lặp:
break— thoát hẳn khỏi vòng lặp (hoặcswitch) gần nhất.continue— bỏ qua phần còn lại của lượt hiện tại và nhảy tới lượt kế tiếp (vớifor, nhảy qua phần cập nhật).
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // bỏ qua số chẵn
if (i > 7) break; // gặp số lớn hơn 7 thì dừng hẳn
System.out.print(i + " ");
}
System.out.println();break hay dùng cho bài tìm kiếm: thấy rồi thì dừng, khỏi duyệt tiếp. Ví dụ kiểm tra số nguyên tố — chỉ cần một ước trong khoảng 2..sqrt(n) là kết luận hợp số.
int n = 97;
boolean prime = n >= 2;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
prime = false;
break;
}
}
System.out.println(n + (prime ? " là số nguyên tố" : " không là số nguyên tố"));Lưu ý: Trong
while, hãy cẩn thận khi dùngcontinue: nếu phần cập nhật biến nằm saucontinuethì nó bị bỏ qua và bạn rơi vào vòng lặp vô hạn. Vớiforthì an toàn vì phần cập nhật nằm trong dấu ngoặc củafor.
6.2. Vòng lặp lồng nhau
Đặt một vòng lặp trong thân vòng lặp khác: vòng ngoài chạy một lượt thì vòng trong chạy trọn vẹn. Tổng số lượt = số lượt ngoài × số lượt trong. Đây là công cụ để in hình, duyệt bảng hai chiều, thử mọi cặp giá trị.
Ví dụ in tam giác sao: dòng i có i dấu *. Vòng ngoài lo dòng, vòng trong lo cột trong dòng đó.
int n = 4;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
System.out.print("*");
}
System.out.println();
}Biến thể: tam giác cân cần in thêm khoảng trắng đứng đầu. Với dòng i (1 đến n) ta in n - i dấu cách rồi 2*i - 1 dấu sao. Phương thức repeat giúp code gọn hơn nhưng ta vẫn nên hiểu cách dùng vòng lặp lồng.
int n = 4;
for (int i = 1; i <= n; i++) {
for (int s = 0; s < n - i; s++) System.out.print(" ");
for (int k = 0; k < 2 * i - 1; k++) System.out.print("*");
System.out.println();
}Bảng cửu chương cũng là vòng lặp lồng điển hình, kết hợp printf để căn lề:
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= 5; j++) {
System.out.printf("%4d", i * j);
}
System.out.println();
}Ví dụ thực tế: Hệ thống xếp lịch thi cần duyệt mọi cặp (ca thi, phòng thi) để kiểm tra trùng — đó chính là hai vòng lặp lồng. Nhưng hãy nhớ độ phức tạp: lồng 3 vòng với
n = 1000là một tỷ lượt, rất chậm.
6.3. Nhãn (label) với break và continue
break và continue thường chỉ tác động đến vòng trong cùng. Muốn thoát khỏi vòng ngoài, đặt một nhãn (tên kèm dấu :) trước vòng cần thoát rồi viết break tenNhan; hoặc continue tenNhan;.
Ví dụ tìm vị trí đầu tiên của giá trị K trong ma trận: vừa thấy là phải thoát cả hai vòng, không chỉ vòng trong.
int[][] a = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int k = 6, foundRow = -1, foundCol = -1;
search:
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[i].length; j++) {
if (a[i][j] == k) {
foundRow = i;
foundCol = j;
break search; // thoát cả hai vòng
}
}
}
System.out.println(foundRow + " " + foundCol);continue có nhãn thì nhảy sang lượt kế của vòng ngoài. Ví dụ in các hàng mà không có số âm (hàng nào gặp số âm thì bỏ cả hàng):
int[][] a = {{1, 2}, {3, -4}, {5, 6}};
rows:
for (int i = 0; i < a.length; i++) {
for (int x : a[i]) {
if (x < 0) continue rows;
}
System.out.println("hàng " + i + " hợp lệ");
}Mẹo: Nhãn là công cụ mạnh nhưng dễ làm code khó theo dõi. Chỉ dùng khi thật cần thoát nhiều vòng; nhiều khi tách đoạn tìm kiếm thành một phương thức rồi dùng
return(buổi 4) sẽ rõ ràng hơn.
Cuối cùng, hãy ghép tất cả: dưới đây là các bài tìm kiếm với break, thoát vòng lồng bằng nhãn và in hình bằng vòng lặp lồng.
Vừa học xong break, continue, nhãn và vòng lặp lồng, hãy làm các bài sau để luyện tìm kiếm có dừng sớm và in hình.
7. Tổng hợp: menu bảng điểm
Ta ghép các mảnh của buổi học thành một chương trình nhỏ: đọc nhiều dòng MSSV,Họ tên,Điểm từ bàn phím, chuẩn hóa tên (cắt khoảng trắng, đổi chữ cái đầu mỗi từ thành hoa), xếp loại, in bảng đẹp và thống kê. Chương trình dùng Scanner + while, split, StringBuilder, printf, if/else if và switch — mọi thứ chúng ta vừa học.
Scanner sc = new Scanner(System.in);
int count = 0, passed = 0;
double sum = 0;
while (sc.hasNextLine()) {
String line = sc.nextLine().trim();
if (line.equals("end")) break;
String[] p = line.split(",");
String[] words = p[1].trim().toLowerCase().split("\\s+");
StringBuilder name = new StringBuilder();
for (String w : words) {
if (name.length() > 0) name.append(' ');
name.append(Character.toUpperCase(w.charAt(0))).append(w.substring(1));
}
double score = Double.parseDouble(p[2]);
String rank;
if (score >= 8.5) rank = "Giỏi";
else if (score >= 7.0) rank = "Khá";
else if (score >= 5.0) rank = "Trung bình";
else rank = "Yếu";
System.out.printf("%-10s %-18s %5.2f %s%n", p[0], name, score, rank);
count++;
sum += score;
if (score >= 5.0) passed++;
}
System.out.printf("Tổng %d sinh viên, đạt %d, điểm trung bình %.2f%n", count, passed, sum / count);Hãy đọc code theo từng lớp: vòng while đọc từng dòng đến khi gặp end (dùng break); split(",") tách cột; vòng for-each + StringBuilder chuẩn hóa tên; chuỗi if/else if xếp loại; printf căn lề cột. Bạn có thể sửa dữ liệu @stdin hoặc thêm xếp loại để tự thử.
Mở rộng (Java 14+): Từ Java 15 có text block (
""") để viết chuỗi nhiều dòng và Java 14 cóswitchbiểu thức; khi nâng cấp phiên bản JDK, nhiều đoạn code ở buổi này có thể viết gọn hơn nữa.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
So sánh chuỗi bằng == |
Kết quả sai tùy chuỗi đến từ đâu | Dùng equals / equalsIgnoreCase |
Gọi s.trim(), s.toUpperCase() mà không gán kết quả |
Chuỗi không đổi (vì bất biến) | s = s.trim(); |
Nối chuỗi bằng + trong vòng lặp lớn |
Chậm, tốn bộ nhớ | Dùng StringBuilder.append |
substring(a, b) tính nhầm b bao hàm |
Lệch một ký tự hoặc StringIndexOutOfBoundsException |
b là vị trí loại trừ |
Dùng %.2f không chỉ định Locale |
Dấu phẩy/chấm khác nhau theo máy | String.format(Locale.US, ...) |
Quên break trong switch |
Chạy luôn các nhánh phía dưới (fall-through) | Thêm break (hoặc cố ý và ghi chú) |
Sắp xếp sai thứ tự các nhánh else if |
Phân loại sai | Đi từ điều kiện chặt đến lỏng |
Dấu ; thừa sau if (...) / for (...) |
Thân lệnh không thuộc về điều kiện/vòng lặp | Bỏ dấu ; |
Sai biên vòng lặp (< thay cho <=) |
Thiếu hoặc thừa một lượt (off-by-one) | Dò bằng ví dụ nhỏ n = 1, 2 |
Quên cập nhật biến điều kiện của while |
Vòng lặp vô hạn | Xác định "cái gì làm điều kiện sai" |
So sánh double bằng == |
Sai do sai số làm tròn | Math.abs(a - b) < eps |
Tóm tắt
Stringbất biến: mọi phương thức biến đổi đều trả về chuỗi mới. So sánh nội dung bằngequals, thứ tự bằngcompareTo.- Dùng
charAt,substring,indexOf,split,join,replace,trim… cho thao tác thường gặp;StringBuilderkhi nối nhiều lần hoặc sửa tại chỗ;String.format/printfđể căn lề và làm tròn. - Biến chỉ sống trong khối khai báo nó; khai báo ở phạm vi nhỏ nhất có thể.
if / else if / elsecho điều kiện theo khoảng (nhánh đầu tiên đúng chạy);switchcho giá trị rời rạc, nhớbreakvà hiểu fall-through.forkhi biết số lượt,whilekhi lặp theo điều kiện,do-whilekhi chạy ít nhất một lần, for-each để duyệt mọi phần tử.breakthoát vòng,continuebỏ qua lượt, nhãn dùng để thoát/nhảy vòng ngoài; vòng lồng cho bảng và hình, nhưng số lượt nhân lên.