Chuyển tới nội dung chính

String cơ bản

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu String là object (không phải primitive) và cơ chế String Pool tối ưu bộ nhớ
  • Phân biệt so sánh String với == vs equals() và tránh lỗi phổ biến
  • Sử dụng các method quan trọng: length(), charAt(), indexOf(), substring(), split()
  • Nắm vững tính immutability của String và ý nghĩa của nó
  • Hiểu nguyên nhân tại sao String là immutable và lợi ích mang lại

String là một trong những kiểu dữ liệu được sử dụng nhiều nhất trong Java. Tuy nhiên, String trong Java có nhiều đặc điểm đặc biệt mà bạn cần hiểu rõ để sử dụng hiệu quả.

String là gì?

Khái niệm quan trọng

String trong Java KHÔNG phải là kiểu dữ liệu nguyên thủy (primitive type) mà là một object (đối tượng) của class java.lang.String.

// String là object, không phải primitive
String name = "Java"; // name là một reference đến String object
int age = 25; // age là primitive type

Đặc điểm chính của String

Đặc điểmMô tả
ImmutableMột khi tạo ra, nội dung String không thể thay đổi
Final classClass String là final, không thể kế thừa
Thread-safeDo tính immutable, String an toàn khi dùng đa luồng
String PoolJava tối ưu hóa bộ nhớ bằng String Pool

String Pool (String Intern Pool)

String Pool là một vùng nhớ đặc biệt trong Java Heap nơi JVM lưu trữ các String literals để tái sử dụng.

Cách hoạt động của String Pool

// Cách 1: String literal - được lưu trong String Pool
String s1 = "Hello";
String s2 = "Hello";

// Cách 2: Sử dụng new keyword - tạo object mới trong Heap
String s3 = new String("Hello");
String s4 = new String("Hello");

System.out.println(s1 == s2); // true - cùng trỏ đến 1 object trong Pool
System.out.println(s3 == s4); // false - 2 object khác nhau trong Heap
Tối ưu bộ nhớ

Khi bạn tạo String bằng literal ("Hello"), JVM sẽ:

  1. Kiểm tra xem "Hello" đã có trong String Pool chưa
  2. Nếu có, trả về reference đến object đó
  3. Nếu chưa, tạo object mới trong Pool và trả về reference

Điều này giúp tiết kiệm bộ nhớ khi có nhiều String giống nhau.

Minh họa bằng sơ đồ

String Pool (trong Java Heap)
┌─────────────────────────┐
│ "Hello" object │ ← s1 trỏ đến đây
│ │ ← s2 cũng trỏ đến đây
└─────────────────────────┘

Heap (ngoài String Pool)
┌─────────────────────────┐
│ "Hello" object #1 │ ← s3 trỏ đến đây
└─────────────────────────┘
┌─────────────────────────┐
│ "Hello" object #2 │ ← s4 trỏ đến đây
└─────────────────────────┘

Đưa String vào Pool với intern()

String s1 = "Hello";
String s2 = new String("Hello");
String s3 = s2.intern(); // Đưa s2 vào Pool, trả về reference từ Pool

System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // true - s3 là reference từ Pool

So sánh String: equals() vs ==

Lỗi thường gặp

Đây là một trong những lỗi phổ biến nhất của người mới học Java!

Sự khác biệt

Toán tửSo sánhKhi nào dùng
==So sánh địa chỉ bộ nhớ (reference)Kiểm tra 2 biến có trỏ đến cùng 1 object không
equals()So sánh nội dung (content)Kiểm tra 2 String có cùng giá trị không

Ví dụ minh họa

