Phân tích sâu về char, String, StringBuilder và StringBuffer trong Java

Trong lập trình Java, char, String, StringBuilderStringBuffer là những thành phần cốt lõi để xử lý dữ liệu văn bản. Việc hiểu rõ bản chất, cơ chế hoạt động và hiệu năng của từng loại giúp tối ưu hóa mã nguồn cả về tốc độ lẫn tài nguyên.

1. Kiểu dữ liệu nguyên thủy: char

char là kiểu dữ liệu nguyên thủy 16-bit, đại diện cho một ký tự Unicode duy nhất. Giá trị hợp lệ nằm trong khoảng từ \u0000 đến \uffff (tức 0–65535).

Từ JDK 9, các lớp như String, StringBuilder không còn dùng mảng char[] nữa mà chuyển sang byte[] kết hợp với trường coder để xác định mã hóa: LATIN1 (1 byte/ký tự) hoặc UTF16 (2 byte/ký tự). Điều này giúp tiết kiệm bộ nhớ khi xử lý chuỗi chủ yếu chứa ký tự ASCII.

Có nhiều cách khởi tạo giá trị cho char:

  • Ký tự trực tiếp: 'A', '中'
  • Mã số Unicode: char c = 65;
  • Dãy escape Unicode: '\u0041'
  • Ký tự đặc biệt: '\n', '\\'

Vì lưu trữ dưới dạng số nguyên, char có thể tham gia vào phép toán số học. Tuy nhiên, kết quả sẽ được nâng lên kiểu int, nên cần ép kiểu tường minh khi gán lại:

char letter = 'A';
letter = (char)(letter + 1); // → 'B'

2. Chuỗi bất biến: String

Lớp String được thiết kế để **bất biến** (immutable), nhờ vào:

  • Từ khóa final trên lớp → không thể kế thừa
  • Mảng lưu trữ nội bộ (value) là private final → không thể thay đổi tham chiếu hay nội dung từ bên ngoài

Tính bất biến mang lại nhiều lợi ích:

  • An toàn đa luồng: Không cần đồng bộ vì giá trị không thay đổi
  • String Pool: Các chuỗi literal giống nhau dùng chung một đối tượng trong vùng nhớ đặc biệt
  • Cache hashCode: Giá trị băm được tính một lần và lưu lại, tối ưu khi dùng làm key trong HashMap

Hai cách tạo String:

  • String s = "abc"; → tìm trong String Pool trước
  • String s = new String("abc"); → luôn tạo mới trên heap (có thể kèm 1 đối tượng trong pool)

Mọi thao tác "sửa đổi" (nối, thay thế, cắt...) đều trả về một đối tượng String mới. Do đó, việc nối chuỗi trong vòng lặp bằng toán tử + rất kém hiệu quả:

// Không nên
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i; // Mỗi lần tạo StringBuilder mới + toString()
}

3. Chuỗi khả biến: StringBuilder và StringBuffer

Cả hai lớp này đều kế thừa từ AbstractStringBuilder, sử dụng mảng byte[] (JDK 9+) có thể mở rộng động. Khi vượt quá dung lượng, mảng mới được cấp phát với kích thước ≈ old * 2 + 2.

Đặc điểmStringBuilderStringBuffer
Phiên bản ra đờiJDK 1.5JDK 1.0
Đa luồngKhông an toànAn toàn (phương thức có synchronized)
Hiệu năngCao hơnThấp hơn do overhead đồng bộ
Ứng dụngXử lý chuỗi trong đơn luồngChia sẻ buffer giữa nhiều luồng

Các phương thức phổ biến: append(), insert(), delete(), reverse(), toString()

So sánh hiệu năng (20.000 lần nối chuỗi):

  • String + : ~1500 ms
  • StringBuffer: ~3 ms
  • StringBuilder: ~1 ms

4. Hướng dẫn lựa chọn

  • Ký tự đơn: Dùng char
  • Chuỗi cố định, ít thay đổi: Dùng String (cấu hình, hằng số, key map)
  • Xử lý chuỗi phức tạp trong đơn luồng: Ưu tiên StringBuilder
  • Nhiều luồng cùng sửa một buffer: Dùng StringBuffer

5. Một số câu hỏi phỏng vấn thường gặp

Câu 1: String s = new String("xyz"); tạo bao nhiêu đối tượng?
→ Nếu "xyz" chưa có trong String Pool: 2 đối tượng (1 trong pool, 1 trên heap). Ngược lại: chỉ 1 trên heap.

Câu 2: Tại sao nên dùng StringBuilder thay vì +?
→ Trong vòng lặp, mỗi lần + tạo một StringBuilder mới → nhiều đối tượng trung gian → GC hoạt động mạnh. Dùng chung một StringBuilder giúp tránh điều này.

Câu 3: Sự khác biệt chính giữa ba lớp chuỗi?
String: bất biến, an toàn đa luồng.
StringBuilder: khả biến, nhanh, không an toàn đa luồng.
StringBuffer: khả biến, chậm hơn do đồng bộ, an toàn đa luồng.

Thẻ: Java string StringBuilder StringBuffer char

Đăng vào ngày 29 tháng 5 lúc 20:01