Chuỗi, StringBuffer và StringBuilder


Câu trả lời:


378

Sự khác biệt về tính tương hợp:

Stringbất biến , nếu bạn cố gắng thay đổi giá trị của chúng, một đối tượng khác sẽ được tạo, trong khi đó StringBufferStringBuilderthể thay đổi để chúng có thể thay đổi giá trị của chúng.

Sự khác biệt về an toàn của chủ đề:

Sự khác biệt giữa StringBufferStringBuilderđó StringBufferlà an toàn chủ đề. Vì vậy, khi ứng dụng chỉ cần chạy trong một luồng duy nhất thì tốt hơn là sử dụng StringBuilder. StringBuilderlà hiệu quả hơn StringBuffer.

Tình huống:

  • Nếu chuỗi của bạn sẽ không thay đổi, hãy sử dụng lớp String vì một Stringđối tượng là bất biến.
  • Nếu chuỗi của bạn có thể thay đổi (ví dụ: rất nhiều logic và hoạt động trong quá trình xây dựng chuỗi) và sẽ chỉ được truy cập từ một luồng duy nhất, sử dụng chuỗi StringBuilderlà đủ tốt.
  • Nếu chuỗi của bạn có thể thay đổi và sẽ được truy cập từ nhiều luồng, hãy sử dụng StringBufferStringBuffernó là đồng bộ để bạn có an toàn luồng.

16
Ngoài ra, việc sử dụng Chuỗi cho các hoạt động logic khá chậm và hoàn toàn không được khuyến khích, vì JVM chuyển đổi Chuỗi thành StringBuffer trong mã byte. Rất nhiều chi phí bị lãng phí khi chuyển đổi từ String sang StringBuffer và sau đó quay lại String.
Pieter van Niekerk

2
Vì vậy, Stringskhi chúng ta thay đổi giá trị, một đối tượng khác được tạo ra. Có phải tham chiếu đối tượng cũ bị vô hiệu hóa để nó có thể là rác được thu thập bởi GChoặc nó thậm chí là rác được thu thập?
Harsh Vardhan

@PietervanNiekerk Bạn có ý nghĩa gì với các hoạt động logic?
emlai

Các hoạt động logic tôi muốn nói là các chuỗi cơ bản, Bây giờ có một điều tôi muốn hỏi, như đã nêu bởi @Peter chúng ta nên bắt đầu sử dụng StringBuffer thay vì trên String trong mọi trường hợp hoặc có một số trường hợp cụ thể?
JavaDragon

@bakkal Tôi có thể sử dụng tất cả các phương pháp Stringvới StringBuilder?
roottraveller

47
  • Bạn sử dụng Stringkhi một cấu trúc bất biến là phù hợp; có được một chuỗi ký tự mới từ một Stringhình phạt hiệu năng không thể chấp nhận được, trong thời gian CPU hoặc bộ nhớ (có được các chuỗi con là hiệu quả của CPU vì dữ liệu không được sao chép, nhưng điều này có nghĩa là lượng dữ liệu lớn hơn nhiều có thể vẫn được phân bổ).
  • Bạn sử dụng StringBuilderkhi bạn cần tạo một chuỗi ký tự có thể thay đổi, thường là để ghép một vài chuỗi ký tự lại với nhau.
  • Bạn sử dụng StringBuffertrong cùng một trường hợp bạn sẽ sử dụng StringBuilder, nhưng khi thay đổi chuỗi bên dưới phải được đồng bộ hóa (vì một số luồng đang đọc / sửa đổi bộ đệm chuỗi).

Xem một ví dụ ở đây .


2
súc tích, nhưng không đầy đủ, nó bỏ lỡ lý do cơ bản để sử dụng StringBuilder / Buffer và đó là để giảm hoặc loại bỏ phân bổ lại và sao chép mảng của hành vi nối chuỗi thông thường.

1
"Bạn sử dụng Chuỗi khi bạn đang xử lý các chuỗi bất biến" - không có ý nghĩa. Các trường hợp của Chuỗi là bất biến, vì vậy có lẽ bình luận nên đọc "Sử dụng Chuỗi khi việc sử dụng bộ nhớ do tính không thay đổi không thành vấn đề". Câu trả lời được chấp nhận bao gồm cơ sở của nó khá tốt.
fooMonster