public class StringComparison {
public static void main(String[] args) {
// Trường hợp 1: String literals
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true - cùng reference
System.out.println(s1.equals(s2)); // true - cùng nội dung

// Trường hợp 2: new String()
String s3 = new String("Java");
String s4 = new String("Java");
System.out.println(s3 == s4); // false - khác reference
System.out.println(s3.equals(s4)); // true - cùng nội dung

// Trường hợp 3: literal vs new
String s5 = "Java";
String s6 = new String("Java");
System.out.println(s5 == s6); // false - khác reference
System.out.println(s5.equals(s6)); // true - cùng nội dung

// Trường hợp 4: Input từ user (luôn là object mới)
String userInput = new Scanner(System.in).nextLine();
if (userInput == "Java") { // SAI! Đừng dùng ==
System.out.println("Không bao giờ chạy vào đây!");
}
if (userInput.equals("Java")) { // ĐÚNG!
System.out.println("Đây mới đúng!");
}
}
}
Quy tắc vàng

LUÔN LUÔN dùng equals() để so sánh nội dung của String, KHÔNG BAO GIỜ dùng ==.

equalsIgnoreCase() - Bỏ qua chữ hoa/thường

String s1 = "Java";
String s2 = "JAVA";
String s3 = "java";

System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
System.out.println(s1.equalsIgnoreCase(s3)); // true

String Methods thường dùng

1. Lấy thông tin về String

String text = "Hello Java";

// length() - độ dài chuỗi
System.out.println(text.length()); // 10

// isEmpty() - kiểm tra chuỗi rỗng
String empty = "";
System.out.println(empty.isEmpty()); // true
System.out.println(text.isEmpty()); // false

// isBlank() - kiểm tra chuỗi rỗng hoặc chỉ có khoảng trắng (Java 11+)
String blank = " ";
System.out.println(blank.isBlank()); // true
System.out.println(text.isBlank()); // false

// charAt(index) - lấy ký tự tại vị trí index
System.out.println(text.charAt(0)); // 'H'
System.out.println(text.charAt(6)); // 'J'
// System.out.println(text.charAt(20)); // StringIndexOutOfBoundsException

2. Tìm kiếm trong String

String text = "Java Programming";

// indexOf() - tìm vị trí xuất hiện đầu tiên
System.out.println(text.indexOf('a')); // 1
System.out.println(text.indexOf("Programming")); // 5
System.out.println(text.indexOf('x')); // -1 (không tìm thấy)

// lastIndexOf() - tìm vị trí xuất hiện cuối cùng
System.out.println(text.lastIndexOf('a')); // 3

// contains() - kiểm tra có chứa chuỗi con không
System.out.println(text.contains("Java")); // true
System.out.println(text.contains("Python")); // false

// startsWith() và endsWith()
System.out.println(text.startsWith("Java")); // true
System.out.println(text.endsWith("ing")); // true

3. Trích xuất chuỗi con

String text = "Hello Java World";

// substring(beginIndex) - từ vị trí beginIndex đến hết
System.out.println(text.substring(6)); // "Java World"

// substring(beginIndex, endIndex) - từ beginIndex đến endIndex-1
System.out.println(text.substring(0, 5)); // "Hello"
System.out.println(text.substring(6, 10)); // "Java"

// Lưu ý: endIndex không bao gồm
String word = "Java";
System.out.println(word.substring(0, 4)); // "Java" (0,1,2,3)

4. Thay đổi định dạng

String text = "  Hello Java World  ";

// trim() - xóa khoảng trắng đầu và cuối
System.out.println(text.trim()); // "Hello Java World"

// strip() - xóa khoảng trắng (bao gồm Unicode whitespace, Java 11+)
System.out.println(text.strip()); // "Hello Java World"
System.out.println(text.stripLeading()); // "Hello Java World "
System.out.println(text.stripTrailing()); // " Hello Java World"

// toUpperCase() và toLowerCase()
String name = "Java";
System.out.println(name.toUpperCase()); // "JAVA"
System.out.println(name.toLowerCase()); // "java"

// repeat() - lặp lại chuỗi (Java 11+)
System.out.println("Ha".repeat(3)); // "HaHaHa"

5. Thay thế nội dung

String text = "Java is fun. Java is powerful.";

// replace() - thay thế tất cả
System.out.println(text.replace("Java", "Python"));
// "Python is fun. Python is powerful."

