Hướng dẫn sử dụng Java String, StringBuffer và StringBuilder

1. Phân cấp thừa kế

Khi làm việc với các dữ liệu văn bản, Java cung cấp cho bạn 3 class String, StringBuffer và StringBuilder. Nếu làm việc với các dữ liệu lớn bạn nên sử dụng StringBuffer hoặc StringBuilder để đạt hiệu năng nhanh nhất. Về cơ bản 3 class này có nhiều điểm giống nhau.

  • String là không thể thay đổi (immutable – khái niệm này sẽ được nói chi tiết ở trong phần tiếp theo của bài viết này) và không cho phép có class con.
  • StringBufferStringBuilder có thể thay đổi (mutable)

StringBuilder và StringBuffer là giống nhau, nó chỉ khác biệt tình huống sử dụng có liên quan tới đa luồng (Multi Thread).

  • Nếu xử lý văn bản sử dụng nhiều luồng (Thread) bạn nên sử dụng StringBuffer để tránh tranh chấp giữa các luồng.
  • Nếu xử lý văn bản sử dụng 1 luồng (Thread) nên sử dụng StringBuilder.

Nếu so sánh về tốc độ xử lý StringBuilder là tốt nhất, sau đó StringBuffer và cuối cùng mới là String.

2. Khái niệm mutable & immutable

  • Mutable Object: khi khởi tạo 1 đối tượng, tức ta có 1 tham chiếu tới 1 thể hiện của 1 lớp, thì trạng thái của đối tượng có thể thay đổi được sau khi việc khởi tạo đối tượng thành công. Trạng thái đối tượng ở đây có thể là các trường thông tin mà đối tượng đó nắm giữ. Ví dụ: tên, tuổi của 1 đối tượng sinh viên chẳng hạn. Điều này có nghĩa là, bạn vừa có thể get() vừa có thể set() giá trị.
  • Immutable Object: khi khởi tạo 1 đối tượng, thì trạng thái của tối tượng đó không thể thay đổi được sau khi việc khởi tạo đối tượng thành công. Điều này có nghĩa là, bạn chỉ có thể get() mà không thể set().

Ví dụ:

// Đây là một class với trường value, name.
// Khi bạn khởi tạo đối tượng class này
// bạn không thể sét đặt lại value từ bên ngoài, và tất cả các trường khác của nó cũng thế.
// Class này không hề có các hàm để sét đặt lại các trường (field) từ bên ngoài.
// Nếu muốn bạn chỉ có thể tạo mới một đối tượng khác.
// Điều đó có nghĩa là class này là không thể thay đổi (immutable)
public class ImmutableClassExample  {
private final int value;
private final String name;
public ImmutableClassExample(String name, int value)  {
       this.value = value;
       this.name= name;
}

public String getName()  {
       return name;
}

public int getValue()  {
      return value;
}
}

// Đây là một class có 1 trường value.
// Sau khi khởi tạo đối tượng bạn có thể sét đặt lại giá trị của trường value
// thông qua việc gọi method setNewValue(int).
// Như vậy đây là class có thể thay đổi (mutable).
public class MutableClassExample  {
private int value;

  public MutableClassExample(int value)  {
        this.value= value;
  }

  public void setNewValue(int newValue)  {
       this.value = newValue;
  }
}

String là một class không thể thay đổi, String có nhiều thuộc tính (trường), ví dụ length,… nhưng các giá trị đó là không thể thay đổi.

3. String

Trong java, String là một class đặc biệt, nguyên nhân là nó được sử dụng một cách thường xuyên trong một chương trình, vì vậy đòi hỏi nó phải có hiệu suất và sự mềm dẻo. Đó là lý do tại sao String có tính đối tượng và vừa có tính nguyên thủy (primitive).

3.1. Tính nguyên thủy:

Bạn có thể tạo một string literal (chuỗi chữ), string literal được lưu trữ trong ngăn sếp (stack), đòi hỏi không gian lưu trữ ít, và rẻ hơn khi thao tác.

  • String literal = “Hello World”;

Bạn có thể sử dụng toán tử + để nối 2 string, toán tử này vốn quen thuộc và sử dụng cho các kiểu dữ liệu nguyên thủy int, float, double.
Các string literal được chứa trong một bể chứa (common pool). Như vậy hai string literal có nội dung giống nhau sử dụng chung một vùng bộ nhớ trên stack, điều này giúp tiết kiệm bộ nhớ.

