99 Ngày Java — Ngày 09: Chuỗi String cơ bản
Ngày 08 khép lại với lời hẹn về "mảng ký tự" nổi tiếng nhất Java — hôm nay nó đây: String. Chuỗi có mặt ở mọi nơi — tên người dùng, URL, JSON — nhưng ẩn sau vẻ quen thuộc là hai đặc sản: tính bất biến (immutable) và cái bẫy so sánh == mà bài Ngày 08 đã "nhá hàng". Hôm nay ta mổ xẻ vì sao chuỗi không bao giờ đổi, so sánh chuỗi thế nào cho đúng, điểm danh các method dùng hằng ngày, và bẫy hiệu năng khi nối chuỗi trong vòng lặp.

String là gì? Vì sao bất biến?
String không phải kiểu nguyên thủy (Ngày 03) — nó là đối tượng trên heap, và biến chuỗi chỉ giữ tham chiếu, y hệt mảng ở Ngày 08. Nhưng khác mảng một điểm chí mạng: chuỗi bất biến — sinh ra với nội dung nào thì giữ nguyên nội dung đó đến hết đời:
String ten = "Java";
ten.toUpperCase(); // tạo chuỗi MỚI "JAVA"… rồi vứt đi luôn
System.out.println(ten); // Java — chuỗi gốc KHÔNG hề đổi
ten = ten.toUpperCase(); // muốn giữ kết quả: phải GÁN LẠI
System.out.println(ten); // JAVA — biến trỏ sang chuỗi mới
// Mọi method "sửa" chuỗi đều trả về CHUỖI MỚI:
// toUpperCase, replace, strip, substring, concat… — chuỗi gốc bất biếnQuên gán lại là lỗi kinh điển số một với người mới — gọi
toUpperCase()xong thắc mắc "sao chuỗi không đổi?".Bất biến đổi lại nhiều quà: chia sẻ chuỗi giữa các nơi an toàn tuyệt đối (không ai sửa được sau lưng ai), làm key trong
HashMapổn định, và mở đường cho String pool ngay bên dưới.Ký tự bên trong là
char(Ngày 03) — truy cập bằngcharAt(i), chỉ số vẫn từ0như mảng.
Bẫy kinh điển: equals vs ==
Vì chuỗi bất biến, JVM chơi một nước cờ tiết kiệm: các literal giống nhau được gom về một chỗ gọi là String pool — hai biến cùng "java" thực chất trỏ cùng một đối tượng. Chính sự tiết kiệm đó tạo ra cái bẫy nổi tiếng nhất Java:
String a = "java";
String b = "java"; // literal → dùng CHUNG một chuỗi trong String pool
String c = new String("java"); // new → ép tạo đối tượng MỚI trên heap
System.out.println(a == b); // true — cùng tham chiếu (nhờ pool) → dễ ngộ nhận!
System.out.println(a == c); // false — hai đối tượng khác nhau
System.out.println(a.equals(c)); // true — so NỘI DUNG: luôn dùng cái này
System.out.println(a.equalsIgnoreCase("JAVA")); // true — bỏ qua hoa/thường
// Bài học Ngày 08 trở lại: == so THAM CHIẾU, equals so NỘI DUNGNguy hiểm ở chỗ: == với hai literal cho true nên code chạy "có vẻ đúng"… cho tới khi chuỗi đến từ bên ngoài — người dùng nhập, đọc file, API trả về — không nằm trong pool, và == lặng lẽ trả false. Quy tắc sắt: so sánh nội dung chuỗi luôn dùng equals (hoặc equalsIgnoreCase); == chỉ dành cho câu hỏi "có phải cùng một đối tượng không?". Còn contract đầy đủ của equals/hashCode — hẹn Ngày 24.
Hộp đồ nghề String
Bất biến không có nghĩa là nghèo nàn — String có hơn 60 method; đây là bộ dùng hằng ngày:
String s = " Hoc Java 21 moi ngay! ";
s.length() // 25 — là METHOD, có () — khác length của mảng!
s.charAt(2) // 'H' — ký tự tại chỉ số 2 (vẫn đếm từ 0)
s.strip() // "Hoc Java 21 moi ngay!" — cắt khoảng trắng hai đầu
s.contains("Java") // true
s.indexOf("Java") // 6 — không tìm thấy thì trả -1
s.substring(6, 10) // "Java" — từ 6 đến TRƯỚC 10 (nửa đóng nửa mở!)
s.replace("21", "25") // đổi MỌI "21" thành "25" — tất nhiên là trả chuỗi mới
s.isBlank() // false — chuỗi rỗng hoặc toàn khoảng trắng?
"9,7,8".split(",") // {"9", "7", "8"} — cắt thành MẢNG (Ngày 08!)
String.join("-", "a", "b", "c") // "a-b-c" — chiều ngược lạilength()có ngoặc — method, trái vớilengthfield của mảng Ngày 08. Java cổ điển hỏi câu này suốt trong phỏng vấn.substring(6, 10)lấy từ 6 đến trước 10 — nửa đóng nửa mở, đúng quy ướci < nđã gặp ởcopyOfRange.strip()(Java 11+) hiểu Unicode tốt hơntrim()cũ — dùngstripcho lành.Chuỗi rỗng
""khác chuỗinull: gọi method trênnulllàNullPointerException— kiểm tras != null && !s.isBlank()trước khi xử lý đầu vào.
Nối chuỗi & bẫy hiệu năng
Toán tử + quen thuộc từ Ngày 04 có nghĩa thứ hai: đứng cạnh chuỗi, nó thành nối chuỗi — và mang theo hai cái bẫy:
String ho = "Mot", ten = "Dev";
String full = ho + " " + ten; // "Mot Dev" — + là nối chuỗi
String sai = "Điểm: " + 9 + 5; // "Điểm: 95" — tính TRÁI QUA PHẢI!
String dung = "Điểm: " + (9 + 5); // "Điểm: 14" — ngoặc tính trước
// Nối trong VÒNG LẶP = bẫy hiệu năng: mỗi dấu + đẻ ra một chuỗi mới
String ketQua = "";
for (int i = 0; i < 10_000; i++) {
ketQua += i; // ~10.000 chuỗi trung gian bị vứt đi!
}
// Thuốc giải tên là StringBuilder — hẹn Ngày 21 mổ xẻBẫy thứ tự: biểu thức tính trái qua phải — gặp chuỗi trước là mọi thứ phía sau bị "dính" thành chuỗi. Muốn cộng số thật, đóng ngoặc.
Bẫy hiệu năng: chuỗi bất biến nên
a += bphải tạo chuỗi mới rồi chép cả hai vào — trong vòng lặp lớn là thảm họa cấp số nhân. Nối vài mảnh lẻ thì+vô tư (compiler tự tối ưu), nhưng nối dồn trong vòng lặp hãy nhớ tênStringBuilder— Ngày 21 phân tích kỹ.
Bài tập nhỏ
Cho
String email = " Nam.Tran@Example.COM ": chuẩn hóa thànhnam.tran@example.comchỉ bằng các method hôm nay.Viết đoạn kiểm tra một chuỗi có phải palindrome (đọc xuôi ngược như nhau) — gợi ý:
charAt+ hai "con trỏ" từ hai đầu, như bài đảo mảng Ngày 08.Chạy thử
new String("java") == "java"và.equalstương ứng — tự chứng kiến pool bằng mắt.Đếm số lần xuất hiện của
"a"trong một chuỗi bằngindexOf+ vòng lặpwhile(Ngày 07).
Kết luận
String gói gọn trong bốn ý: đối tượng bất biến — mọi method trả chuỗi mới, phải gán lại; so nội dung dùng equals, == chỉ so tham chiếu; hộp đồ nghề hơn 60 method với quy ước chỉ số quen thuộc; và + tiện tay nhưng đừng nối dồn trong vòng lặp. Ngày 10 ta đóng gói logic vào phương thức (method) — khai báo, overloading, và sự thật đằng sau câu thần chú "Java luôn pass-by-value". Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


