String Pool, intern và concatenation
String Pool cho phép các chuỗi bằng nhau dùng chung instance khi phù hợp. Nhưng logical equality vẫn phải dùng equals; không dựa vào pooling để so sánh bằng ==.
intern(), giới hạn HotSpot và bằng chứng thực nghiệm. Đừng dùng một kết quả benchmark để thay thế quy tắc Java.1. Literal và pool
Ví dụ nguồn · đặt trong main · hai biến, cùng canonical identity.
String a = "java";
String b = "java";
System.out.println(a == b); // true: cùng pooled instance
System.out.println(a.equals(b)); // true: cùng nội dung
String literals và constant string expressions được intern. JVM dùng canonical representation cho nội dung đó, thay vì tạo một object mới cho mọi lần gặp cùng literal. Cần phân biệt việc dẫn xuất string constant với thời điểm loading/linking: không mặc định mọi literal đều đã được tạo ngay khi class bắt đầu được load.
Bổ sung · đã đối chiếu tài liệu
Đọc theo reference và value, không đọc theo tên biến
a và b là hai biến reference; cả hai trỏ tới cùng một String object biểu diễn "java". == kiểm identity, equals kiểm nội dung. Literal vẫn là object kiểu String, không phải một vùng ký tự nằm ngoài hệ thống object. [S1] [S2]
Guarantee: literal cùng nội dung và giá trị String của constant expression được intern. Không suy rộng thành “mọi String bằng nội dung đều cùng object”: dữ liệu đọc từ file, HTTP hoặc được xây lúc chạy không có contract tự động canonicalize chỉ vì nội dung giống literal. [S2] [S3]
.class/run-time constant pool của mỗi class không đồng nghĩa String intern pool. Một string constant ở runtime là reference tới String object; nhiều class có thể dùng cùng canonical String. Thời điểm tạo/dẫn xuất reference không phải quy tắc đếm object theo từng dòng source. [S9] [S10]2. new String khác gì?
Ví dụ nguồn · đặt trong main · khác identity nhưng cùng nội dung.
String pooled = "java";
String explicit = new String("java");
System.out.println(pooled == explicit); // false
System.out.println(pooled.equals(explicit)); // true
Literal "java" vẫn có pooled instance; new String(...) yêu cầu tạo thêm object String riêng. Vì vậy cách viết này thường lãng phí nếu không cố ý cần identity khác—một nhu cầu rất hiếm.
Bổ sung · đã đối chiếu tài liệu
Đếm object có điều kiện, không học thuộc “luôn tạo hai”
| Dòng đang xét theo thứ tự | Reference / identity sau dòng đó | Điều cần phân biệt |
|---|---|---|
String a = "java"; | a giữ canonical reference. | Canonical có thể đã tồn tại trước đó; không kết luận luôn vừa tạo một object. |
String b = "java"; | a và b cùng identity. | Thêm biến không đồng nghĩa thêm String object. |
String c = new String("java"); | c khác identity với a/b, nhưng equals là true. | new yêu cầu một String riêng trong semantics của chương trình. |
String d = c.intern(); | d cùng identity với a/b. | c không tự được gán lại; caller cần dùng giá trị trả về. |
Bảng giả định các dòng chạy tuần tự và hoàn thành bình thường. Ngay sau dòng tạo c, có hai String identities đang được các biến này giữ: canonical và explicit. Không đồng nhất số identity quan sát được với số lần cấp phát vật lý sau tối ưu JIT, hoặc số backing arrays. API/JLS quy định behavior quan sát được, không bắt người học dự đoán layout nội bộ. [S1] [S10]
Tự kiểm: trước khi trả lời “có bao nhiêu object”, ghi rõ pool đã có literal chưa, đang đếm String hay cả buffer, đang xét trạng thái sống hay tổng allocation. Câu trả lời thiếu các giả định này dễ sai.
3. intern()
Ví dụ nguồn · comment đã làm rõ điều kiện pool; xem phân tích hai nhánh ngay bên dưới.
String runtime = new StringBuilder()
.append("ja")
.append("va")
.toString();
String canonical = runtime.intern();
System.out.println(runtime == "java"); // phụ thuộc trạng thái pool trước intern()
System.out.println(canonical == "java"); // true
intern() trả canonical pooled reference có cùng nội dung. Nếu pool đã chứa String bằng theo equals, method trả reference đang có; nếu chưa có, chính object nhận lời gọi được thêm vào pool và trả về. Không gọi intern() đại trà: nó thêm global/shared state, lookup cost và có thể tăng memory pressure với dữ liệu cardinality cao.
Bổ sung · contract chính xác và thí nghiệm có điều kiện
Điểm khó: intern xảy ra trước phép so sánh với literal
Đọc đúng thứ tự của đoạn nguồn: runtime.intern() được gọi trước hai lệnh println. Vì vậy không được kết luận dòng đầu luôn false chỉ vì StringBuilder.toString() đã tạo object mới. Canonicalization có thể chọn chính object mới đó. [S1] [S6]
| Trước runtime.intern() | canonical là gì? | runtime == "java" sau intern |
|---|---|---|
| Pool đã có String "java" khác runtime. | Reference tới object đã có. | false. |
| Pool chưa có String bằng "java". | Chính runtime được thêm vào pool và trả về. | true. |
Ở cả hai nhánh, canonical == "java" là true. Trạng thái pool có thể đã bị ảnh hưởng bởi code khác hoặc việc chuẩn bị literal; đoạn nguồn không tự thiết lập một pool rỗng. Nhánh “chưa có” là giải thích contract, không phải lời hứa lần chạy nào cũng quan sát được. [S1] [S9]
Biến thể có precondition rõ · đặt trong main · đầu ra false rồi true.
String existing = "java"; // Establish canonical identity first.
String built = new StringBuilder().append("ja").append("va").toString();
String canonical = built.intern();
System.out.println(built == existing); // false
System.out.println(canonical == existing); // trueTrong biến thể này, canonical reference đã tồn tại và còn được giữ trước khi tạo built, nên kết quả là xác định. Bỏ qua giá trị trả về của intern() không tự gán lại biến reference của caller. [S1] [S6]
4. Compile-time và runtime concatenation
Ví dụ nguồn · false của runtime concatenation là kết quả xác định trong đoạn này.
String a = "ja" + "va"; // constant expression, compiler fold thành "java"
String part = "ja";
String b = part + "va"; // runtime concatenation
System.out.println(a == "java"); // true
System.out.println(b == "java"); // false: runtime concatenation tạo String mới
System.out.println(b.equals("java")); // true
Một biến final kiểu primitive/String chỉ là constant variable khi được khởi tạo bằng constant expression; khi đó nó có thể tham gia constant expression để compiler fold. Concatenation runtime có thể dùng invokedynamic/StringConcatFactory; không nên thuộc lòng rằng compiler luôn sinh StringBuilder cho mọi biểu thức.
Bổ sung · đã đối chiếu tài liệu
final không đồng nghĩa compile-time constant
Bài tập bổ sung · ConcatProbe.java · bốn biến thể cần phân biệt.
public class ConcatProbe {
static String constant() { return "ja" + "va"; }
static String runtime(String part) { return part + "va"; }
static String finalConstant() {
final String part = "ja";
return part + "va";
}
static String finalRuntime(String value) {
final String part = value;
return part + "va";
}
public static void main(String[] args) {
System.out.println(constant() == "java"); // true
System.out.println(runtime("ja") == "java"); // false
System.out.println(finalConstant() == "java"); // true
System.out.println(finalRuntime("ja") == "java"); // false
}
}Một biến final String lấy giá trị từ parameter hoặc lời gọi method không tự trở thành constant variable. Trái lại, final String part = "ja" thỏa điều kiện để tham gia constant expression. Output của chương trình là true, false, true, false theo đúng thứ tự. [S3] [S4] [S5]
== quan sát được từ false thành true. Điều này không nói rằng mọi API xử lý String đều phải trả instance mới. [S4]Kiểm bytecode trên đúng JDK đang dùng; lưu phiên bản compiler cùng output.
javac --release 21 ConcatProbe.java
java ConcatProbe
javap -c -v ConcatProbe > concat-bytecode.txtExpected observations: tìm literal dùng bởi constant()/finalConstant(); trên javac 21 của lần kiểm chứng này, runtime concat đi qua invokedynamic và bootstrap StringConcatFactory.makeConcatWithConstants. Không chép thuộc số index constant pool hoặc coi một bytecode shape là cam kết cho mọi compiler. StringConcatFactory có từ Java 9; tài liệu tham chiếu ở đây là Java 21. [S8] [S13]
5. Vì sao String immutable?
- Cho phép pooled instances được chia sẻ mà caller không thể sửa nội dung của người khác.
- Hash code và behavior ổn định khi làm key trong hash collections.
- An toàn hơn cho class name, path, URL, SQL fragments và security-sensitive values được truyền giữa APIs—dù immutability không tự chống injection.
- Dễ chia sẻ giữa threads vì state không đổi sau construction.
Immutability không có nghĩa dữ liệu nhạy cảm biến mất sớm khỏi memory. Password/token dài hạn thường không nên giữ trong String nếu API cho phép buffer có thể xóa, nhưng điều đó cũng không tạo guarantee tuyệt đối do copies/runtime.
Bổ sung · đã đối chiếu tài liệu
Immutable content không biến mọi workflow thành thread-safe
Biến String vẫn có thể được gán một reference khác; object cũ không bị sửa nội dung. Ngược lại, final StringBuilder chỉ không cho gán lại reference, không ngăn append. Khi nhiều threads cùng cập nhật một biến chia sẻ bằng đọc–tính–ghi, cần protocol đồng bộ riêng dù mỗi giá trị String là immutable. [S1] [S5] [S15]
char[]. [S12]Tự kiểm: tìm một chỗ dùng String làm key, một chỗ chia sẻ reference giữa threads và một chỗ xử lý secret. Với từng chỗ, nêu điều immutability bảo đảm và phần contract còn phải thiết kế riêng.
6. Pool nằm ở đâu?
Trong HotSpot hiện đại, interned Strings là heap objects; mô tả “String Pool nằm trong PermGen” chỉ đúng với JVM cũ. Từ Java 7, interned strings được quản lý trên heap. Không nên biến chi tiết HotSpot thành guarantee của Java Language Specification.
Bổ sung · đã đối chiếu tài liệu
Ba khái niệm cần giữ tách biệt khi nhìn javap và heap
| Khái niệm | Vai trò | Không nên kết luận |
|---|---|---|
| constant_pool trong .class | Metadata/hằng được mã hóa trong class file; gồm entry mô tả String và UTF-8. | Một dòng #n = String không có nghĩa file .class chứa sẵn một live heap object. |
| Run-time constant pool | Cấu trúc theo class/interface; string constant là reference tới String. | Không phải một String pool riêng để mọi class giữ bản sao nội dung độc lập. |
| Intern pool / canonical Strings | Canonicalization theo nội dung; interned Strings là heap objects trên HotSpot từ JDK 7. | Không đồng nhất vị trí String object với toàn bộ cấu trúc quản lý của VM. |
JVMS mô tả run-time constant pool và cách dẫn xuất string constants; tài liệu JDK 7 xác nhận thay đổi allocation của interned Strings khỏi permanent generation sang phần heap chính. Đây là lịch sử implementation HotSpot, không phải một yêu cầu layout mới của JLS. [S9] [S10] [S11]
Quan sát có giới hạn: dùng javap -v để xem metadata; với một JVM thử nghiệm đang chạy, xem jcmd PID help VM.stringtable trước rồi mới dùng jcmd PID VM.stringtable nếu runtime hỗ trợ. Thay PID bằng process ID thực và kiểm quyền truy cập. Hai công cụ cho hai loại bằng chứng khác nhau, không phải hai góc nhìn của cùng một bảng. [S13] [S14]
7. StringBuilder và StringBuffer
| Loại | Semantics | Dùng khi |
|---|---|---|
| String | Immutable | Value text thông thường, chia sẻ an toàn. |
| StringBuilder | Mutable, không synchronized | Xây chuỗi qua loop/nhiều bước trong một thread. |
| StringBuffer | Mutable, methods synchronized | Legacy/shared buffer đặc biệt; synchronization từng method không tự bảo vệ workflow nhiều bước. |
Với một biểu thức nối đơn giản, ưu tiên code rõ ràng và để compiler/JVM tối ưu. Dùng StringBuilder rõ ràng khi append lặp trong loop hoặc xây output có nhiều nhánh.
Bổ sung · lựa chọn API và evidence
Xây chuỗi cục bộ rồi xuất một snapshot
Ví dụ bổ sung · đặt trong main · mutation của builder không đổi String đã trả về.
StringBuilder builder = new StringBuilder();
for (String item : java.util.List.of("java", "core")) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(item);
}
String result = builder.toString();
builder.append("!");
System.out.println(result); // java, coreOutput là java, core, không có dấu chấm than. StringBuilder.toString() cung cấp String snapshot độc lập với các lần sửa builder tiếp theo. Đề xuất giữ builder cục bộ trong một lần xây output; cân nhắc capacity khi biết kích thước và có bằng chứng allocation phù hợp. [S6]
StringBuffer đồng bộ các method cần thiết, nhưng chuỗi thao tác “kiểm length rồi append” có thể bị thread khác xen giữa nếu caller không bảo vệ toàn bộ workflow. Đừng coi việc thay tên class từ Builder sang Buffer là phép sửa mọi lỗi concurrency. [S7]
8. Các bẫy phỏng vấn
Bẫy nguồn · giữ đủ ba cặp so sánh; expected output: true, true, false.
String x = "hello";
String y = new String("hello").intern();
System.out.println(x == y); // true
String p = "ab";
String q = "a" + "b";
System.out.println(p == q); // true: compile-time constant folding
String r = "a";
String s = r + "b";
System.out.println(p == s); // false: r không phải constant variable
- Dùng
equalscho nội dung;==cho identity. - Giải thích literal,
new Stringvàintern()bằng số object/reference. - Phân biệt constant expression với runtime concatenation.
- Biết pool ở heap trên HotSpot hiện đại, không còn PermGen.
- Không gọi
intern()như optimization mặc định. - Chọn StringBuilder cho repeated mutation, không tối ưu vi mô thiếu benchmark.
Bổ sung · interview evidence
Giữ 6 mục checklist, kiểm chứng bằng 16 điều kiện
| Mục nguồn | Evidence cần có |
|---|---|
| 1. equals và == | Giải thích được value bằng nhau nhưng identity khác ở new String; không dùng pooling làm điều kiện business. |
| 2. Literal / new / intern | Vẽ các reference; ghi precondition về pool; giải thích đúng cả nhánh intern trả receiver. |
| 3. Constant và runtime concat | Dự đoán ConcatProbe; chỉ ra vì sao finalRuntime không phải constant expression. |
| 4. Pool và HotSpot heap | Phân biệt class-file constant pool, run-time constant pool và canonical String objects; nêu mốc JDK 7 đúng phạm vi. |
| 5. Không intern mặc định | Nêu cardinality, mức lặp lại, chi phí và số đo cần có trước khi chấp nhận optimization. |
| 6. Builder và benchmark | Chứng minh snapshot không đổi; mô tả workload repeated mutation và giới hạn đồng bộ của Buffer. |
Bài tập bổ sung · StringIdentityCheck.java · cố ý thiết lập canonical reference trước để có kết quả xác định.
public class StringIdentityCheck {
static int checks;
static void verify(String name, boolean actual, boolean expected) {
if (actual != expected) {
throw new AssertionError(name + ": " + actual);
}
checks++;
}
public static void main(String[] args) {
String pooled = "java"; // Establish a live canonical reference first.
String explicit = new String("java");
String built = new StringBuilder().append("ja").append("va").toString();
String part = "ja";
final String constantPart = "ja";
final String runtimePart = new String("ja");
verify("same literal", pooled == "java", true);
verify("new identity", pooled == explicit, false);
verify("new equality", pooled.equals(explicit), true);
verify("builder identity before intern", built == pooled, false);
verify("intern canonical", built.intern() == pooled, true);
verify("intern did not reassign", built == pooled, false);
verify("new then intern", explicit.intern() == pooled, true);
verify("constant concatenation", ("ja" + "va") == pooled, true);
verify("runtime concatenation", (part + "va") == pooled, false);
verify("runtime value equality", (part + "va").equals(pooled), true);
verify("final constant", (constantPart + "va") == pooled, true);
verify("final runtime", (runtimePart + "va") == pooled, false);
verify("hello trap", "hello" == new String("hello").intern(), true);
verify("ab constant trap", "ab" == ("a" + "b"), true);
String r = "a";
verify("ab runtime trap", "ab" == (r + "b"), false);
StringBuilder builder = new StringBuilder("ja");
String snapshot = builder.toString();
builder.append("va");
verify("snapshot unchanged", snapshot.equals("ja"), true);
System.out.println("PASS: " + checks + " checks");
}
}Chạy độc lập trên JDK 21; chương trình dùng AssertionError trực tiếp nên không cần -ea.
javac --release 21 StringIdentityCheck.java ConcatProbe.java
java StringIdentityCheck
java ConcatProbeAcceptance: StringIdentityCheck in PASS: 16 checks; ConcatProbe in bốn dòng true, false, true, false. Phần “intern did not reassign” chỉ đúng với precondition canonical đã được giữ trước đó trong bài test; không phủ nhận nhánh pool chưa có ở mục 3.
Artifact tự học: giữ source đã chạy, java -version, stdout, concat-bytecode.txt và câu trả lời cho 6 mục checklist. Chưa có benchmark về memory/throughput trong bài này; không suy ra hiệu năng từ 16 kiểm tra correctness. Các bài kiểm chứng bổ sung không thay đổi tổng lab chính thức của Part 01.
Tài liệu tham khảo & phạm vi phiên bản
[S1] String API — Immutability, new String, equality và contract intern chính xác. Bản Java SE 21.
[S2] JLS 3.10.5 · String Literals — Literal/canonical identity và interning.
[S3] JLS 15.29 · Constant Expressions — Đã hiệu chỉnh anchor của reference nguồn: ở Java SE 21, Constant Expressions là §15.29.
[S4] JLS 15.18.1 · String Concatenation Operator + — String kết quả mới trừ trường hợp constant expression.
[S5] JLS 4.12.4 · final Variables — Điều kiện để một final variable là constant variable.
[S6] Java 21 API · StringBuilder — Mutable builder, không đồng bộ; toString trả String mới và độc lập với mutation tiếp theo.
[S7] Java 21 API · StringBuffer — Giới hạn synchronization và các thao tác trên buffer.
[S8] Java 21 API · StringConcatFactory — Bootstrap cho concatenation; API có từ Java 9. Bài không dùng các API preview xuất hiện trong trang Java 21.
[S9] JVMS 5.1 · The Run-Time Constant Pool — String constants là reference và cách dẫn xuất chúng từ class-file metadata.
[S10] JVMS 2.5.5 · Run-Time Constant Pool — Cấu trúc theo class/interface; chapter 2 phân biệt mô hình VM với layout implementation.
[S11] Oracle · Java Virtual Machine Enhancements in JDK 7 — Reference lịch sử: interned Strings trong HotSpot được chuyển khỏi permanent generation sang main heap.
[S12] Oracle · Secure Coding Guidelines for Java SE — Confidentiality 2-2/2-3: tránh log/copy secret, giới hạn của việc xóa buffer; injection là concern riêng.
[S13] JDK 21 · javap — Disassembly và verbose class-file metadata; không phải heap profiler.
[S14] JDK 21 · jcmd — Diagnostic commands, help, VM.stringtable và mức tác động cần kiểm trước khi chạy.
[S15] JLS 17 · Threads and Locks — Immutability/final-field semantics không thay thế synchronization cho cập nhật shared reference.