28

Những thứ cơ bản:

Stringlà một lớp bất biến, nó không thể thay đổi. StringBuilderlà một lớp có thể thay đổi có thể được thêm vào, các ký tự được thay thế hoặc xóa và cuối cùng được chuyển đổi thành một String StringBufferlà phiên bản được đồng bộ hóa ban đầu củaStringBuilder

Bạn nên ưu tiên StringBuildertrong mọi trường hợp bạn chỉ có một luồng duy nhất truy cập vào đối tượng của mình.

Chi tiết:

Cũng lưu ý rằng đó StringBuilder/Bufferskhông phải là phép thuật, họ chỉ sử dụng một Array làm đối tượng sao lưu và Array đó phải được phân bổ lại khi bao giờ nó đầy. Hãy chắc chắn và tạo các StringBuilder/Bufferđối tượng của bạn đủ lớn ban đầu, nơi chúng không phải liên tục được định cỡ lại mỗi lần .append()được gọi.

Việc thay đổi kích thước có thể rất thoái hóa. Về cơ bản, nó thay đổi kích thước Mảng sao lưu thành 2 lần kích thước hiện tại của nó mỗi lần nó cần được mở rộng. Điều này có thể dẫn đến một lượng lớn RAM được phân bổ và không được sử dụng khi StringBuilder/Buffercác lớp bắt đầu phát triển lớn.

Trong Java String x = "A" + "B";sử dụng một StringBuilderhậu trường. Vì vậy, đối với các trường hợp đơn giản, không có lợi ích của việc khai báo của riêng bạn. Nhưng nếu bạn đang xây dựng Stringcác đối tượng lớn, giả sử dưới 4k, thì khai báo StringBuilder sb = StringBuilder(4096);sẽ hiệu quả hơn nhiều so với ghép hoặc sử dụng hàm tạo mặc định chỉ có 16 ký tự. Nếu bạn Stringsẽ có ít hơn 10k thì hãy khởi tạo nó với hàm tạo thành 10k để an toàn. Nhưng nếu nó được khởi tạo thành 10k thì bạn viết 1 ký tự hơn 10k, nó sẽ được phân bổ lại và sao chép vào một mảng 20k. Vì vậy, khởi tạo cao là tốt hơn so với thấp.

Trong trường hợp kích thước lại tự động, ở ký tự thứ 17, mảng sao lưu được phân bổ lại và sao chép thành 32 ký tự, ở ký tự thứ 33, điều này lại xảy ra và bạn có thể phân bổ lại và sao chép Mảng thành 64 ký tự. Bạn có thể thấy làm thế nào điều này thoái hóa thành nhiều phân bổ lại và bản sao, đó là những gì bạn thực sự đang cố gắng tránh sử dụng StringBuilder/Bufferở nơi đầu tiên.

Đây là từ mã nguồn JDK 6 cho AbstractStringBuilder

   void expandCapacity(int minimumCapacity) {
    int newCapacity = (value.length + 1) * 2;
        if (newCapacity < 0) {
            newCapacity = Integer.MAX_VALUE;
        } else if (minimumCapacity > newCapacity) {
        newCapacity = minimumCapacity;
    }
        value = Arrays.copyOf(value, newCapacity);
    }

Một thực hành tốt nhất là khởi tạo StringBuilder/Buffermột chút lớn hơn bạn nghĩ bạn sẽ cần nếu bạn không biết ngay lập tức Stringnó sẽ lớn đến mức nào nhưng bạn có thể đoán. Một phân bổ bộ nhớ nhiều hơn một chút so với bạn cần sẽ tốt hơn nhiều phân bổ lại và bản sao.

Ngoài ra, hãy cẩn thận khi khởi tạo a StringBuilder/Buffervới một Stringcái sẽ chỉ phân bổ kích thước của Chuỗi + 16 ký tự, trong hầu hết các trường hợp sẽ chỉ bắt đầu chu trình phân bổ lại và sao chép suy biến mà bạn đang cố tránh. Dưới đây là trực tiếp từ mã nguồn Java 6.

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
    }