3.2. Tính đối tượng

Vì String là một class, vì vậy nó có thể được tạo ra thông qua toán tử new.

  • String object = new String(“Hello World”);

Các đối tượng String được lưu trữ trên Heap, yêu cầu quản lý bộ nhớ phức tạp và tốn không gian lưu trữ. Hai đối tượng String có nội dung giống nhau lưu trữ trên 2 vùng bộ nhớ khác nhau của Heap. Ví dụ:

// Tạo ngầm một String, thông qua "string literal".
// Đây là một "string literal".
// Cách này thể hiện tính nguyên thủy của String.

String str1 = "Java is Hot";

// Tạo một cách rõ ràng thông qua toán tử new.
// Đây là một "String object".
// Cách này thể hiện tính đối tượng của String,
// giống như các đối tượng khác trong Java.

String str2 = new String("I'm cool");

3.3. String Literal vs. String Object

Như đã đề cập, có hai cách để xây dựng một chuỗi (String): ngầm xây dựng bằng cách chỉ định một chuỗi chữ ( String literal) hay một cách rõ ràng tạo ra một đối tượng String thông qua toán tử new và cấu tử của String. Ví dụ:

String s1 = "Hello";              // String literal
String s2 = "Hello";              // String literal
String s3 = s1;                   // Cùng tham chiếu (trỏ tới cùng một vị trí)
String s4 = new String("Hello");  // Tạo mới một đối tượng String
String s5 = new String("Hello");  // Tạo mới một đối tượng String

Chúng ta sẽ giải thích bằng hình minh họa dưới đây:

Các string literal có cùng một nội dung, chúng sẽ chia sẻ cùng một vị trí lưu trữ trong bể chứa ( common pool). Trong khi đó các đối tượng String lưu trữ trong Heap, và không chia sẻ vị trí lưu trữ kể cả 2 đối tượng string này có nội dung giống nhau.

3.4. Sự khác nhau giữa tạo chuỗi bằng cách sử dụng từ khóa new () và literal

Khi chúng ta tạo chuỗi với toán tử new (), nó được tạo ra trong heap và không được thêm vào trong String pool. Trong khi String được tạo bằng cách sử dụng literal được tạo ra trong String pool và tồn tại trong vùng PermGen của heap.

String s = new String("Test");

Nó không đặt đối tượng trong String pool, chúng ta cần gọi phương thức String.intern() để đưa chúng vào String pool một cách tường minh.

Nếu tạo ra đối tượng String như String literal. Ví dụ: String s = “Test” Java tự động đưa nó vào trong String pool.

Như hình trên:

  • Chuỗi str1: không được quản lý bởi String Pool, tạo thêm vùng nhớ mới để lưu giá trị trong heap.
  • Chuỗi str2: được quản lý bởi String Pool, kiểm tra giá trị java5 trong String Pool đã tồn tại hay chưa, nếu chưa thì tạo vùng nhớ mới để lưu giá trị trong heap.
  • Chuỗi str3: mặc dù cùng giá trị java5 nhưng nó vẫn lưu ở vùng nhớ mới do không được quản lý bởi String Pool.
  • Chuỗi str4: không tạo thêm vùng nhớ mới do nó đã tồn tại tại trong String Pool, nó được quản lý bởi String Pool.

3.5. So sánh equals() và ==

Phương thức equals() sử dụng để so sánh 2 đối tượng, với String nó có ý nghĩa là so sánh nội dung của 2 string. Đối với các kiểu tham chiếu (reference) toán tử == có ý nghĩa là so sánh địa chỉ vùng bộ nhớ lưu trữ của đối tượng. Hãy xem ví dụ:

String s1 = "Hello"; // String literal
String s2 = "Hello"; // String literal
String s3 = s1; // Cùng tham chiếu (trỏ tới cùng một vị trí)
String s4 = new String("Hello"); // Tạo mới một đối tượng String
String s5 = new String("Hello"); // Tạo mới một đối tượng String

