Backend

99 Ngày Java — Ngày 09: Chuỗi String cơ bản

SSite Admin
6 tháng 08, 2026 6 phút đọc 31 lượt xem
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.

Sketchnote Ngày 09: chuỗi String trong Java — bất biến, equals vs ==, method hay dùng

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ến
  • Quê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ằng charAt(i), chỉ số vẫn từ 0 như 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 DUNG

Nguy 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.

Những dòng code rực rỡ trên màn hình — chuỗi ký tự có mặt ở khắp mọi nơi trong lập trình

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ại
  • length() có ngoặc — method, trái với length field 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 ước i < n đã gặp ở copyOfRange.

  • strip() (Java 11+) hiểu Unicode tốt hơn trim() cũ — dùng strip cho lành.

  • Chuỗi rỗng "" khác chuỗi null: gọi method trên nullNullPointerException — kiểm tra s != 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 += b phả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ên StringBuilder — 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ành nam.tran@example.com chỉ 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".equals tươ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ằng indexOf + vòng lặp while (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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9