Nếu bạn tình cờ kết thúc với một thể hiện StringBuilder/Buffermà bạn không tạo và không thể kiểm soát hàm tạo được gọi, có một cách để tránh hành vi phân bổ lại và sao chép suy biến. Gọi .ensureCapacity()với kích thước bạn muốn để đảm bảo kết quả của bạn Stringsẽ phù hợp với.

Các lựa chọn thay thế:

Cũng như một lưu ý, nếu bạn đang thực hiện việc xây dựng và thao tác thực sự nặng nề String , có một sự thay thế định hướng hiệu suất cao hơn nhiều gọi là Ropes .

Một cách khác, là tạo ra một sự bổ sung StringListbằng cách phân lớp phụ ArrayList<String>và thêm các bộ đếm để theo dõi số lượng ký tự trên mỗi .append()và các hoạt động đột biến khác của danh sách, sau đó ghi đè .toString()để tạo StringBuilderkích thước chính xác bạn cần và lặp qua danh sách và xây dựng đầu ra, bạn thậm chí StringBuildercó thể biến biến đó thành một biến thể và 'bộ đệm' kết quả .toString()và chỉ phải tạo lại nó khi có gì đó thay đổi.

Cũng đừng quên String.format()khi xây dựng đầu ra được định dạng cố định, có thể được tối ưu hóa bởi trình biên dịch vì chúng làm cho nó tốt hơn.


1
String x = "A" + "B";thực sự biên dịch để trở thành một StringBuilder? Tại sao nó không chỉ biên dịch String x = "AB";, nó chỉ nên sử dụng StringBuilder nếu các thành phần không được biết đến vào thời gian biên dịch.
Matt Greer

nó có thể tối ưu hóa các hằng chuỗi, tôi không thể nhớ lại lần trước đã dịch ngược mã byte, nhưng tôi biết rằng nếu có bất kỳ biến nào trong đó thì chắc chắn sẽ sử dụng triển khai StringBuilder. Bạn có thể tải xuống mã nguồn JDK và tự tìm hiểu. "A" + "B" là một ví dụ rõ ràng.

Tôi đã tự hỏi về String.format (). Tôi chưa bao giờ thực sự thấy nó được sử dụng trong các dự án. Thông thường đó là StringBuilder. Chà, thông thường nó thực sự là "A" + "B" + "C" vì mọi người lười biếng;) Tôi có xu hướng luôn sử dụng StringBuilder, ngay cả khi chỉ có hai chuỗi được nối, bởi vì trong tương lai, có lẽ nhiều chuỗi sẽ được nối thêm . Tôi chưa bao giờ sử dụng String.format () chủ yếu vì tôi không bao giờ nhớ JDK được giới thiệu là gì - Tôi thấy đó là JDK1.5, tôi sẽ sử dụng nó để ủng hộ các tùy chọn khác.
jamiebarrow

9

Bạn có nghĩa là, để nối?

Ví dụ về thế giới thực: Bạn muốn tạo một chuỗi mới từ nhiều người khác .

Ví dụ để gửi tin nhắn:

Chuỗi

String s = "Dear " + user.name + "<br>" + 
" I saw your profile and got interested in you.<br>" +
" I'm  " + user.age + "yrs. old too"

StringBuilder

String s = new StringBuilder().append.("Dear ").append( user.name ).append( "<br>" ) 
          .append(" I saw your profile and got interested in you.<br>") 
          .append(" I'm  " ).append( user.age ).append( "yrs. old too")
          .toString()

Hoặc là

String s = new StringBuilder(100).appe..... etc. ...
// The difference is a size of 100 will be allocated upfront as  fuzzy lollipop points out.

StringBuffer (cú pháp chính xác như với StringBuilder, các hiệu ứng khác nhau)

Trong khoảng

StringBuffer so với StringBuilder

Cái trước được đồng bộ hóa và sau này thì không.

