String cơ bản
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ì?
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ểm | Mô tả |
|---|---|
| Immutable | Một khi tạo ra, nội dung String không thể thay đổi |
| Final class | Class String là final, không thể kế thừa |
| Thread-safe | Do tính immutable, String an toàn khi dùng đa luồng |
| String Pool | Java 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
Khi bạn tạo String bằng literal ("Hello"), JVM sẽ:
- Kiểm tra xem "Hello" đã có trong String Pool chưa
- Nếu có, trả về reference đến object đó
- 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 ==
Đâ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ánh | Khi 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!");
}
}
}
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"
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)
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ý do | Giải thích |
|---|---|
| String Pool | Cho phép tái sử dụng String trong Pool an toàn |
| Thread-safe | Nhiều thread dùng chung String không lo bị thay đổi |
| Security | String dùng cho username, password không bị sửa đổi |
| Hashcode caching | Hashcode 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
| Version | String Pool ở đâu | Lý do thay đổi |
|---|---|---|
| Java 6- | PermGen | PermGen size cố định → OutOfMemoryError: PermGen space nếu intern() nhiều |
| Java 7 | Heap | Chuyể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) |
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ào | String chỉ chứa ASCII/Latin-1 (0-255) | String chứa ký tự Unicode (tiếng Việt, CJK...) |
| Bộ nhớ | 1 byte/char | 2 bytes/char |
| Tiết kiệm | ~50% so với Java 8 | Không tiết kiệm |
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
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
- String là object, không phải primitive type
- String là immutable - không thể thay đổi sau khi tạo
- String Pool giúp tối ưu bộ nhớ cho String literals
- LUÔN dùng equals() để so sánh nội dung, không dùng ==
- String literal (
"text") khác vớinew String("text") - Các phương thức String trả về String MỚI, không thay đổi String gốc
- Nối String với
+trong vòng lặp kém hiệu quả