s1 == s1; // true, cùng trỏ vào một vị trí
s1 == s2; // true, s1 và s2 cùng trỏ tới 1 ví trí trong "bể chứa" (common pool)
s1 == s3; // true, s3 được gán bởi s1, nó sẽ trỏ tới vị trí s1 trỏ tới.
s1 == s4; // false, trỏ tới khác vị trí.
s4 == s5; // false, trỏ tới khác vị trí trên heap

s1.equals(s3); // true, cùng nội dung
s1.equals(s4); // true, cùng nội dung
s4.equals(s5); // true, cùng nội dung

Trong thực tế bạn nên sử dụng String literal, thay vì sử dụng toán tử new. Điều này làm tăng tốc chương trình của bạn.

3.6. Các phương thức của String

#METHODSDESCRIPTION
1char charAt(int index)Trả về một ký tự tại vị trí có chỉ số được chỉ định.
2int compareTo(Object o)So sánh một String với một Object khác.
3int compareTo(String anotherString)So sánh hai chuỗi theo từ điển. (Phân biệt chữ hoa chữ thường)
4int compareToIgnoreCase(String str)So sánh hai chuỗi theo từ điển. (Không phân biệt chữ hoa chữ thường)
5String concat(String str)Nối chuỗi được chỉ định đến cuối của chuỗi này.
6boolean contentEquals(StringBuffer sb)Trả về true nếu và chỉ nếu chuỗi này đại diện cho cùng một chuỗi ký tự như là StringBuffer quy định.
7static String copyValueOf(char[] data)Trả về một chuỗi đại diện cho chuỗi ký tự trong mảng quy định.
8static String copyValueOf(char[] data, int offset, int count)Trả về một chuỗi đại diện cho chuỗi ký tự trong mảng quy định.
9boolean endsWith(String suffix)Kiểm tra nếu chuỗi này kết thúc với hậu tố quy định.
10boolean equals(Object anObject)So sánh với một đối tượng
11boolean equalsIgnoreCase(String anotherString)So sánh với một String khác, không phân biệt chữ hoa chữ thường.
12byte[] getBytes()Mã hóa chuỗi này thành một chuỗi các byte bằng cách sử dụng bảng mã mặc định của flatform (nền tảng), lưu trữ kết quả vào một mảng byte mới.
13byte[] getBytes(String charsetName)Mã hóa chuỗi này thành một chuỗi các byte bằng cách sử dụng bảng mã cho trước, lưu trữ kết quả vào một mảng byte mới.
14void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)Copy các ký tự từ chuỗi này vào mảng ký tự đích.
15int hashCode()Trả về một mã “hash code” cho chuỗi này.
16int indexOf(int ch)Trả về chỉ số trong chuỗi này xuất hiện đầu tiên của ký tự cụ thể.
17int indexOf(int ch, int fromIndex)Trả về chỉ số trong chuỗi này xuất hiện đầu tiên của ký tự được chỉ định, bắt đầu tìm kiếm từ chỉ số cụ thể đến cuối.
18int indexOf(String str)Trả về chỉ số trong chuỗi này xuất hiện đầu tiên của chuỗi quy định.
19int indexOf(String str, int fromIndex)Trả về chỉ số trong chuỗi này xuất hiện đầu tiên của chuỗi quy định, bắt đầu từ chỉ số xác định.
20String intern()Returns a canonical representation for the string object.
21int lastIndexOf(int ch)Trả về chỉ số trong chuỗi này về sự xuất hiện cuối cùng của ký tự cụ thể.
22int lastIndexOf(int ch, int fromIndex)Trả về chỉ số trong chuỗi này về sự xuất hiện cuối cùng của ký tự được chỉ định, tìm kiếm lùi lại bắt đầu từ chỉ số xác định.
23int lastIndexOf(String str)Trả về chỉ số trong chuỗi này xảy ra cuối cùng bên phải của chuỗi quy định.
24int lastIndexOf(String str, int fromIndex)Trả về chỉ số trong chuỗi này về sự xuất hiện cuối cùng của chuỗi xác định, tìm kiếm lùi lại bắt đầu từ chỉ số xác định.
25int length()Trả về độ dài chuỗi.
26boolean matches(String regex)Kiểm tra chuỗi này khớp với biểu thức chính quy chỉ định hay không.
27boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)Kiểm tra chuỗi có một phần giống nhau.
28boolean regionMatches(int toffset, String other, int ooffset, int len)Kiểm tra chuỗi có một phần giống nhau.
29String replace(char oldChar, char newChar)Trả về một chuỗi mới từ thay thế tất cả các lần xuất hiện của ký tự oldChar trong chuỗi này với ký tự newChar.
30String replaceAll(String regex, String replacement)Thay thế tất cả các chuỗi con của chuỗi này khớp với biểu thức chính quy bởi String mới replacement
31String replaceFirst(String regex, String replacement)Thay thế chuỗi con đầu tiên của chuỗi này khớp với biểu thức chính quy bởi một String mới replacement
32String[] split(String regex)Tách chuỗi này thành các chuỗi con, tại các chỗ khớp với biểu thức chính quy cho trước.
33String[] split(String regex, int limit)Tách chuỗi này thành các chuỗi con, tại các chỗ khớp với biểu thức chính quy cho trước. Tối đa limit chuỗi con.
34boolean startsWith(String prefix)Kiểm tra nếu chuỗi này bắt đầu với tiền tố quy định.
35boolean startsWith(String prefix, int toffset)Kiểm tra nếu chuỗi này bắt đầu với tiền tố quy định bắt đầu một chỉ số xác định.
36CharSequence subSequence(int beginIndex, int endIndex)Trả về một chuỗi ký tự mới là một dãy con của dãy này.
37String substring(int beginIndex)Trả về một chuỗi ký tự mới là một dãy con của dãy này. Từ chỉ số cho trước tới cuối
38String substring(int beginIndex, int endIndex)Trả về một chuỗi ký tự mới là một dãy con của dãy này. Từ chỉ số bắt đầu cho tới chỉ số cuối.
39char[] toCharArray()Chuyển chuỗi này thành mảng ký tự.
40String toLowerCase()Chuyển tất cả các ký tự của chuỗi này sang chữ thường, sử dụng miền địa phương mặc định (default locale)
41String toLowerCase(Locale locale)Chuyển tất cả các ký tự của chuỗi này sang chữ thường, sử dụng miền địa phương (locale) cho trước.
42String toString()Trả về String này.
43String toUpperCase()Chuyển tất cả các ký tự của chuỗi này sang chữ hoa, sử dụng miền địa phương mặc định (default locale)
44String toUpperCase(Locale locale)Chuyển tất cả các ký tự của chuỗi này sang chữ hoa, sử dụng miền địa phương (locale) cho trước.
45String trim()Trả về một String mới, sau khi loại bỏ các ký tự trắng (whitespace) bên trái và bên phải.
46static String valueOf(primitive data type x)Returns the string representation of the passed data type argument.