// replaceFirst() - thay thế lần đầu tiên
System.out.println(text.replaceFirst("Java", "Python"));
// "Python is fun. Java is powerful."

// replaceAll() - thay thế với regex
String phone = "0123-456-789";
System.out.println(phone.replaceAll("-", "")); // "0123456789"

6. Tách chuỗi

String csv = "Java,Python,C++,JavaScript";

// split() - tách chuỗi thành mảng
String[] languages = csv.split(",");
for (String lang : languages) {
System.out.println(lang);
}
// Output:
// Java
// Python
// C++
// JavaScript

// split() với giới hạn
String text = "one:two:three:four";
String[] parts = text.split(":", 2);
System.out.println(Arrays.toString(parts)); // ["one", "two:three:four"]

String Concatenation (Nối chuỗi)

1. Sử dụng toán tử +

String firstName = "John";
String lastName = "Doe";
String fullName = firstName + " " + lastName;
System.out.println(fullName); // "John Doe"

// Nối với các kiểu dữ liệu khác
int age = 25;
String message = "Age: " + age; // Java tự động chuyển age thành String
System.out.println(message); // "Age: 25"
Hiệu năng

Toán tử + tạo ra String object mới mỗi lần nối. Nếu nối nhiều lần trong vòng lặp, hiệu năng sẽ kém.

2. Sử dụng concat()

String s1 = "Hello";
String s2 = "World";
String result = s1.concat(" ").concat(s2);
System.out.println(result); // "Hello World"

3. Ví dụ về hiệu năng kém

// KHÔNG NÊN làm như này
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Tạo ra 1000 String objects mới!
}

// NÊN dùng StringBuilder (sẽ học ở bài tiếp theo)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();

Tính Immutability của String

String là Immutable (Bất biến)

Định nghĩa

Immutable nghĩa là một khi String object được tạo ra, nội dung của nó không thể thay đổi.

Hãy nghĩ như Căn cước công dân (CCCD): khi đã in ra thì không thể sửa tên, ngày sinh trên tấm thẻ đó. Muốn thay đổi thông tin? Phải cấp thẻ mới. String trong Java cũng vậy — mọi "thay đổi" thực ra là tạo String mới.

String s1 = "Hello";
s1.toUpperCase(); // Tạo ra String MỚI "HELLO"
System.out.println(s1); // "Hello" - s1 KHÔNG thay đổi!

String s2 = s1.toUpperCase();
System.out.println(s2); // "HELLO" - phải gán vào biến mới

Minh họa Immutability

public class StringImmutability {
public static void main(String[] args) {
String original = "Java";
String modified = original;

// Các phương thức này KHÔNG thay đổi original
original.concat(" Programming");
original.toUpperCase();
original.replace('J', 'K');

System.out.println(original); // "Java" - KHÔNG đổi!

// Phải gán lại để lưu kết quả
String result = original.concat(" Programming");
System.out.println(result); // "Java Programming"
}
}

Tại sao String là Immutable?

Lý doGiải thích
String PoolCho phép tái sử dụng String trong Pool an toàn
Thread-safeNhiều thread dùng chung String không lo bị thay đổi
SecurityString dùng cho username, password không bị sửa đổi
Hashcode cachingHashcode tính 1 lần, dùng nhiều lần (hiệu quả cho HashMap)

Ví dụ về vấn đề nếu String là mutable

// GIẢ SỬ String là mutable (đây chỉ là ví dụ minh họa)
String username = "admin";
String auth = username; // Dùng cho authentication

// Sau đó code khác thay đổi username
username.setValue("hacker"); // Giả sử có method này

// Thì auth cũng bị thay đổi! (vì cùng trỏ đến 1 object)
System.out.println(auth); // "hacker" - VẤN ĐỀ BẢO MẬT!

Ví dụ tổng hợp

Bài tập: Xử lý thông tin sinh viên