Vì vậy, nếu bạn gọi nó nhiều lần trong một luồng (chiếm 90% các trường hợp), StringBuildersẽ chạy nhanh hơn nhiều vì nó sẽ không dừng lại để xem liệu nó có sở hữu khóa luồng không.

Vì vậy, nên sử dụng StringBuilder(trừ khi tất nhiên bạn có nhiều hơn một luồng truy cập vào nó cùng một lúc, điều này rất hiếm)

Stringghép nối ( sử dụng toán tử + ) có thể được trình biên dịch tối ưu hóa để sử dụng StringBuilderbên dưới, do đó, nó không còn là điều đáng lo ngại, trong những ngày xưa của Java, đây là điều mà mọi người đều nên tránh bằng mọi giá, bởi vì mọi sự kết hợp đã tạo một đối tượng String mới. Trình biên dịch hiện đại không làm điều này nữa, nhưng vẫn là một cách tốt để sử dụng StringBuilderthay vì chỉ trong trường hợp bạn sử dụng trình biên dịch "cũ".

biên tập

Chỉ dành cho những ai tò mò, đây là những gì trình biên dịch làm cho lớp này:

class StringConcatenation {
    int x;
    String literal = "Value is" + x;
    String builder = new StringBuilder().append("Value is").append(x).toString();
}

javap -c StringConcatenation

Compiled from "StringConcatenation.java"
class StringConcatenation extends java.lang.Object{
int x;

java.lang.String literal;

java.lang.String builder;

StringConcatenation();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   new #2; //class java/lang/StringBuilder
   8:   dup
   9:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   12:  ldc #4; //String Value is
   14:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  aload_0
   18:  getfield    #6; //Field x:I
   21:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   24:  invokevirtual   #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   27:  putfield    #9; //Field literal:Ljava/lang/String;
   30:  aload_0
   31:  new #2; //class java/lang/StringBuilder
   34:  dup
   35:  invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   38:  ldc #4; //String Value is
   40:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   43:  aload_0
   44:  getfield    #6; //Field x:I
   47:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   50:  invokevirtual   #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   53:  putfield    #10; //Field builder:Ljava/lang/String;
   56:  return

}

Các dòng được đánh số 5 - 27 dành cho Chuỗi có tên là "bằng chữ"

Các dòng được đánh số 31-53 dành cho Chuỗi có tên "trình xây dựng"

Không có sự khác biệt, chính xác cùng một mã được thực thi cho cả hai chuỗi.


2
đây là một ví dụ thực sự xấu Khởi tạo StringBuilder bằng "Dear" có nghĩa là .append () đầu tiên sẽ gây ra sự phân bổ lại và sao chép. Hoàn toàn phủ nhận bất kỳ hiệu quả nào mà bạn đang cố gắng đạt được trong việc ghép nối "bình thường". Một ví dụ tốt hơn sẽ là tạo nó với kích thước ban đầu sẽ chứa toàn bộ nội dung của Chuỗi cuối cùng.

1
Nói chung KHÔNG phải là một cách thực hành tốt để sử dụng StringBuilderphép nối Chuỗi để thực hiện ở phía bên phải của bài tập. Bất kỳ triển khai tốt nào cũng sẽ sử dụng StringBuilder đằng sau hậu trường như bạn nói. Hơn nữa, ví dụ của bạn "a" + "b"sẽ được tổng hợp thành một nghĩa đen duy nhất "ab"nhưng nếu bạn sử dụng StringBuildernó sẽ dẫn đến hai cuộc gọi không cần thiết đến append().
Mark Peters

@Mark Tôi không có ý sử dụng "a"+"b"nhưng để nói phần nối chuỗi là gì về việc tôi thay đổi nó thành rõ ràng. Những gì bạn không nói là, tại sao không phải là một thực hành tốt để làm điều đó. Đó chính xác là những gì (một trình biên dịch) hiện đại làm. @fuzzy, tôi đồng ý, đặc biệt khi bạn biết kích thước của chuỗi cuối cùng sẽ là gì (aprox).
OscarRyz