3.6.1. length()

public class LengthDemo {

public static void main(String[] args) {
String str = "This is text";
int len = str.length();
System.out.println("String Length is : " + len);
}
}

Kết quả:

String Length is : 12

3.6.2. concat(String)

public class ConcatDemo {

public static void main(String[] args) {
String s1 = "One";
String s2 = "Two";
String s3 = "Three";
 // s1.concat(s2) rất giống với s1 + s2;
 String s = s1.concat(s2);
 System.out.println("s1.concat(s2) = " + s);

 // s1.concat(s2).concat(s3) rất giống với s1 + s2 + s3;
 s = s1.concat(s2).concat(s3);

 System.out.println("s1.concat(s2).concat(s3) = " + s);
}
}

Kết quả:

s1.concat(s2) = OneTwo
s1.concat(s2).concat(s3) = OneTwoThree

3.6.3. indexOf(..)

Ví dụ:

public class IndexOfDemo {

public static void main(String[] args) {
String str = "This is text";
// Tìm vị trí xuất hiện ký tự 'i' đầu tiên.
  // ==> 2
  int idx = str.indexOf('i');
  System.out.println("- indexOf('i') = " + idx);

  // Tìm vị trí xuất hiện ký tự 'i' đầu tiên
  // tính từ chỉ số thứ 4 trở về cuối chuỗi.
  // ==> 5
  idx = str.indexOf('i', 4);
  System.out.println("- indexOf('i',4) = " + idx);

  // Tìm vị trí xuất hiện chuỗi con "te" đầu tiên.
  // ==> 8
  idx = str.indexOf("te");
  System.out.println("- indexOf('te') = " + idx);
}
}

Kết quả:

- indexOf('i') = 2
- indexOf('i',4) = 5
- indexOf('te') = 8

