Java Core / String
Part 01 · Java Core · 1.1.02

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 ==.

1.1.02 · String Pool8 mục nền6 mục interview checklistJava SE 21 · không dùng preview
Cách học: vẽ reference trước khi đoán output. Tách identity, value, constant expression và runtime allocation. Tám mục nền được giữ; các bổ sung làm rõ contract, trạng thái pool trước 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

ab 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]

Không gộp hai loại pool: constant pool của file .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"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); // true

Trong 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]

Trade-off production: chỉ cân nhắc intern khi domain có lượng giá trị hữu hạn, lặp lại nhiều và phép đo cho thấy lợi ích. Với ID/request payload/user input gần như duy nhất, đề xuất ưu tiên không intern đại trà. Đo allocation, heap sống, GC, CPU và latency trên workload thật; không hứa tỉ lệ tiết kiệm hay tốc độ lookup cố định. Intern không phải công cụ xóa dữ liệu hay cơ chế giới hạn cache.

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]

Guarantee chứ không phải “thường”: trong các biểu thức runtime concatenation cụ thể ở đây, JLS quy định String kết quả mới; identity khác literal đã có. Tối ưu compiler/JIT không được biến một kết quả == 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.txt

Expected 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?

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]

Security boundary: immutability không phải validation, authorization hay chống SQL injection. Nó chỉ ngăn caller sửa nội dung của cùng một String thông qua API thông thường. Không ghi password/token vào log; hạn chế thời gian giữ và số bản copy. Buffer xóa được có thể giảm cửa sổ phơi lộ, nhưng library/runtime/OS vẫn có thể tạo bản sao. Không hứa “zeroize tuyệt đối” chỉ bằng ghi đè một 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ệmVai tròKhông nên kết luận
constant_pool trong .classMetadata/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 poolCấ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 StringsCanonicalization 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]

Giới hạn vận hành: không chạy dump hoặc bật chi tiết chứa giá trị String trên production một cách tùy tiện. Đọc mức tác động của command trong đúng runtime, bảo vệ artifact và tránh lộ dữ liệu nhạy cảm. Cũng không lấy một ảnh chụp heap làm bảo đảm về thời điểm GC hoặc thời điểm xóa secret. [S12] [S14]

7. StringBuilder và StringBuffer

LoạiSemanticsDùng khi
StringImmutableValue text thông thường, chia sẻ an toàn.
StringBuilderMutable, không synchronizedXây chuỗi qua loop/nhiều bước trong một thread.
StringBufferMutable, methods synchronizedLegacy/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, core

Output 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]

Trade-off: một biểu thức nối ngắn rõ ràng không cần đổi máy móc sang builder. Repeated concatenation trong loop là nơi nên khảo sát allocation, nhưng lợi ích phụ thuộc kích thước, số vòng lặp, compiler và workload. Khi đo, giữ kết quả thực sự được sử dụng và tách warm-up khỏi measurement; không suy ra benchmark cố định từ ví dụ nhỏ.

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

Bổ sung · interview evidence

Giữ 6 mục checklist, kiểm chứng bằng 16 điều kiện

Mục nguồnEvidence 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 / internVẽ các reference; ghi precondition về pool; giải thích đúng cả nhánh intern trả receiver.
3. Constant và runtime concatDự đoán ConcatProbe; chỉ ra vì sao finalRuntime không phải constant expression.
4. Pool và HotSpot heapPhâ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 địnhNê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à benchmarkChứ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 ConcatProbe

Acceptance: 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

Đối chiếu ngày 12/09/2026. Dùng tài liệu Java SE/JDK 21 làm baseline; tài liệu lịch sử được ghi rõ. Các liên kết là nguồn đọc thêm, không phải dependency để hiển thị trang.

[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.