public class StudentInfoProcessor {
public static void main(String[] args) {
String studentInfo = " nguyen van a, 20, ha noi ";

// 1. Xóa khoảng trắng thừa
studentInfo = studentInfo.trim();
System.out.println("After trim: " + studentInfo);

// 2. Tách thông tin
String[] parts = studentInfo.split(",");
String name = parts[0].trim();
String ageStr = parts[1].trim();
String city = parts[2].trim();

// 3. Chuẩn hóa tên (chữ cái đầu viết hoa)
String[] nameWords = name.split(" ");
StringBuilder properName = new StringBuilder();
for (String word : nameWords) {
if (!word.isEmpty()) {
String capitalized = word.substring(0, 1).toUpperCase()
+ word.substring(1).toLowerCase();
properName.append(capitalized).append(" ");
}
}
name = properName.toString().trim();

// 4. Chuyển đổi tuổi
int age = Integer.parseInt(ageStr);

// 5. Chuẩn hóa thành phố
city = city.substring(0, 1).toUpperCase()
+ city.substring(1).toLowerCase();

// 6. In kết quả
System.out.println("=== Thông tin sinh viên ===");
System.out.println("Họ tên: " + name);
System.out.println("Tuổi: " + age);
System.out.println("Thành phố: " + city);
}
}

Bài tập: Email validation đơn giản

public class EmailValidator {
public static boolean isValidEmail(String email) {
if (email == null || email.isEmpty()) {
return false;
}

// Kiểm tra có @ không
if (!email.contains("@")) {
return false;
}

// Kiểm tra @ không ở đầu hoặc cuối
if (email.startsWith("@") || email.endsWith("@")) {
return false;
}

// Tách local và domain
String[] parts = email.split("@");
if (parts.length != 2) {
return false; // Có nhiều hơn 1 @
}

String local = parts[0];
String domain = parts[1];

// Kiểm tra domain có dấu chấm không
if (!domain.contains(".")) {
return false;
}

// Kiểm tra domain không bắt đầu hoặc kết thúc bằng dấu chấm
if (domain.startsWith(".") || domain.endsWith(".")) {
return false;
}

return true;
}

public static void main(String[] args) {
String[] emails = {
"[email protected]", // valid
"@example.com", // invalid
"user@", // invalid
"userexample.com", // invalid
"user@@example.com", // invalid
"user@domain.", // invalid
"[email protected]" // invalid
};

for (String email : emails) {
System.out.printf("%-25s -> %s%n", email,
isValidEmail(email) ? "Valid" : "Invalid");
}
}
}

String Pool qua các phiên bản Java

String Pool đã thay đổi vị trí lưu trữ qua các phiên bản Java:

Java 6 trở về trước     Java 7              Java 8+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ JVM Heap │ │ JVM Heap │ │ JVM Heap │
│ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │ │ │String Pool│ │ │ │String Pool│ │
│ │ │ └──────────┘ │ │ └──────────┘ │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ PermGen │ │ PermGen │ │ Metaspace │
│ ┌──────────┐ │ │ │ │ (native mem) │
│ │String Pool│ │ │ │ │ │
│ └──────────┘ │ └──────────────┘ └──────────────┘
└──────────────┘
Pool ở PermGen → Pool chuyển lên PermGen bị xóa,
PermGen có size Heap → GC thu hồi thay bằng Metaspace
cố định → OOM risk String không dùng
VersionString Pool ở đâuLý do thay đổi
Java 6-PermGenPermGen size cố định → OutOfMemoryError: PermGen space nếu intern() nhiều
Java 7HeapChuyển sang Heap để GC thu hồi interned strings, giảm OOM risk
Java 8+Heap (PermGen bị xóa)PermGen hoàn toàn bị thay bằng Metaspace (native memory)
Thực tế

Từ Java 7+, bạn có thể gọi intern() thoải mái hơn mà ít lo OOM — Garbage Collector sẽ thu hồi interned strings khi không còn reference.

Compact Strings (Java 9+)