3.6.4. substring(..)

Ví dụ:

public class SubstringDemo {

public static void main(String[] args) {
String str = "This is text";
// Trả về chuỗi con từ chỉ số thứ 3 tới cuối chuỗi.
  String substr = str.substring(3);

  System.out.println("- substring(3)=" + substr);

  // Trả về chuỗi con từ chỉ số thứ 2 cho tới chỉ số 7
  substr = str.substring(2, 7);

  System.out.println("- substring(2, 7) =" + substr);
}
}

Kết quả:

- substring(3)=s is text
- substring(2, 7) =is text

3.6.5. replace

// Trả về một chuỗi mới từ thay thế tất cả các lần xuất hiện
// của ký tự oldChar trong chuỗi này với ký tự newChar.
public String replace(char oldChar, char newChar)

// Thay thế tất cả các chuỗi con của chuỗi này khớp
// với biểu thức chính quy bởi String mới replacement
public String replaceAll(String regex, String replacement)

// Thay thế chuỗi con đầu tiên của chuỗi này khớp
// với biểu thức chính quy bởi một String mới replacement
public String replaceFirst(String regex, String replacement)

Ví dụ:

public class ReplaceDemo {

public static void main(String[] args) {
String str = "This is text";
// Thay thế các ký tự 'i' bởi ký tự 'x'.
  String s2 = str.replace('i', 'x');

  System.out.println("- s2=" + s2);

  // Thay thế tất cả các chuỗi con khớp với "is" bởi "abc".
  String s3 = str.replaceAll("is", "abc");

  System.out.println("- s3=" + s3);

  // Thay thế tất cả các chuỗi con đầu tiên khớp với "is" bởi "abc".
  String s4 = str.replaceFirst("is", "abc");

  System.out.println("- s4=" + s4);

  // (Xem thêm tài liệu biểu thức chính quy)
  // Thay thế tất cả các chuỗi con khớp với biểu thức:
  // "is|te": Nghia là "is" hoặc "te"
  // thay bởi "+".
  String s5 = str.replaceAll("is|te", "+");
  System.out.println("- s5=" + s5);
}
}

Kết quả:

- s2=Thxs xs text
- s3=Thabc abc text
- s4=Thabc is text
- s5=Th+ + +xt

Các ví dụ khác

- str=This is text
- s2=this is text
- s3=THIS IS TEXT
- 'str' startsWith This ? true
- str= 	 Java is hot!  	 
- s4=Java is hot!

4. StringBuffer vs StringBuilder

StringBuilder và StringBuffer là rất giống nhau, điều khác biệt là tất cả các phương thức của StringBuffer đã được đồng bộ, nó thích hợp khi bạn làm việc với ứng dụng đa luồng, nhiều luồng có thể truy cập vào một đối tượng StringBuffer cùng lúc. Trong khi đó StringBuilder có các phương thức tương tự nhưng không được đồng bộ, nhưng vì vậy mà hiệu suất của nó cao hơn, bạn nên sử dụng StringBuilder trong ứng dụng đơn luồng, hoặc sử dụng như một biến địa phương trong một phương thức.

4. 1. Các method của StringBuffer (StringBuilder)

// Cấu tử.
StringBuffer() // an initially-empty StringBuffer
StringBuffer(int size) // with the specified initial size
StringBuffer(String s) // with the specified initial content

// Độ dài
int length()

// Các method xây dựng nội dung
// type ở đây có thể là kiểu nguyên thủy (primitive), char[], String, StringBuffer, .v.v..
StringBuffer append(type arg) // ==> chú ý (ở trên)
StringBuffer insert(int offset, type arg) // ==> chú ý (ở trên)

// Các method thao tác trên nội dung.
StringBuffer delete(int start, int end)
StringBuffer deleteCharAt(int index)
void setLength(int newSize)
void setCharAt(int index, char newChar)
StringBuffer replace(int start, int end, String s)
StringBuffer reverse()

// Các method trích ra toàn bộ hoặc một phần dữ liệu.
char charAt(int index)
String substring(int start)
String substring(int start, int end)
String toString()

// Các method tìm kiếm vị trí.
int indexOf(String searchKey)
int indexOf(String searchKey, int fromIndex)
int lastIndexOf(String searchKey)
int lastIndexOf(String searchKey, int fromIndex)

