Bài 1 đã giới thiệu String như kiểu reference quen dùng nhất. Bài này đi sâu hơn vào 3 công cụ text Java có: khi nào dùng String thuần, khi nào cần Text Block cho chuỗi nhiều dòng, và khi nào bắt buộc phải dùng StringBuilder để tránh bẫy hiệu năng trong vòng lặp.
1. Analogy — Máy đánh chữ và bảng viết tay
Hãy nghĩ về 2 công cụ viết:
- Máy đánh chữ (
String) — gõ xong thì in ra giấy. Muốn sửa? Phải lấy tờ giấy mới, đánh lại từ đầu. Mỗi lần "cộng thêm" ký tự là tạo ra tờ giấy mới hoàn toàn. - Bảng viết tay (
StringBuilder) — viết rồi xoá được, ghi thêm vào cuối không cần bắt đầu lại. Khi xong việc mới "in ra" thànhString. - Tờ giấy in sẵn đa dòng (Text Block
"""...""") — copy nguyên văn bản có format từ template, giữ nguyên xuống dòng, thụt đầu dòng, không cần escape.
2. Ba công cụ text trong Java
| Công cụ | Mutable? | Multi-line? | Dùng khi |
|---|---|---|---|
String | ❌ Immutable | Phải escape \n | Hầu hết trường hợp |
StringBuilder | ✅ Mutable | Append tuỳ ý | Concat trong loop, build dynamic string |
Text Block """ | ❌ Immutable | ✅ Native | JSON, SQL, HTML, regex nhiều dòng nhúng vào code |
3. String — immutable, mọi thao tác tạo object mới
String trong Java là immutable: một khi object String được tạo ra, nội dung không thể thay đổi. Mọi method như .toUpperCase(), .substring(), .replace() đều trả về object String mới.
String s = "hello";
String upper = s.toUpperCase(); // tao String moi "HELLO"
System.out.println(s); // "hello" -- s khong doi
System.out.println(upper); // "HELLO"
String s2 = s + " world"; // tao String moi "hello world"
System.out.println(s); // "hello" -- s van nguyen
Hệ quả quan trọng: cộng chuỗi trong loop tạo object mới mỗi iteration — phần lý do dùng StringBuilder được giải thích ở mục 6.
4. Text Block — chuỗi nhiều dòng giữ nguyên format
Text Block (Java 15+, JEP 378) là cú pháp khai báo String nhiều dòng mà không cần escape \n hay \".
4.1 Cú pháp
String json = """
{
"name": "An",
"age": 25,
"active": true
}
""";
Quy tắc:
- Mở bằng
"""rồi xuống dòng ngay (không được có nội dung trên dòng mở""") - Đóng bằng
"""trên dòng riêng - Indentation tự động strip: Java lấy indent của dấu
"""đóng làm mốc, bỏ phần whitespace chung ở đầu mỗi dòng
4.2 Auto-strip indentation
String sql = """
SELECT *
FROM users
WHERE active = true
""";
// Ket qua: "SELECT *\nFROM users\nWHERE active = true\n"
// (khong co 8 khoang trang dau -- bi strip)
Vị trí """ đóng xác định baseline indent. Dịch """ đóng sang trái để bỏ ít hơn; sang phải để giữ nhiều hơn.
4.3 Không cần escape quote đơn bên trong
// Text Block -- khong can escape " don
String html = """
<a href="https://example.com">Click here</a>
""";
// String thuong -- phai escape moi dau "
String htmlOld = "<a href=\"https://example.com\">Click here</a>";
4.4 Use case phù hợp nhất
// SQL query nhung vao code
String query = """
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.active = true
AND o.total > 100
ORDER BY o.total DESC
""";
// JSON template
String body = """
{
"event": "login",
"userId": %d,
"timestamp": "%s"
}
""".formatted(userId, timestamp);
// HTML snippet
String card = """
<div class="card">
<h2>%s</h2>
<p>%s</p>
</div>
""".formatted(title, content);
💡 💡 Cách nhớ — Text Block
Text Block như tờ giấy có in sẵn template: copy nguyên nội dung nhiều dòng, giữ format, không cần gõ lại \n hay \" tẻ nhạt. Mỗi lần đọc lại code thấy ngay hình dạng thực sự của chuỗi.
5. String formatting — 3 cách
Java có 3 cách format chuỗi với giá trị động, cùng chung hệ format specifier:
String ten = "An";
int tuoi = 25;
double diem = 8.567;
// Cach 1: String.format -- classic, dung moi noi
String s1 = String.format("Xin chao %s, %d tuoi, diem %.2f", ten, tuoi, diem);
// Cach 2: .formatted() -- Java 15+, viet gan voi template hon
String s2 = "Xin chao %s, %d tuoi, diem %.2f".formatted(ten, tuoi, diem);
// Cach 3: printf -- in thang ra console, khong can bien trung gian
System.out.printf("Xin chao %s, %d tuoi, diem %.2f%n", ten, tuoi, diem);
5.1 Format specifiers quan trọng
| Specifier | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
%s | String (hoặc .toString()) | "%s".formatted("hi") | "hi" |
%d | Số nguyên (int, long) | "%d".formatted(42) | "42" |
%f | Số thực (float, double) | "%f".formatted(3.14) | "3.140000" |
%.2f | Số thực, 2 chữ số thập phân | "%.2f".formatted(3.14159) | "3.14" |
%10d | Số nguyên, right-align, rộng 10 | "%10d".formatted(42) | " 42" |
%-10s | String, left-align, rộng 10 | "%-10s".formatted("hi") | "hi " |
%05d | Số nguyên, zero-padded rộng 5 | "%05d".formatted(42) | "00042" |
%n | Newline (platform-independent) | "line1%nline2" | "line1\nline2" |
%% | Ký tự % literal | "100%%" | "100%" |
⚠️ Pitfall — locale và số thập phân
%f dùng Locale mặc định của JVM. Ở một số hệ thống châu Âu, dấu phân cách là dấu phẩy , thay vì .. Nếu cần output nhất quán (ví dụ ghi file CSV), dùng String.format(Locale.US, "%.2f", value) để chỉ định locale rõ ràng.
5.2 Dùng .formatted() với Text Block
.formatted() gọi được trực tiếp trên Text Block — kết hợp rất tự nhiên:
String report = """
=== Bao cao ===
Ten: %s
Tuoi: %d
Diem: %.2f
""".formatted(ten, tuoi, diem);
System.out.print(report);
6. StringBuilder — vì sao cần và khi nào dùng
6.1 String concat trong loop: bẫy O(n²)
// SAI -- O(n^2), cuc cham khi n lon
String result = "";
for (int i = 0; i < 10_000; i++) {
result += "x"; // moi iteration: copy toan bo result + "x" -> String moi
}
// Iteration 1: copy 0 ky tu + "x" -> 1 ky tu
// Iteration 2: copy 1 ky tu + "x" -> 2 ky tu
// Iteration 3: copy 2 ky tu + "x" -> 3 ky tu
// ...
// Iteration 10000: copy 9999 ky tu + "x" -> 10000 ky tu
// Tong so byte copy: 0+1+2+...+9999 = ~50 trieu byte copy!
Với n=10.000, số byte được copy là n*(n-1)/2 ≈ 50 triệu. Với n=100.000 con số tăng lên ~5 tỷ byte — chậm hàng giây so với millisecond của StringBuilder.
6.2 StringBuilder — buffer mutable, append O(1) amortized
// DUNG -- O(n), nhanh
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append("x"); // chi ghi them vao buffer, khong copy toan bo
}
String result = sb.toString(); // chi tao String 1 lan duy nhat o cuoi
flowchart LR
subgraph Before["append truoc khi day"]
buf1["char[] buffer [x][x][x][ ][ ][ ][ ][ ]"]
sz1["size = 3"]
end
subgraph After["append them 1 ky tu"]
buf2["char[] buffer [x][x][x][x][ ][ ][ ][ ]"]
sz2["size = 4"]
end
Before -->|"O(1) amortized"| AfterStringBuilder giữ một char[] buffer nội bộ. append chỉ ghi thêm vào vị trí size rồi tăng size lên — O(1). Khi buffer đầy, Java cấp phát buffer mới gấp đôi (giống ArrayList) — chi phí amortized vẫn O(1).
6.3 API StringBuilder thường dùng
StringBuilder sb = new StringBuilder();
sb.append("Hello"); // "Hello"
sb.append(", "); // "Hello, "
sb.append("An"); // "Hello, An"
sb.append('!'); // "Hello, An!"
sb.insert(0, ">>> "); // ">>> Hello, An!"
sb.delete(0, 4); // "Hello, An!"
sb.reverse(); // "!nA ,olleH"
sb.replace(0, 3, "Bye"); // "Bye ,olleH" (vi du)
String result = sb.toString(); // lay String cuoi cung
int len = sb.length(); // do dai hien tai
sb.setLength(0); // xoa het, dung lai tu dau (hieu qua hon new)
6.4 Compiler tự dùng StringBuilder — nhưng không trong loop
Ngoài loop, compiler Java đã tự tối ưu concat:
// Viet nhu vay...
String s = "Xin chao, " + ten + "! Tuoi: " + tuoi;
// Compiler Java 8 dich thanh:
// new StringBuilder().append("Xin chao, ").append(ten).append("! Tuoi: ").append(tuoi).toString()
// Java 9+ dung invokedynamic + StringConcatFactory (hieu qua hon nua)
Nhưng trong loop, compiler không tối ưu qua nhiều iteration:
// Moi iteration van la 1 StringBuilder moi -> khong gop lai duoc
String s = "";
for (String item : list) {
s += item; // compiler tao new StringBuilder moi moi lap
}
// -> O(n^2) van xay ra
Bài học thực tế: tự dùng StringBuilder cho concat trong loop.
💡 💡 Cách nhớ
- String (đá ngọc): đẹp, cứng, không sửa được sau khi tạo. Dùng khi giá trị cố định.
- StringBuilder (bảng viết tay): linh hoạt, ghi thêm thoải mái. Dùng khi build chuỗi từng bước trong loop.
- Text Block (tờ giấy in sẵn): giữ nguyên format như tờ giấy template. Dùng cho JSON/SQL/HTML nhúng vào code.
7. String methods "must know"
Không cần nhớ hết — chỉ cần biết những cái dùng hàng ngày:
| Method | Ý nghĩa | Ví dụ |
|---|---|---|
.length() | Số ký tự | "hello".length() → 5 |
.charAt(i) | Ký tự tại index i | "hello".charAt(1) → 'e' |
.isEmpty() | Rỗng (length == 0)? | "".isEmpty() → true |
.isBlank() | Rỗng hoặc chỉ whitespace? (Java 11+) | " ".isBlank() → true |
.substring(i, j) | Cắt từ i đến j (không tính j) | "hello".substring(1, 4) → "ell" |
.indexOf(x) | Vị trí đầu tiên của x, -1 nếu không có | "hello".indexOf('l') → 2 |
.contains(x) | Có chứa chuỗi con x? | "hello".contains("ell") → true |
.toLowerCase() | Chuyển thường | "HELLO".toLowerCase() → "hello" |
.toUpperCase() | Chuyển hoa | "hello".toUpperCase() → "HELLO" |
.trim() | Bỏ whitespace ASCII đầu cuối | " hi ".trim() → "hi" |
.strip() | Bỏ whitespace Unicode đầu cuối (Java 11+) | " hi ".strip() → "hi" |
.split(regex) | Tách thành mảng | "a,b,c".split(",") → ["a","b","c"] |
String.join(sep, parts) | Nối mảng/list | String.join(", ", "a","b","c") → "a, b, c" |
.equals(x) | So sánh nội dung | "hi".equals("hi") → true |
.equalsIgnoreCase(x) | So sánh không phân biệt hoa thường | "Hi".equalsIgnoreCase("hi") → true |
.startsWith(x) | Bắt đầu bằng x? | "hello".startsWith("he") → true |
.endsWith(x) | Kết thúc bằng x? | "hello".endsWith("lo") → true |
.replace(old, new) | Thay thế tất cả | "aabbcc".replace("bb", "XX") → "aaXXcc" |
.lines() | Stream các dòng (Java 11+) | dùng với Text Block |
⚠️ Pitfall — trim() vs strip()
.trim() chỉ xử lý whitespace ASCII (code point ≤ 32). .strip() (Java 11+) xử lý đúng Unicode whitespace (full-width space, non-breaking space...). Với text tiếng Việt hoặc quốc tế, ưu tiên dùng .strip().
8. StringBuffer vs StringBuilder — khi nào cần cái nào
StringBuilder | StringBuffer | |
|---|---|---|
| Thread-safe? | ❌ Không | ✅ Có (synchronized) |
| Hiệu năng | ✅ Nhanh hơn | Chậm hơn do sync overhead |
| Java version | Java 1.5+ | Java 1.0+ |
| Dùng khi | Single-thread (99% trường hợp) | Shared mutable string giữa nhiều thread không có lock bên ngoài |
Trong thực tế hiện đại:
- ✅ Dùng
StringBuildercho hầu hết mọi thứ - ❌ Hiếm khi cần
StringBuffer— nếu đang share string giữa threads, thường có design tốt hơn (nhưConcurrentLinkedQueuehoặc immutable message passing)
9. ✅/❌ Khi nào dùng gì?
| Tình huống | ✅ Dùng | ❌ Tránh |
|---|---|---|
| Chuỗi cố định, không loop | String + + | StringBuilder (overkill) |
| Concat trong loop (n lần) | StringBuilder.append() | String += (O(n²)) |
| JSON/SQL/HTML nhiều dòng nhúng code | Text Block """ | "line1\nline2\n..." (khó đọc) |
| Format có giá trị động (2-3 biến) | String.format() hoặc .formatted() | StringBuilder nếu chỉ format |
| In trực tiếp ra console | System.out.printf() | System.out.println(String.format(...)) |
| String concat ngoài loop, ít giá trị | + trực tiếp | StringBuilder (compiler đã tối ưu) |
10. Code example — String concat O(n²) vs StringBuilder
public class StringBuilderDemo {
// SAI: O(n^2) vi moi lan += tao String moi
static String concatSlow(int n) {
String result = "";
for (int i = 0; i < n; i++) {
result += "x";
}
return result;
}
// DUNG: O(n) voi StringBuilder
static String concatFast(int n) {
StringBuilder sb = new StringBuilder(n); // pre-size buffer tranh resize
for (int i = 0; i < n; i++) {
sb.append('x'); // char append nhanh hon String append
}
return sb.toString();
}
public static void main(String[] args) {
int n = 50_000;
long t1 = System.currentTimeMillis();
String slow = concatSlow(n);
long t2 = System.currentTimeMillis();
long t3 = System.currentTimeMillis();
String fast = concatFast(n);
long t4 = System.currentTimeMillis();
System.out.println("slow (n=50000): " + (t2 - t1) + " ms");
System.out.println("fast (n=50000): " + (t4 - t3) + " ms");
// slow co the mat hang tram ms; fast < 5 ms
// Text Block + formatted()
String ten = "An";
int tuoi = 25;
String profile = """
Ho va ten: %s
Tuoi: %d
""".formatted(ten, tuoi);
System.out.print(profile);
// String join
String[] fruits = {"tao", "cam", "chuoi"};
System.out.println(String.join(", ", fruits)); // tao, cam, chuoi
// strip vs trim voi full-width space
String s = " xin chao "; // full-width space (Unicode)
System.out.println("|" + s.trim() + "|"); // trim khong xu ly -- giu nguyen
System.out.println("|" + s.strip() + "|"); // strip xu ly dung -- bo duoc
}
}
11. Pitfall tổng hợp
❌ Concat trong loop: s += item trong vòng lặp dài → O(n²). Dùng StringBuilder.
❌ Text Block indentation: đặt """ đóng trên cùng dòng với content → compile error. """ đóng phải trên dòng riêng.
❌ Locale surprise với %f: String.format("%.2f", 3.14) có thể ra "3,14" trên hệ thống European locale. Dùng String.format(Locale.US, "%.2f", 3.14) nếu cần cố định.
❌ StringBuffer thay StringBuilder không cần thiết: StringBuffer có sync overhead, không cần dùng trong single-thread code.
❌ Quên .strip() cho Unicode whitespace: .trim() không xử lý được full-width space, non-breaking space trong text quốc tế.
12. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec và API chính thức Java 21:
- JLS §3.10.6 — Text Blocks: quy tắc syntax text block, indentation stripping algorithm.
- JEP 378 — Text Blocks: rationale và thiết kế của Text Block, các trường hợp có thể nhầm.
- String API — Java 21: toàn bộ method, bao gồm
strip(),isBlank(),lines(),formatted()(Java 15+). - StringBuilder API — Java 21:
append,insert,delete,reverse,setLength.
Diễn giải đơn giản: JEP 378 giải thích vì sao triple-quote được chọn thay vì syntax khác — tránh clash với heredoc của ngôn ngữ khác. Indentation stripping algorithm dùng "common white space prefix" là cột của """ đóng. StringBuilder là không thread-safe theo spec (ngược lại StringBuffer); JVM có thể thay + ngoài loop bằng invokedynamic từ Java 9.
13. Tóm tắt
Stringlà immutable: mọi thao tác trả về object mới. An toàn, dùng mặc định.- Text Block
"""...""": chuỗi nhiều dòng giữ format, tự strip indent, không cần escape"đơn. Dùng cho JSON/SQL/HTML nhúng vào code. String.format()/.formatted()/printf(): cùng format specifier, 3 cách viết khác nhau.+=trong loop gây O(n²) vì tạo String mới mỗi iteration. DùngStringBuilderthay thế.StringBuilder: buffer mutable,appendO(1) amortized,toString()lấy kết quả cuối.- Compiler tự tối ưu concat ngoài loop — không cần tự viết
StringBuildercho concat đơn giản.
14. Tự kiểm tra
- Vì sao
s += "x"trong loop với n=10.000 lần chậm hơnStringBuilder.append("x")hàng chục lần? - Đoạn Text Block sau có indentation bị strip không? Kết quả ra sao?
String s = """ hello world """; String.format("%.2f", 3.14159)cho kết quả gì?%10dvới giá trị42ra sao?- Vì sao không nên dùng
StringBufferthayStringBuildertrong đoạn code single-thread? .trim()vs.strip(): khác nhau ở điểm gì? Khi nào cần.strip()?- Compiler có tự tối ưu
a + b + cthànhStringBuilderkhông? Còn trong loop thì sao?
Bài tiếp theo: Hằng số, final và enum — giá trị không thay đổi