1
Tôi không nghĩ rằng đó là thực tế đặc biệt xấu , nhưng tôi chắc chắn sẽ không muốn bất kỳ đề nghị nào để làm điều đó nói chung. Nó khó hiểu và khó đọc, và quá dài dòng. Thêm vào đó, nó khuyến khích những sai lầm như của bạn khi bạn chia ra hai chữ có thể được biên dịch thành một. Chỉ khi hồ sơ nói với tôi nó làm cho một sự khác biệt tôi sẽ làm theo cách của bạn.
Đánh dấu Peters

@Mark Có rồi. Tôi đã suy nghĩ nhiều hơn trong "đoạn mã lớn như mẫu" và không thực sự trong mỗi chuỗi ký tự thông thường. Nhưng vâng, tôi đồng ý, vì ngày nay họ làm điều tương tự, điều đó không có ý nghĩa (10 năm trước là lý do để từ chối thay đổi trong sửa đổi mã) :)
OscarRyz

8
-------------------------------------------------- --------------------------------
                  Chuỗi StringBuffer StringBuilder
-------------------------------------------------- --------------------------------                 
Khu vực lưu trữ | Chuỗi liên tục Heap Heap
Có thể sửa đổi | Không (không thay đổi) Có (có thể thay đổi) Có (có thể thay đổi)
Chủ đề an toàn | Có Có Không
 Hiệu suất | Nhanh Rất chậm Nhanh
-------------------------------------------------- --------------------------------

Tại sao hiệu suất String nhanh và hiệu suất StringBuffer rất chậm?
gaurav

1
@gaurav bạn có thể đọc mã nguồn của nó, tất cả các phương thức trong StringBuffer là synchronisedvà đó là lý do .
Nghe

8

Chuỗi họ

Chuỗi

Các String classchuỗi ký tự đại diện. Tất cả các chuỗi ký tự trong chương trình Java, chẳng hạn như "abc"được triển khai như các thể hiện của lớp này.

Các đối tượng chuỗi là bất biến khi chúng được tạo, chúng ta không thể thay đổi. ( Chuỗi là hằng số )

  • Nếu một String được tạo ra sử dụng constructor hoặc phương pháp sau đó những chuỗi sẽ được lưu trữ trong bộ nhớ heap cũng như SringConstantPool. Nhưng trước khi lưu trong pool, nó gọi intern()phương thức để kiểm tra tính khả dụng của đối tượng với cùng một nội dung trong pool bằng phương thức equals. Nếu String-copy có sẵn trong Pool thì trả về tham chiếu. Mặt khác, đối tượng String được thêm vào nhóm và trả về tham chiếu.

    • Ngôn ngữ Java cung cấp hỗ trợ đặc biệt cho toán tử nối chuỗi ( +) và để chuyển đổi các đối tượng khác thành chuỗi. Nối chuỗi được thực hiện thông qua lớp StringBuilder (hoặc StringBuffer) và phương thức nối thêm của nó.

    String heapSCP = new String("Yash");
    heapSCP.concat(".");
    heapSCP = heapSCP + "M";
    heapSCP = heapSCP + 777;
    
    // For Example: String Source Code 
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
  • Chuỗi ký tự được lưu trữ trong StringConstantPool.

    String onlyPool = "Yash";

StringBuilderStringBuffer là chuỗi ký tự có thể thay đổi. Điều đó có nghĩa là người ta có thể thay đổi giá trị của các đối tượng này. StringBuffer có các phương thức giống như StringBuilder, nhưng mỗi phương thức trong StringBuffer được đồng bộ hóa nên nó là luồng an toàn.

  • Dữ liệu StringBuffer và StringBuilder chỉ có thể được tạo bằng toán tử mới. Vì vậy, chúng được lưu trữ trong bộ nhớ Heap.

  • Các trường hợp của StringBuilder không an toàn để sử dụng bởi nhiều luồng. Nếu cần đồng bộ hóa như vậy thì nên sử dụng StringBuffer.

    StringBuffer threadSafe = new StringBuffer("Yash");
    threadSafe.append(".M");
    threadSafe.toString();
    
    StringBuilder nonSync = new StringBuilder("Yash");
    nonSync.append(".M");
    nonSync.toString();
  • StringBuffer và StringBuilder đang có một phương thức đặc biệt như., replace(int start, int end, String str)reverse().

    LƯU Ý : StringBuffer và SringBuilder có thể thay đổi khi chúng cung cấp việc triển khai Appendable Interface.


