Backend

99 Ngày Java — Ngày 21: StringBuilder & xử lý chuỗi nâng cao

SSite Admin
17 tháng 08, 2026 6 phút đọc 2 lượt xem
99 Ngày Java — Ngày 21: StringBuilder & xử lý chuỗi nâng cao

Giai đoạn Core API mở đầu bằng thao tác phổ biến nhất trong mọi chương trình: xử lý chuỗi. Ngày 09 đã xác lập String là bất biến — hôm nay ta đối mặt với hệ quả hiệu năng của tính chất đó: vì sao nối chuỗi bằng + trong vòng lặp có chi phí bình phương, StringBuilder giải quyết thế nào, String pool vận hành ra sao — và hai công cụ hiện đại giúp chuỗi dễ đọc: String.format cùng text block.

Sketchnote Ngày 21: StringBuilder & xử lý chuỗi nâng cao — chi phí nối +, String pool, String.format và text block

Chi phí thật của nối chuỗi bằng +

// String là BẤT BIẾN (Ngày 09) — "nối" thực chất là TẠO CHUỖI MỚI:
String ketQua = "";
for (int i = 0; i < 10_000; i++) {
    ketQua = ketQua + i;        // mỗi vòng: tạo một String MỚI,
}                               // sao chép TOÀN BỘ nội dung cũ sang

// Vòng i = 9.999: sao chép ~40.000 ký tự chỉ để thêm vài ký tự
// → tổng chi phí tăng theo BÌNH PHƯƠNG số vòng lặp (O(n²))

// Lưu ý công bằng: nối + trong MỘT biểu thức thì hoàn toàn ổn —
String loiChao = "Xin chào " + ten + "!";
// compiler tự tối ưu biểu thức đơn lẻ.
// Vấn đề CHỈ nằm ở nối chuỗi TÍCH LŨY TRONG VÒNG LẶP.
  • Gốc rễ nằm ở tính bất biến: không thể "gắn thêm" vào String hiện có — mỗi phép + tích lũy tạo đối tượng mới và sao chép toàn bộ nội dung cũ; 10.000 vòng lặp là hàng chục triệu thao tác sao chép ký tự.

  • Phân định công bằng để không sửa nhầm chỗ: + trong một biểu thức đơn lẻ được compiler tối ưu sẵn — vấn đề chỉ nằm ở tích lũy qua nhiều vòng lặp.

StringBuilder — bộ đệm ký tự có thể ghi

// StringBuilder: bộ đệm ký tự CÓ THỂ GHI — nối không tạo chuỗi mới:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    sb.append(i);               // ghi thẳng vào bộ đệm — O(1) trung bình
}
String ketQua = sb.toString();  // tạo String đúng MỘT lần ở cuối

// Bộ API hay dùng:
sb.append("Java")               // nối mọi kiểu dữ liệu
  .insert(0, ">> ")             // chèn tại vị trí
  .reverse();                   // đảo ngược tại chỗ
sb.length();  sb.charAt(3);  sb.deleteCharAt(0);

// Biết trước kích thước? Khai báo capacity để tránh cấp phát lại:
StringBuilder lon = new StringBuilder(100_000);

// StringBuffer: phiên bản đồng bộ hóa (thread-safe) — hầu như
// không còn cần: xây chuỗi trong MỘT luồng rồi chia sẻ String bất biến.
  • Nguyên lý một câu: ghi vào bộ đệm trong suốt quá trình, tạo String đúng một lần ở cuối — chi phí từ bình phương về tuyến tính.

  • Khai báo capacity khi ước lượng được kích thước giúp tránh các lần cấp phát lại bộ đệm — cùng bản chất với mảng động sẽ gặp ở ArrayList (Ngày 32).

  • StringBuffer là phiên bản đồng bộ hóa từ thời Java cũ — thực tế hiện đại hầu như luôn chọn StringBuilder: xây chuỗi trong một luồng, chia sẻ kết quả bất biến (tinh thần Ngày 20).

String pool — vì sao == đôi khi "tình cờ đúng"

// STRING POOL: JVM lưu trữ chung các chuỗi literal — tiết kiệm bộ nhớ:
String a = "java";              // vào pool
String b = "java";              // TÁI SỬ DỤNG đúng đối tượng trong pool
a == b;                         // true — cùng một đối tượng!

String c = new String("java");  // ép tạo đối tượng MỚI ngoài pool
a == c;                         // false — hai đối tượng khác nhau
a.equals(c);                    // true  — nội dung như nhau

// Bài học không đổi từ Ngày 09: so sánh nội dung LUÔN dùng equals —
// == "tình cờ đúng" với literal là cái bẫy, không phải quy tắc.

