String và StringBuilder¶
1. Khái niệm¶
String trong Java là một chuỗi ký tự bất biến (immutable). Một khi đã tạo, nội dung bên trong không thể thay đổi — mọi thao tác "chỉnh sửa" đều tạo ra object mới.
StringBuilder là phiên bản mutable (có thể sửa nội dung tại chỗ) — dùng khi cần xây dựng chuỗi trong vòng lặp hoặc ghép nhiều phần.
2. Tại sao quan trọng¶
String xuất hiện ở khắp nơi trong mọi ứng dụng thực tế: tên người dùng, URL, JSON, log message, câu truy vấn SQL... Hiểu đúng String giúp:
- Tránh bug so sánh
==thay vì.equals()— lỗi kinh điển nhất - Tránh tạo hàng nghìn object thừa khi ghép chuỗi trong vòng lặp
- Hiểu tại sao String an toàn khi dùng làm key trong
HashMap - Giải thích được câu hỏi phỏng vấn về immutability và String Pool
3. String literal và String Pool¶
Java có một vùng nhớ đặc biệt trong Heap gọi là String Pool — nơi lưu trữ các String literal để tái sử dụng.
String s1 = "Hello"; // tạo "Hello" trong pool
String s2 = "Hello"; // tái sử dụng object đã có
String s3 = new String("Hello"); // bắt buộc tạo object mới ngoài pool
System.out.println(s1 == s2); // true — cùng địa chỉ (cùng object trong pool)
System.out.println(s1 == s3); // false — s3 là object khác ngoài pool
System.out.println(s1.equals(s3)); // true — cùng nội dung
Luôn dùng .equals() để so sánh nội dung String
== so sánh địa chỉ bộ nhớ, không phải nội dung. Chỉ dùng == khi bạn cố ý kiểm tra xem hai biến có trỏ vào cùng một object hay không — rất hiếm gặp trong thực tế.
4. String là immutable¶
Immutable có nghĩa là sau khi tạo, object không thể bị thay đổi. Mọi method "sửa" String đều trả về object mới.
String s = "hello";
s.toUpperCase(); // ❌ không làm gì với s — kết quả bị bỏ qua
System.out.println(s); // hello — s vẫn không đổi
String upper = s.toUpperCase(); // ✅ lưu kết quả vào biến mới
System.out.println(upper); // HELLO
Tại sao Java thiết kế String immutable?¶
- An toàn cho HashMap/HashSet — hash code tính một lần và không đổi
- Thread-safe — nhiều thread có thể dùng chung String mà không cần đồng bộ
- Bảo mật — password, path, tên class không thể bị thay đổi sau khi kiểm tra
- String Pool — chỉ khả thi khi String không thể bị thay đổi sau khi chia sẻ
5. Các method phổ biến¶
String s = "Hello, Java World";
s.length() // 17
s.charAt(7) // 'J'
s.indexOf('o') // 4 — lần xuất hiện đầu tiên
s.lastIndexOf('o') // 14 — lần xuất hiện cuối
s.indexOf("Java") // 7
s.contains("Java") // true
s.startsWith("Hello") // true
s.endsWith("World") // true
s.isEmpty() // false (length > 0)
s.isBlank() // false (Java 11 — kiểm tra cả whitespace-only)
String s = " Hello, Java! ";
s.toLowerCase() // " hello, java! "
s.toUpperCase() // " HELLO, JAVA! "
s.trim() // "Hello, Java!" — bỏ space đầu/cuối (ASCII)
s.strip() // "Hello, Java!" — (Java 11) Unicode-aware, dùng strip thay trim
s.stripLeading() // "Hello, Java! "
s.stripTrailing() // " Hello, Java!"
s.replace('l', 'r') // " Herro, Java! "
s.replace("Java", "World")// " Hello, World! "
s.replaceAll("\\s+", "_") // dùng regex — thay mọi chuỗi whitespace bằng _
"ha".repeat(3) // "hahaha" — Java 11+
String s = "Hello, Java World";
s.substring(7) // "Java World" — từ index 7 đến hết
s.substring(7, 11) // "Java" — [7, 11)
String csv = "a,b,c,d";
String[] parts = csv.split(","); // ["a", "b", "c", "d"]
String[] two = csv.split(",", 2); // ["a", "b,c,d"] — tối đa 2 phần
split() nhận regex, không phải plain text
"1.2.3".split(".") trả về [] rỗng vì . trong regex nghĩa là "bất kỳ ký tự nào".
Dùng split("\\.") để tách theo dấu chấm thật sự.
// String → char array và ngược lại
char[] chars = "Hello".toCharArray(); // ['H','e','l','l','o']
String back = new String(chars); // "Hello"
// Primitive → String
String n = String.valueOf(42); // "42"
String d = String.valueOf(3.14); // "3.14"
// String → primitive
int i = Integer.parseInt("42"); // 42
double x = Double.parseDouble("3.14"); // 3.14
// Định dạng chuỗi
String msg = String.format("Xin chào %s, bạn %d tuổi", "An", 25);
// "Xin chào An, bạn 25 tuổi"
// Text block — Java 15+ (chuỗi nhiều dòng)
String json = """
{
"name": "An",
"age": 25
}
""";
// Nối nhiều chuỗi với dấu phân cách
String joined = String.join(", ", "An", "Bình", "Chi"); // "An, Bình, Chi"
6. Ghép chuỗi và hiệu năng¶
Vấn đề với + trong vòng lặp¶
// ❌ O(n²) — mỗi lần + tạo một String mới
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i; // tạo 10.000 object String trung gian
}
// ✅ O(n) — StringBuilder sửa nội dung tại chỗ
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i);
}
String result = sb.toString();
Compiler tối ưu + trong các trường hợp đơn giản
Với biểu thức một dòng "Hello " + name + "!", compiler Java tự động dùng StringBuilder bên dưới. Nhưng khi + nằm bên trong vòng lặp, compiler không tối ưu được — bạn phải tự dùng StringBuilder.
7. StringBuilder¶
StringBuilder cho phép sửa chuỗi tại chỗ (không tạo object mới mỗi lần).
StringBuilder sb = new StringBuilder();
sb.append("Hello"); // "Hello"
sb.append(", ").append("Java"); // "Hello, Java" — chaining
sb.insert(5, " World"); // "Hello World, Java"
sb.delete(5, 11); // "Hello, Java"
sb.replace(7, 11, "World"); // "Hello, World"
sb.reverse(); // "dlroW ,olleH"
sb.reverse(); // "Hello, World" — back to original
System.out.println(sb.length()); // 12
System.out.println(sb.charAt(0)); // 'H'
System.out.println(sb.toString()); // "Hello, World"
StringBuilder vs StringBuffer¶
StringBuilder |
StringBuffer |
|
|---|---|---|
| Thread-safe | Không | Có (synchronized) |
| Tốc độ | Nhanh hơn | Chậm hơn |
| Khi dùng | Single-thread (99% trường hợp) | Multi-thread cần modify chuỗi |
Trong thực tế hiếm khi cần
StringBuffer. DùngStringBuildermặc định — nếu cần multi-thread thì thường có cách tốt hơn như tổng hợp kết quả sau rồi mới nối.
8. Code ví dụ¶
Verified
Bản đầy đủ có thể compile: StringDemo.java
replaceAll("[^a-z0-9]", "")xóa mọi ký tự không phải chữ cái hoặc chữ số — regex pattern cơ bản cần nhớ.- Dùng
StringBuilderthay vì+trong vòng lặp — tránh tạo O(n) String object trung gian. indexOf(word, fromIndex)— overload với tham số thứ hai chỉ định vị trí bắt đầu tìm, tránh tìm lại từ đầu mỗi lần.
9. Lỗi thường gặp¶
Lỗi 1 — So sánh String bằng ==¶
String a = new String("Java");
String b = new String("Java");
if (a == b) { ... } // ❌ false — so sánh địa chỉ
if (a.equals(b)) { ... } // ✅ true — so sánh nội dung
// Tránh NullPointerException khi a có thể null
if ("Java".equals(a)) { ... } // ✅ đặt literal ở trước
Lỗi 2 — Bỏ qua kết quả trả về của String method¶
String s = " hello ";
s.trim(); // ❌ kết quả bị bỏ qua, s vẫn như cũ
System.out.println(s); // " hello "
s = s.trim(); // ✅ gán lại
System.out.println(s); // "hello"
Lỗi 3 — Ghép chuỗi trong vòng lặp bằng +¶
// ❌ Tạo 10.000 String object trung gian
String result = "";
for (String word : words) result += word + " ";
// ✅ StringBuilder
StringBuilder sb = new StringBuilder();
for (String word : words) sb.append(word).append(' ');
String result = sb.toString();
Lỗi 4 — StringIndexOutOfBoundsException với substring¶
String s = "Hello"; // length = 5
s.substring(3, 10); // ❌ end index 10 > length 5
s.substring(3, s.length()); // ✅ an toàn
s.substring(3); // ✅ tương đương, gọn hơn
Lỗi 5 — NullPointerException khi String là null¶
String name = null;
name.equals("Admin"); // ❌ NullPointerException
name.length(); // ❌ NullPointerException
"Admin".equals(name); // ✅ false — an toàn, literal không null
Objects.equals(name, "Admin"); // ✅ false — null-safe (Java 7+)
10. Câu hỏi phỏng vấn¶
Q1: Tại sao String trong Java là immutable?
Ba lý do chính: (1) String Pool — chỉ có thể chia sẻ object khi chắc chắn nó không bị thay đổi; (2) Thread safety — immutable object tự nhiên thread-safe, không cần đồng bộ; (3) Security — class name, database URL, password không thể bị thay đổi sau khi xác thực.
Q2: Sự khác nhau giữa String, StringBuilder, và StringBuffer?
Stringimmutable, thay đổi luôn tạo object mới.StringBuildermutable, thay đổi tại chỗ, không thread-safe, nhanh hơn.StringBuffergiốngStringBuildernhưng các method đượcsynchronized— thread-safe nhưng chậm hơn. Trong thực tế: dùngStringcho giá trị cố định,StringBuilderkhi cần build/modify chuỗi,StringBufferrất hiếm.
Q3: String Pool là gì? intern() làm gì?
String Pool là một cache trong Heap — JVM tái sử dụng String literal thay vì tạo object mới mỗi lần.
intern()đưa một String (thường được tạo bằngnew) vào pool và trả về reference trong pool. Ít dùng trực tiếp trong code hiện đại.
Q4: Tại sao + trong vòng lặp lớn là vấn đề hiệu năng?
Mỗi phép
+tạo một String mới vì String immutable. Với n lần ghép, Java tạo các String có độ dài 1, 2, 3... n — tổng ký tự sao chép là 1+2+...+n = O(n²).StringBuilderduy trì một buffer có thể mở rộng, sao chép amortized O(1) mỗi lầnappend— tổng O(n).
Q5: equals() và equalsIgnoreCase() khác nhau thế nào? Khi nào dùng compareTo()?
equals()so sánh chính xác từng ký tự, phân biệt hoa/thường.equalsIgnoreCase()bỏ qua hoa/thường.compareTo()trả về số âm/0/dương theo thứ tự từ điển Unicode — dùng khi cần sắp xếp hoặc so sánh thứ tự (ví dụ:Arrays.sort()với custom comparator), không phải khi chỉ kiểm tra bằng nhau.
11. Tài liệu tham khảo¶
| Tài liệu | Nội dung |
|---|---|
| JLS §4.3.3 — The String Type | Đặc tả chính thức |
| java.lang.String Javadoc | API reference đầy đủ |
| java.lang.StringBuilder Javadoc | API reference |
| Oracle — Text Blocks | Text Block (Java 15+) |
| Effective Java — Joshua Bloch | Item 63: Beware the performance of string concatenation |