Khi nào nên dùng cái nào.

  • Nếu bạn sẽ không thay đổi giá trị mỗi lần thì tốt hơn nên sử dụng String Class. Là một phần của Generics nếu bạn muốn Sắp xếp Comparable<T>hoặc so sánh một giá trị thì hãy tìm String Class.

    //ClassCastException: java.lang.StringBuffer cannot be cast to java.lang.Comparable
    Set<StringBuffer> set = new TreeSet<StringBuffer>();
    set.add( threadSafe );
    System.out.println("Set : "+ set);
  • Nếu bạn định sửa đổi giá trị mỗi lần sử dụng StringBuilder nhanh hơn StringBuffer. Nếu nhiều luồng đang sửa đổi giá trị, hãy truy cập StringBuffer.


4

Ngoài ra, StringBufferlà an toàn chủ đề, mà StringBuilderkhông.

Vì vậy, trong một tình huống thời gian thực khi các luồng khác nhau đang truy cập vào nó, StringBuildercó thể có một kết quả không xác định.


3

Lưu ý rằng nếu bạn đang sử dụng Java 5 hoặc mới hơn, bạn nên sử dụng StringBuilderthay vì StringBuffer. Từ tài liệu API:

Khi phát hành JDK 5, lớp này đã được bổ sung một lớp tương đương được thiết kế để sử dụng bởi một luồng duy nhất , StringBuilder. Các StringBuilderlớp học nói chung nên được sử dụng trong ưu tiên cho một này, vì nó hỗ trợ tất cả các hoạt động tương tự, nhưng nó là nhanh hơn, vì nó thực hiện không đồng bộ.

Trong thực tế, bạn sẽ hầu như không bao giờ sử dụng điều này từ nhiều luồng cùng một lúc, vì vậy việc đồng bộ hóa StringBuffergần như luôn luôn là không cần thiết.


3

Cá nhân, tôi không nghĩ có bất kỳ thế giới thực nào được sử dụng cho StringBuffer. Khi nào tôi muốn giao tiếp giữa nhiều luồng bằng cách thao tác một chuỗi ký tự? Điều đó không có vẻ hữu ích chút nào, nhưng có lẽ tôi vẫn chưa thấy ánh sáng :)


3

Sự khác biệt giữa String và hai lớp còn lại là String là bất biến và hai lớp còn lại là các lớp có thể thay đổi.

Nhưng tại sao chúng ta có hai lớp cho cùng một mục đích?

Lý do là đó StringBufferlà chủ đề an toàn và StringBuilderkhông. StringBuilderlà một lớp mới StringBuffer Apivà nó đã được giới thiệu JDK5và luôn được khuyến nghị nếu bạn đang làm việc trong một môi trường luồng đơn vì nó nhiềuFaster

Để biết chi tiết đầy đủ, bạn có thể đọc http://www.codingeek.com/java/opesbuilder-and-opesbuffer-a-way-to-create-mutable-strings-in-java/


2

Trong java, String là bất biến. Trở nên bất biến, chúng tôi muốn nói rằng một khi Chuỗi được tạo, chúng tôi không thể thay đổi giá trị của nó. StringBuffer có thể thay đổi. Khi một đối tượng StringBuffer được tạo, chúng ta chỉ cần nối nội dung vào giá trị của đối tượng thay vì tạo một đối tượng mới. StringBuilder tương tự StringBuffer nhưng nó không an toàn cho chủ đề. Các phương thức của StingBuilder không được đồng bộ hóa nhưng so với các Chuỗi khác, Stringbuilder chạy nhanh nhất. Bạn có thể tìm hiểu sự khác biệt giữa String, StringBuilder và StringBuffer bằng cách triển khai chúng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.