// c.intern() trả về bản trong pool — chi tiết về pool & intern: Ngày 77.
  • Pool tồn tại được nhờ tính bất biến của String: nhiều tham chiếu chia sẻ một đối tượng an toàn tuyệt đối vì không ai thay đổi được nội dung — mắt xích nối Ngày 09 với Ngày 20.

  • Hiện tượng a == b đúng với hai literal là chi tiết cài đặt, không phải hợp đồng — mọi so sánh nội dung vẫn bắt buộc equals; phân tích sâu pool và intern() hẹn Ngày 77.

Máy đánh chữ cổ điển — StringBuilder là trang giấy đang gõ dở: thêm ký tự tự do, chỉ đóng thành bản in cuối cùng một lần

String.format & text block

// String.format / formatted — dựng chuỗi theo mẫu thay vì nối từng khúc:
String bc = String.format("%s: %,.0f VND (%.1f%%)", ten, luong, tyLe);
String bc2 = "Ngày %d/%d".formatted(ngay, thang);     // gọi thẳng trên chuỗi

// %s chuỗi · %d số nguyên · %f số thực · %,.0f có phân tách nghìn · %n xuống dòng

// TEXT BLOCK (chuẩn từ Java 15) — chuỗi nhiều dòng không cần \n và escape:
String json = """
        {
          "ten": "%s",
          "luong": %.0f
        }
        """.formatted(ten, luong);

// - Mở bằng ba nháy kép + XUỐNG DÒNG; đóng quyết định mức thụt lề chung
// - Không cần escape nháy kép bên trong — JSON/SQL/HTML dễ đọc hơn hẳn
String sql = """
        SELECT id, title FROM posts
        WHERE status = 'PUBLISHED'
        ORDER BY published_at DESC
        """;
  • format/formatted tách khuôn mẫu khỏi dữ liệu — chuỗi phức tạp đọc được như văn bản thay vì chuỗi + chắp nối; bộ %s %d %f đủ dùng cho tuyệt đại đa số nhu cầu.

  • Text block chấm dứt kỷ nguyên escape \"\n cho JSON/SQL/HTML nhúng trong code — vị trí dấu đóng quyết định mức thụt lề được loại bỏ chung.

  • Kết hợp text block + formatted (ví dụ JSON ở trên) là mẫu dựng chuỗi nhiều dòng có tham số gọn nhất Java hiện có.

Bài tập nhỏ

  • Đo thời gian nối 100.000 số bằng + và bằng StringBuilder (System.nanoTime()) — ghi lại chênh lệch trên máy của bạn.

  • Dự đoán rồi kiểm chứng: "java" == "java", new String("java") == "java", và new String("java").intern() == "java".

  • Viết inBangLuong(List<NhanVienDTO>) dùng StringBuilder + String.format tạo bảng thẳng cột — tận dụng record của Ngày 20.

  • Chuyển một câu SQL nhiều dòng đang viết bằng nối + sang text block — so sánh độ dễ đọc hai phiên bản.

Kết luận

Bốn mảnh ghép của xử lý chuỗi hiệu quả đã vào chỗ: hiểu chi phí bình phương của nối + tích lũy, dùng StringBuilder cho vòng lặp, nắm String pool để không bao giờ nhầm == với equals, và format + text block cho chuỗi phức tạp dễ đọc. Ngày 22 tiếp tục Core API với một lớp ranh giới hay gây bất ngờ: wrapper class và autoboxing — vì sao Integer từ 127 xuống -128 lại so sánh == ra kết quả khác nhau. 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 21: JPA & Hibernate tổng quan

Ngày 21 của 99 Ngày Spring, mở đầu giai đoạn Spring Data JPA: impedance mismatch giữa đối tượng và bảng quan hệ, phân vai JPA (đặc tả) — Hibernate (cài đặt) — Spring Data JPA (tầng tiện ích), EntityManager với persistence context, dirty checking, first-level cache, và lựa chọn ddl-auto đúng cho từng môi trường.

17 thg 8, 20267 phút4
99 Ngày Java — Ngày 20: Record & lớp bất biến

Ngày 20 của 99 Ngày Java: record — lớp dữ liệu trong một dòng khai báo với equals/hashCode/toString tự sinh, compact constructor cho validation, so sánh với Lombok và JavaBean, value semantics cùng record pattern của Java 21 — và ranh giới sử dụng: DTO dùng record, entity dùng class.

16 thg 8, 20266 phút12
99 Ngày Spring — Ngày 20: CORS

Ngày 20 của 99 Ngày Spring: same-origin policy — luật của trình duyệt, preflight OPTIONS và vì sao một lần gọi sinh hai request, cấu hình @CrossOrigin và global theo profile, cùng các quy tắc an toàn: không mở * ở production, đọc lỗi CORS ở DevTools, và cái bẫy khi thêm Spring Security.

16 thg 8, 20266 phút16