Trong lập trình Java, char, String, StringBuilder và StringBuffer 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
finaltrê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ướcString 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ểm | StringBuilder | StringBuffer |
|---|---|---|
| Phiên bản ra đời | JDK 1.5 | JDK 1.0 |
| Đa luồng | Không an toàn | An toàn (phương thức có synchronized) |
| Hiệu năng | Cao hơn | Thấp hơn do overhead đồng bộ |
| Ứng dụng | Xử lý chuỗi trong đơn luồng | Chia 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 msStringBuffer: ~3 msStringBuilder: ~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.