4.2. Ví dụ minh họa


public class StringBuilderDemo {
 
  public static void main(String[] args) {
 
      // Tạo đối tượng StringBuilder
      // Hiện tại chưa có dữ liệu trên StringBuilder.
      StringBuilder sb = new StringBuilder(10);
      
      // Nối thêm chuỗi Hello vào sb.
      sb.append("Hello...");
      System.out.println("- sb after appends a string: " + sb);
 
      // append a character
      char c = '!';
      sb.append(c);
      System.out.println("- sb after appending a char: " + sb);
 
      // Trèn một String vào vị trí thứ 5
      sb.insert(8, " Java");
      System.out.println("- sb after insert string: " + sb);
      
      // Xóa đoạn String con trên StringBuilder.
      // Tại vị trí có chỉ số 5 tới 8
      sb.delete(5,8);
 
      System.out.println("- sb after delete: " + sb);
  }
}

Kết quả:

- sb after appends a string: Hello...
- sb after appending a char: Hello...!
- sb after insert string: Hello... Java!
- sb after delete: Hello Java!

5. So sánh hiệu suất giữa String, StringBuilder và StringBuffer

package com.maixuanviet;

import org.apache.commons.lang3.time.StopWatch;

public class StringConcatenate {
public static final int NUM_OF_ITEM = 10000;
public static void main(String[] args) {
    StopWatch stopwatch = new StopWatch();

    // Concat string using String Object
    stopwatch.start();
    stringConcat();
    stopwatch.stop();
    System.out.println("time taken by String : " + stopwatch.getNanoTime() + " nanoseconds");

    // Concat string using StringBuilder
    stopwatch.reset();
    stopwatch.start();
    stringBuilder();
    stopwatch.stop();
    System.out.println("time taken by StringBuilder : " + stopwatch.getNanoTime() + " nanoseconds");

    // Concat string using StringBuffer
    stopwatch.reset();
    stopwatch.start();
    stringBuffer();
    stopwatch.stop();
    System.out.println("time taken by StringBuffer : " + stopwatch.getNanoTime() + " nanoseconds");
}

public static String stringConcat() {
    String res = "";
    for (int i = 0; i < NUM_OF_ITEM; i++) {
        res += i;
    }
    return res;
}

public static String stringBuilder() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < NUM_OF_ITEM; i++) {
        sb.append(i);
    }
    return sb.toString();
}

public static String stringBuffer() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < NUM_OF_ITEM; i++) {
        sb.append(i);
    }
    return sb.toString();
}
}

Kết quả thực thi chương trình trên:

time taken by String : 205909572 nanoseconds
time taken by StringBuilder : 572201 nanoseconds
time taken by StringBuffer : 1484385 nanoseconds

Như bạn thấy: hiệu suất tốt nhất là StringBuilder -> tiếp theo là StringBuffer và String Object.

6. Lưu ý

Cần lưu ý rằng nếu có một câu lệnh chứa nhiều toán tử ‘+’ để nối các chuỗi thì JVM sẽ tự động sử dụng StringBuffer / StringBuilder.

Ví dụ:

public class StringConcatenate {
public final String SITE = " maixuanviet" + ".com";
public static void main(String[] args) {
    StringConcatenate str = new StringConcatenate();
    System.out.println("Welcome" + " to" + str.SITE);
}
}

Khi bạn build lớp trên sang .class, bạn sẽ thấy nội dung như sau:

import java.io.PrintStream;

public class StringConcatenate {
public final String SITE = " maixuanviet.com";
public static void main(String[] arg) {
    StringConcatenate arg0 = new StringConcatenate();
    PrintStream arg9999 = System.out;
    StringBuilder arg10000 = (new StringBuilder()).append("Welcome to");
    arg0.getClass();
    arg9999.println(arg10000.append("maixuanviet.com").toString());
}
}

7. Trong những trường hợp nào thì nên dùng toán tử + với String và lúc nào thì nên dùng StringBuilder?