Trước Java 9, String lưu nội dung trong char[] — mỗi ký tự chiếm 2 bytes (UTF-16). Nhưng phần lớn strings trong ứng dụng chỉ chứa ký tự ASCII/Latin (1 byte là đủ) → lãng phí 50% bộ nhớ.

Từ Java 9, String dùng byte[] + coder flag:

Trước Java 9:     String "Hello" → char[] { 'H', 'e', 'l', 'l', 'o' }  = 10 bytes

Từ Java 9: String "Hello" → byte[] { 72, 101, 108, 108, 111 } = 5 bytes (LATIN1)
String "Xin chào" → byte[] { ... } = UTF16 (2 bytes/char)
LATIN1 (coder = 0)UTF16 (coder = 1)
Khi nàoString chỉ chứa ASCII/Latin-1 (0-255)String chứa ký tự Unicode (tiếng Việt, CJK...)
Bộ nhớ1 byte/char2 bytes/char
Tiết kiệm~50% so với Java 8Không tiết kiệm
Tự động, không cần config

Compact Strings được bật mặc định từ Java 9. JVM tự chọn LATIN1 hay UTF16 cho từng String. Nếu muốn tắt (hiếm khi cần): -XX:-CompactStrings

Lưu ý: split(".") dùng regex

Bẫy phổ biến

String.split() nhận regex pattern, không phải plain text. Dấu . trong regex nghĩa là "bất kỳ ký tự nào":

String filename = "report.2024.pdf";

// ❌ SAI: "." trong regex = bất kỳ ký tự nào
String[] parts = filename.split(".");
System.out.println(parts.length); // 0 — tất cả bị match!

// ✅ ĐÚNG: Escape dấu chấm
String[] parts2 = filename.split("\\.");
System.out.println(Arrays.toString(parts2)); // [report, 2024, pdf]

// ✅ Hoặc dùng Pattern.quote()
String[] parts3 = filename.split(Pattern.quote("."));

Tương tự: split("|"), split("("), split("+") đều cần escape vì là regex special characters.

Bài tập thực hành

Bài 1: Palindrome Checker

Viết chương trình kiểm tra một chuỗi có phải là palindrome không (đọc xuôi ngược như nhau).

public class PalindromeChecker {
public static boolean isPalindrome(String str) {
// TODO: Implement this
// Hint: So sánh ký tự đầu với cuối, tiếp tục vào trong
return false;
}

public static void main(String[] args) {
System.out.println(isPalindrome("racecar")); // true
System.out.println(isPalindrome("hello")); // false
System.out.println(isPalindrome("A man a plan a canal Panama")); // true (nếu bỏ qua space và case)
}
}

Bài 2: Word Counter

Đếm số lượng từ trong một câu.

public class WordCounter {
public static int countWords(String sentence) {
// TODO: Implement this
// Hint: Dùng split() và xử lý khoảng trắng thừa
return 0;
}

public static void main(String[] args) {
System.out.println(countWords("Hello World")); // 2
System.out.println(countWords(" Java Programming ")); // 2
System.out.println(countWords("")); // 0
}
}

Bài 3: String Reverser

Viết chương trình đảo ngược một chuỗi.

public class StringReverser {
public static String reverse(String str) {
// TODO: Implement this
// Hint: Duyệt từ cuối về đầu
return "";
}

public static void main(String[] args) {
System.out.println(reverse("Hello")); // "olleH"
System.out.println(reverse("Java")); // "avaJ"
}
}

Tổng kết

Những điểm quan trọng cần nhớ
  1. String là object, không phải primitive type
  2. String là immutable - không thể thay đổi sau khi tạo
  3. String Pool giúp tối ưu bộ nhớ cho String literals
  4. LUÔN dùng equals() để so sánh nội dung, không dùng ==
  5. String literal ("text") khác với new String("text")
  6. Các phương thức String trả về String MỚI, không thay đổi String gốc
  7. Nối String với + trong vòng lặp kém hiệu quả

Đọc thêm