Vấn đề của bài toán về string là string tốn nhiều không gian lưu trữ hơn number rất nhiều. Một kí tự có thể lên tới tối đa 16-bits vậy một chuỗi như ‘Hello world’ đã có thể chiếm hết 88-bits ~ 11-bytes thông qua encoder UTF-8. Trong khi kiểu long là số nguyên to lắm mới chiếm 64-bits. Chưa kể có đôi khi trong ứng dụng thì các chuỗi string giống nhau được sử dụng lặp đi lặp lại rất nhiều lần. Giải pháp được đưa ra là sử dụng một khái niệm gọi là String pool trong đó chứa các string độc lập lẫn nhau (unique) rồi dùng một class String giao tiếp với String pool. Điểm đặc biệt là các String giống nhau sẽ cùng trỏ đến một vị trí thay vì thể hiện giống như các class bình thường (thực thể mới được tạo sẽ chiếm 1 vùng nhớ mới) Điều này giúp cho chương trình tiết kiệm bộ nhớ nhưng bù lại các xử lý sẽ trở nên phức tạp hơn. Vậy cũng chính là lí do từ khoá string vẫn tồn tại trong Java nhưng lại bị khoá lại không thể dùng được.

Một trong những sự phức tạp là bài toán cộng chuỗi. Bởi vì String thì immutable (bất khả chuyển, không thể thay đổi kích thước) dẫn đến để giải bài toán cộng chuỗi thì ta sẽ mô tả một cách không chính xác như sau (chỉ là tinh thần/chiến lược cộng chuỗi)
– Lấy độ dài của chuỗi thứ nhất
– Lấy độ dài của chuỗi thứ hai
– Tìm ra độ dài của chuỗi mới
– Tìm vùng nhớ có độ dài phù hợp
– Sao lưu chuỗi thứ nhất
– Sao lưu chuỗi thứ hai vào ngay sau chuỗi thứ nhất
– Kiểm tra xem chuỗi thứ nhất còn có ai dùng không?
– Nếu không thì để GC giải phóng nó
– Kiểm tra xem chuỗi thứ hai còn có ai dùng không?
– Nếu không thì để GC giải phóng nó
– Tái cấu trúc lại String pool để chống phân mảnh.
=> Quá nhiều bước thuật toán cho một cuộc tình.

Vậy StringBuilder (class tương tự là StringBuffer) ra đời làm cứu cánh cho vấn đề này. Chiến lược được đặt ra là cho đến khi String chính thức được hoàn thành, thì không lưu vào String pool vội mà dùng đến một mảng char[] để lưu tạm bên ngoài, xử lý hoàn toàn các thao tác với string rồi mới build ra một String để cho nó vào String pool. Nhờ vậy mà số lượng bước thuật toán được giảm đi giúp chương trình chạy nhanh hơn trong xử lý chuỗi. Tuy nhiên, vấn đề gặp phải là phải trả giá về không gian lưu trữ.

Ngoài ra, bạn có thể thấy một điều trong danh sách các cấu tử (hàm khởi tạo, constructor…) có một hàm như sau:

public StringBuffer(int capacity) {
value = new char[capacity];
}

Vậy capacity là gì? Về bản chất thì mảng char vẫn là array và mảng này sẽ chịu chung yếu điểm của tất cả các array đó là immutable. Điểm khác biệt hơn so với string trong String pool là array có thể khai báo một kích thước lớn hơn kích thước nó cần. Vì vậy khi cần phải thực hiện cộng chuỗi, nếu kích thước của array vẫn lớn hơn kích thước của chuổi mới, ta chỉ cần copy vào trong là xong. Không cần tìm vùng nhớ khác. Vậy capacity chính là kích thước đầu tiên dùng để khởi tạo nên mảng này.

Tại sao điều này lại quan trọng? Bởi vì bạn khéo chọn capacity phù hợp thì StringBuilder của bạn mới phát huy toàn bộ tác dụng của nó. Còn thế nào là phù hợp thì lại dựa vào kinh nghiệm làm việc của bạn.

Tóm lại, String là đối tượng final. Mọi thay đổi của một chuỗi sẽ tạo ra một chuỗi mới và bản gốc trở thành rác (garbage). Càng nhiều chuỗi được sửa đổi thì càng có nhiều rác xuất hiện. Cuối cùng, nó làm chậm ứng dụng hoặc toàn bộ hệ thống (do các hoạt động Thu gom rác). Do đó, chúng ta sử dụng StringBuilder khi nội dung thường xuyên thay đổi, sử dụng String khi nội dung ít hoặc không thay đổi.