Tại sao String là bất biến trong Java?


78

Tôi không thể hiểu lý do của nó. Tôi luôn sử dụng lớp String như các nhà phát triển khác, nhưng khi tôi sửa đổi giá trị của nó, phiên bản mới của String đã được tạo.

Điều gì có thể là lý do bất biến cho lớp String trong Java?

Tôi biết có một số lựa chọn thay thế như StringBuffer hoặc StringBuilder. Đó chỉ là sự tò mò.


20
Về mặt kỹ thuật, nó không phải là một bản sao, nhưng Eric Lippert đưa ra một câu trả lời tuyệt vời cho câu hỏi này tại đây: lập trình
viên.stackexchange.com/a/190913/33843

Câu trả lời:


105

Đồng thời

Java đã được định nghĩa ngay từ đầu với những cân nhắc về sự tương tranh. Như thường được đề cập chia sẻ đột biến là có vấn đề. Một điều có thể thay đổi khác đằng sau mặt sau của một chủ đề khác mà không nhận ra chủ đề đó.

Có một loạt các lỗi C ++ đa luồng đã xuất hiện do một chuỗi được chia sẻ - trong đó một mô-đun nghĩ rằng nó an toàn để thay đổi khi một mô-đun khác trong mã đã lưu một con trỏ vào nó và hy vọng nó sẽ giữ nguyên.

"Giải pháp" cho vấn đề này là mọi lớp tạo ra một bản sao phòng thủ của các vật thể đột biến được truyền cho nó. Đối với các chuỗi có thể thay đổi, đây là O (n) để tạo bản sao. Đối với các chuỗi bất biến, tạo một bản sao là O (1) vì nó không phải là một bản sao, cùng một đối tượng không thể thay đổi.

Trong một môi trường đa luồng, các đối tượng bất biến luôn có thể được chia sẻ an toàn với nhau. Điều này dẫn đến việc giảm tổng thể sử dụng bộ nhớ và cải thiện bộ nhớ đệm.

Bảo vệ

Nhiều lần các chuỗi được truyền xung quanh dưới dạng đối số cho các nhà xây dựng - các kết nối mạng và các nguyên mẫu là hai thứ dễ dàng xuất hiện nhất trong tâm trí. Có thể thay đổi điều này tại một thời điểm không xác định sau đó trong quá trình thực thi có thể dẫn đến các vấn đề bảo mật (chức năng nghĩ rằng nó đang kết nối với một máy, nhưng đã được chuyển hướng sang một máy khác, nhưng mọi thứ trong đối tượng trông giống như được kết nối với ... nó thậm chí cùng một chuỗi).

Java cho phép một người sử dụng sự phản chiếu - và các tham số cho điều này là các chuỗi. Sự nguy hiểm của việc truyền một chuỗi có thể được sửa đổi thông qua cách này đến một phương thức khác phản ánh. Thật tồi tệ.

Chìa khóa để băm

Bảng băm là một trong những cấu trúc dữ liệu được sử dụng nhiều nhất. Các khóa cho cấu trúc dữ liệu là rất thường xuyên chuỗi. Có các chuỗi bất biến có nghĩa là (như trên) bảng băm không cần tạo một bản sao của khóa băm mỗi lần. Nếu các chuỗi có thể thay đổi và bảng băm không tạo ra điều này, thì có thể có thứ gì đó để thay đổi khóa băm ở khoảng cách xa.

Cách mà Object trong java hoạt động, là mọi thứ đều có khóa băm (được truy cập thông qua phương thức hashCode ()). Có một chuỗi bất biến có nghĩa là hashCode có thể được lưu trữ. Xem xét tần suất sử dụng Chuỗi làm khóa cho hàm băm, điều này mang lại hiệu suất tăng đáng kể (thay vì phải tính toán lại mã băm mỗi lần).

Chất nền

Bằng cách có Chuỗi là bất biến, mảng ký tự bên dưới hỗ trợ cấu trúc dữ liệu cũng là bất biến. Điều này cho phép tối ưu hóa nhất định về substringphương pháp được thực hiện (chúng không nhất thiết phải được thực hiện - nó cũng giới thiệu khả năng bị rò rỉ bộ nhớ).

Nếu bạn làm:

String foo = "smiles";
String bar = foo.substring(1,5);

Giá trị của barlà 'dặm'. Tuy nhiên, cả hai foobarcó thể được hỗ trợ bởi cùng một mảng ký tự, làm giảm khả năng khởi tạo của nhiều mảng ký tự hoặc sao chép nó - chỉ sử dụng các điểm bắt đầu và kết thúc khác nhau trong chuỗi.

foo | | (0, 6)
    vv
    những nụ cười
     ^ ^
thanh | | (1, 5)

Bây giờ, nhược điểm của điều đó (rò rỉ bộ nhớ) là nếu một người có chuỗi dài 1k và lấy chuỗi con của ký tự thứ nhất và thứ hai, thì nó cũng sẽ được hỗ trợ bởi mảng ký tự dài 1k. Mảng này sẽ vẫn còn trong bộ nhớ ngay cả khi chuỗi ban đầu có giá trị của toàn bộ mảng ký tự là rác được thu thập.

Mọi người có thể thấy điều này trong Chuỗi từ JDK 6b14 (đoạn mã sau lấy từ nguồn GPL v2 và được sử dụng làm ví dụ)

   public String(char value[], int offset, int count) {
       if (offset < 0) {
           throw new StringIndexOutOfBoundsException(offset);
       }
       if (count < 0) {
           throw new StringIndexOutOfBoundsException(count);
       }
       // Note: offset or count might be near -1>>>1.
       if (offset > value.length - count) {
           throw new StringIndexOutOfBoundsException(offset + count);
       }
       this.offset = 0;
       this.count = count;
       this.value = Arrays.copyOfRange(value, offset, offset+count);
   }

   // Package private constructor which shares value array for speed.
   String(int offset, int count, char value[]) {
       this.value = value;
       this.offset = offset;
       this.count = count;
   }

   public String substring(int beginIndex, int endIndex) {
       if (beginIndex < 0) {
           throw new StringIndexOutOfBoundsException(beginIndex);
       }
       if (endIndex > count) {
           throw new StringIndexOutOfBoundsException(endIndex);
       }
       if (beginIndex > endIndex) {
           throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
       }
       return ((beginIndex == 0) && (endIndex == count)) ? this :
           new String(offset + beginIndex, endIndex - beginIndex, value);
   }

Lưu ý cách chuỗi con sử dụng hàm tạo Chuỗi cấp gói không liên quan đến bất kỳ sự sao chép nào của mảng và sẽ nhanh hơn nhiều (với chi phí có thể giữ xung quanh một số mảng lớn - mặc dù không sao chép các mảng lớn).

Xin lưu ý rằng đoạn mã trên dành cho Java 1.6. Cách trình xây dựng chuỗi con được triển khai đã được thay đổi với Java 1.7 như được ghi lại trong biểu diễn bên trong Thay đổi thành Chuỗi được thực hiện trong Java 1.7.0_06 - vấn đề gây ra rò rỉ bộ nhớ mà tôi đã đề cập ở trên. Java có thể không được coi là một ngôn ngữ có nhiều thao tác Chuỗi và vì vậy việc tăng hiệu năng cho một chuỗi con là một điều tốt. Bây giờ, với các tài liệu XML khổng lồ được lưu trữ trong các chuỗi không bao giờ được thu thập, điều này trở thành một vấn đề ... và do đó, việc thay đổi Stringkhông sử dụng cùng một mảng bên dưới với một chuỗi con, để mảng ký tự lớn hơn có thể được thu thập nhanh hơn.

Đừng lạm dụng Stack

Người ta có thể chuyển giá trị của chuỗi xung quanh thay vì tham chiếu đến chuỗi bất biến để tránh các vấn đề có tính biến đổi. Tuy nhiên, với các chuỗi lớn, việc chuyển chuỗi này trên ngăn xếp sẽ ... bị lạm dụng đối với hệ thống (đặt toàn bộ tài liệu xml dưới dạng chuỗi trên ngăn xếp và sau đó gỡ chúng ra hoặc tiếp tục chuyển chúng theo ...).

Khả năng trùng lặp

Được cho rằng, đây không phải là động lực ban đầu cho lý do tại sao Chuỗi nên bất biến, nhưng khi người ta nhìn vào lý do tại sao Chuỗi bất biến là một điều tốt, đây chắc chắn là điều cần xem xét.

Bất cứ ai từng làm việc với String một chút đều biết rằng họ có thể hút bộ nhớ. Điều này đặc biệt đúng khi bạn đang làm những việc như lấy dữ liệu từ cơ sở dữ liệu trong một thời gian ngắn. Nhiều lần với các stings này, chúng là cùng một chuỗi lặp đi lặp lại (một lần cho mỗi hàng).

Nhiều ứng dụng Java quy mô lớn hiện đang bị tắc nghẽn trong bộ nhớ. Các phép đo đã chỉ ra rằng khoảng 25% tập dữ liệu trực tiếp của vùng heap Java trong các loại ứng dụng này được sử dụng bởi các đối tượng String. Hơn nữa, khoảng một nửa trong số các đối tượng Chuỗi đó là trùng lặp, trong đó trùng lặp có nghĩa là chuỗi1.equals (chuỗi2) là đúng. Có các đối tượng String trùng lặp trên heap, về cơ bản, chỉ là một sự lãng phí bộ nhớ. ...

Với bản cập nhật Java 8 20, JEP 192 (động lực được trích dẫn ở trên) đang được triển khai để giải quyết vấn đề này. Không đi sâu vào chi tiết về cách thức sao chép chuỗi hoạt động, điều cần thiết là bản thân các Chuỗi là bất biến. Bạn không thể sao chép StringBuilders vì chúng có thể thay đổi và bạn không muốn ai đó thay đổi thứ gì đó từ bên dưới bạn. Chuỗi không thay đổi (liên quan đến nhóm Chuỗi đó) có nghĩa là bạn có thể đi qua và nếu bạn tìm thấy hai chuỗi giống nhau, bạn có thể trỏ một chuỗi tham chiếu đến chuỗi khác và để trình thu gom rác tiêu thụ chuỗi không sử dụng.

Những ngôn ngữ khác

Mục tiêu C (có trước Java) có NSStringNSMutableString.

C # và .NET đã đưa ra các lựa chọn thiết kế giống nhau của chuỗi mặc định là không thay đổi.

Dây Lua cũng bất biến.

Python cũng vậy.

Trong lịch sử, Lisp, Scheme, Smalltalk đều thực hiện chuỗi và do đó, nó là bất biến. Các ngôn ngữ động hiện đại hơn thường sử dụng các chuỗi theo một cách nào đó đòi hỏi chúng phải bất biến (nó có thể không phải là Chuỗi , nhưng nó là bất biến).

Phần kết luận

Những cân nhắc thiết kế này đã được thực hiện lặp đi lặp lại trong vô số ngôn ngữ. Đó là sự đồng thuận chung rằng các chuỗi bất biến, cho tất cả sự lúng túng của họ, tốt hơn so với các lựa chọn thay thế và dẫn đến mã tốt hơn (ít lỗi hơn) và tổng thể thực thi nhanh hơn.


3
Java cung cấp các chuỗi có thể thay đổi và bất biến. Câu trả lời này nêu chi tiết một số lợi thế về hiệu suất có thể được thực hiện trên các chuỗi bất biến và một số lý do người ta có thể chọn dữ liệu bất biến; nhưng không thảo luận tại sao phiên bản bất biến là phiên bản mặc định.
Billy ONeal

3
@BillyONeal: một mặc định an toàn và một sự thay thế không an toàn hầu như luôn dẫn đến các hệ thống an toàn hơn so với cách tiếp cận ngược lại.
Joachim Sauer

4
@BillyONeal Nếu bất biến không phải là mặc định thì các vấn đề về đồng thời, bảo mật và băm sẽ phổ biến hơn. Các nhà thiết kế ngôn ngữ đã chọn (một phần để đáp ứng với C) để tạo ra một ngôn ngữ nơi các mặc định được thiết lập để cố gắng ngăn chặn một số lỗi phổ biến để cố gắng cải thiện hiệu quả của lập trình viên (không phải lo lắng về các lỗi này nữa). Có ít lỗi hơn (rõ ràng và ẩn) với các chuỗi bất biến so với các chuỗi có thể thay đổi.

@Joachim: Tôi không yêu cầu khác.
Billy ONeal

1
Về mặt kỹ thuật, Common Lisp có các chuỗi có thể thay đổi, cho các hoạt động và biểu tượng "giống như chuỗi" với các tên bất biến cho các định danh bất biến.
Vatine

21

Những lý do tôi có thể nhớ lại:

  1. Cơ sở chuỗi Pool mà không tạo chuỗi bất biến là hoàn toàn không thể bởi vì trong trường hợp chuỗi chuỗi, một đối tượng chuỗi / chữ, ví dụ "XYZ" sẽ được tham chiếu bởi nhiều biến tham chiếu, do đó, nếu bất kỳ một trong số chúng thay đổi giá trị, các giá trị khác sẽ tự động bị ảnh hưởng .

  2. Chuỗi đã được sử dụng rộng rãi làm tham số cho nhiều lớp java, ví dụ như để mở kết nối mạng, để mở kết nối cơ sở dữ liệu, mở tệp. Nếu String không bất biến, điều này sẽ dẫn đến mối đe dọa bảo mật nghiêm trọng.

  3. Tính không thay đổi cho phép String lưu trữ mã băm của nó.

  4. Làm cho nó an toàn chủ đề.


7

1) Chuỗi bể bơi

Nhà thiết kế Java biết rằng String sẽ là loại dữ liệu được sử dụng nhiều nhất trong tất cả các loại ứng dụng Java và đó là lý do tại sao họ muốn tối ưu hóa từ đầu. Một trong những bước quan trọng trên hướng đó là ý tưởng lưu trữ chuỗi ký tự chuỗi trong nhóm Chuỗi. Mục tiêu là giảm đối tượng Chuỗi tạm thời bằng cách chia sẻ chúng và để chia sẻ, chúng phải xuất phát từ lớp Bất biến. Bạn không thể chia sẻ một đối tượng có thể thay đổi với hai bên mà không biết nhau. Hãy lấy một ví dụ giả thuyết, trong đó hai biến tham chiếu đang trỏ đến cùng một đối tượng Chuỗi:

String s1 = "Java";
String s2 = "Java";

Bây giờ nếu s1 thay đổi đối tượng từ "Java" thành "C ++", biến tham chiếu cũng có giá trị s2 = "C ++", điều mà nó thậm chí không biết về nó. Bằng cách làm cho String không thay đổi, việc chia sẻ chuỗi ký tự này là có thể. Nói tóm lại, ý tưởng chính về nhóm Chuỗi không thể được thực hiện mà không làm cho Chuỗi cuối cùng hoặc Không thể thay đổi trong Java.

2) Bảo mật

Java có mục tiêu rõ ràng về mặt cung cấp một môi trường an toàn ở mọi cấp độ dịch vụ và String rất quan trọng trong toàn bộ nội dung bảo mật đó. Chuỗi đã được sử dụng rộng rãi làm tham số cho nhiều lớp Java, ví dụ để mở kết nối mạng, bạn có thể truyền máy chủ và cổng dưới dạng Chuỗi, để đọc tệp trong Java, bạn có thể truyền đường dẫn của tệp và thư mục dưới dạng Chuỗi và để mở kết nối cơ sở dữ liệu, bạn có thể vượt qua URL cơ sở dữ liệu dưới dạng Chuỗi. Nếu String không phải là bất biến, người dùng có thể đã được cấp quyền truy cập vào một tệp cụ thể trong hệ thống, nhưng sau khi xác thực, anh ta có thể thay đổi PATH thành một thứ khác, điều này có thể gây ra vấn đề bảo mật nghiêm trọng. Tương tự, trong khi kết nối với cơ sở dữ liệu hoặc bất kỳ máy nào khác trong mạng, việc thay đổi giá trị Chuỗi có thể gây ra các mối đe dọa bảo mật. Chuỗi có thể thay đổi cũng có thể gây ra vấn đề bảo mật trong Reflection,

3) Sử dụng chuỗi trong cơ chế tải lớp

Một lý do khác để làm cho String cuối cùng hoặc bất biến được thúc đẩy bởi thực tế là nó được sử dụng rất nhiều trong cơ chế tải lớp. Vì String không phải là bất biến, kẻ tấn công có thể lợi dụng thực tế này và yêu cầu tải các lớp Java tiêu chuẩn, ví dụ java.io.Reader có thể được thay đổi thành lớp độc hại com.unknown.DataStolenReader. Bằng cách giữ cho Chuỗi cuối cùng và không thay đổi, ít nhất chúng ta có thể chắc chắn rằng JVM đang tải các lớp chính xác.

4) Lợi ích đa luồng

Do Đồng thời và Đa luồng là cung cấp khóa của Java, nên rất có ý nghĩa khi nghĩ về an toàn luồng của các đối tượng Chuỗi. Vì người ta hy vọng rằng String sẽ được sử dụng rộng rãi, làm cho nó bất biến có nghĩa là không có đồng bộ hóa bên ngoài, có nghĩa là mã sạch hơn nhiều liên quan đến việc chia sẻ Chuỗi giữa nhiều luồng. Tính năng duy nhất này, làm cho việc mã hóa đồng thời trở nên phức tạp, khó hiểu và dễ bị lỗi hơn nhiều. Bởi vì String là bất biến và chúng tôi chỉ chia sẻ nó giữa các luồng, dẫn đến mã dễ đọc hơn.

5) Tối ưu hóa và hiệu suất

Bây giờ khi bạn tạo một lớp bất biến, bạn sẽ biết trước rằng, lớp này sẽ không thay đổi một khi được tạo. Điều này đảm bảo đường dẫn mở cho nhiều tối ưu hóa hiệu suất, ví dụ như bộ nhớ đệm. Bản thân String biết rằng, tôi sẽ không thay đổi, vì vậy String lưu trữ mã băm của nó. Nó thậm chí còn tính toán mã băm một cách lười biếng và một khi được tạo, chỉ cần lưu trữ nó. Trong thế giới đơn giản, khi bạn lần đầu tiên gọi phương thức hashCode () của bất kỳ đối tượng String nào, nó sẽ tính toán mã băm và tất cả các lệnh gọi tiếp theo tới hàm hashCode () đã được tính toán, giá trị được lưu trong bộ nhớ cache. Điều này dẫn đến tăng hiệu suất tốt, Chuỗi đã cho được sử dụng nhiều trong Bản đồ dựa trên hàm băm, ví dụ Hashtable và HashMap. Bộ nhớ đệm của mã băm là không thể nếu không biến nó thành bất biến và cuối cùng, vì nó phụ thuộc vào nội dung của chính Chuỗi.


5

Máy ảo Java thực hiện một số tối ưu hóa liên quan đến các hoạt động chuỗi không thể được thực hiện theo cách khác. Ví dụ: nếu bạn có một chuỗi có giá trị "Mississippi" và bạn đã gán "Mississippi" .sub chuỗi (0, 4) cho một chuỗi khác, theo như bạn biết, một bản sao được tạo từ bốn ký tự đầu tiên để tạo thành "Miss" . Điều bạn không biết là cả hai đều chia sẻ cùng một chuỗi gốc "Mississippi" với một người là chủ sở hữu và người kia là tham chiếu của chuỗi đó từ vị trí 0 đến 4. (Tham chiếu đến chủ sở hữu ngăn chủ sở hữu khỏi bị thu thập bởi người thu gom rác khi chủ sở hữu đi ra khỏi phạm vi)

Điều này là không quan trọng đối với một chuỗi nhỏ như "Mississippi", nhưng với các chuỗi lớn hơn và nhiều thao tác, không phải sao chép chuỗi là một trình tiết kiệm thời gian lớn! Nếu các chuỗi có thể thay đổi, thì bạn không thể làm điều này, vì sửa đổi bản gốc cũng sẽ ảnh hưởng đến "bản sao" của chuỗi con.

Ngoài ra, như Donal đề cập, lợi thế sẽ bị đè nặng rất nhiều bởi nhược điểm của nó. Hãy tưởng tượng rằng bạn viết một chương trình phụ thuộc vào thư viện và bạn sử dụng hàm trả về một chuỗi. Làm thế nào bạn có thể chắc chắn rằng giá trị đó sẽ không đổi? Để đảm bảo không có điều đó xảy ra, bạn luôn phải tạo một bản sao.

Điều gì nếu bạn có hai luồng chia sẻ cùng một chuỗi? Bạn sẽ không muốn đọc một chuỗi hiện đang được viết lại bởi một chủ đề khác, phải không? Do đó, chuỗi phải là luồng an toàn, là lớp phổ biến mà nó có, sẽ làm cho hầu như mọi chương trình Java chậm hơn nhiều. Mặt khác, bạn phải tạo một bản sao cho mọi luồng yêu cầu chuỗi đó hoặc bạn sẽ phải đặt mã bằng chuỗi đó trong một khối đồng bộ hóa, cả hai đều chỉ làm chậm chương trình của bạn.

Vì tất cả những lý do này, đó là một trong những quyết định ban đầu được đưa ra cho Java để phân biệt chính nó với C ++.


Về mặt lý thuyết, bạn có thể thực hiện quản lý bộ đệm nhiều lớp cho phép sao chép trên đột biến nếu được chia sẻ, nhưng rất khó để thực hiện công việc hiệu quả trong môi trường đa luồng.
Donal Fellows

@DonalFellows Tôi chỉ giả định rằng Máy ảo Java không được viết bằng Java (rõ ràng), nên nó được quản lý nội bộ bằng cách sử dụng các con trỏ được chia sẻ hoặc một cái gì đó tương tự.
Neil

5

Lý do cho tính bất biến của chuỗi xuất phát từ tính nhất quán với các loại nguyên thủy khác trong ngôn ngữ. Nếu bạn có intchứa giá trị 42 và bạn thêm giá trị 1 vào giá trị đó, bạn không thay đổi 42. Bạn nhận được một giá trị mới, 43, hoàn toàn không liên quan đến các giá trị bắt đầu. Các nguyên thủy đột biến khác với chuỗi không có ý nghĩa khái niệm; và như các chương trình coi các chuỗi là bất biến thường dễ dàng hơn để lý giải và hiểu.

Hơn nữa, Java thực sự cung cấp cả chuỗi có thể thay đổi và bất biến, như bạn thấy với StringBuilder; thực sự, chỉ có mặc định là chuỗi bất biến. Nếu bạn muốn chuyển các tài liệu tham khảo đến StringBuildermọi nơi, bạn hoàn toàn có thể làm như vậy. Java sử dụng các loại riêng biệt ( StringStringBuilder) cho các khái niệm này bởi vì nó không hỗ trợ để thể hiện tính biến đổi hoặc thiếu trong hệ thống loại của nó. Trong các ngôn ngữ có hỗ trợ tính bất biến trong các hệ thống loại của chúng (ví dụ: C ++ const), thường có một loại chuỗi duy nhất phục vụ cả hai mục đích.

Có, việc có chuỗi là bất biến cho phép người ta thực hiện một số tối ưu hóa cụ thể cho các chuỗi bất biến, chẳng hạn như thực tập và cho phép truyền tham chiếu chuỗi xung quanh mà không cần đồng bộ hóa qua các luồng. Tuy nhiên, điều này làm lẫn lộn cơ chế với mục tiêu dự định của một ngôn ngữ với một hệ thống loại đơn giản và nhất quán. Tôi thích điều này với cách mọi người nghĩ về việc thu gom rác sai cách; thu gom rác không phải là "khai hoang bộ nhớ không sử dụng"; đó là "mô phỏng một máy tính có bộ nhớ không giới hạn" . Các tối ưu hóa hiệu suất được thảo luận là những điều được thực hiện để làm cho mục tiêu của các chuỗi bất biến hoạt động tốt trên các máy thực; không phải là lý do cho các chuỗi như vậy là bất biến ở nơi đầu tiên.


@ Billy-Oneal .. Về "Nếu bạn có một int chứa giá trị 42 và bạn thêm giá trị 1 vào nó, bạn không thay đổi 42. Bạn nhận được một giá trị mới, 43, hoàn toàn không liên quan đến bắt đầu giá trị. " Bạn có chắc chắn về điều đó không?
Shamit Verma

@Shamit: Vâng, tôi chắc chắn. Thêm 1 đến 42 kết quả vào 43. Nó không làm cho số 42 có nghĩa tương tự như số 43.
Billy ONeal

@Shamit: Tương tự như vậy, bạn không thể làm điều gì đó giống như 43 = 6và mong đợi số 43 có ý nghĩa tương tự như số 6.
Billy ONeal

int i = 42; i = i + 1; mã này sẽ lưu trữ 42 trong bộ nhớ và sau đó thay đổi giá trị ở cùng một vị trí thành 43. Vì vậy, trên thực tế, biến "i" có được giá trị mới là 43.
Shamit Verma

@Shamit: Trong trường hợp đó, bạn bị đột biến ichứ không phải 42. Hãy xem xét string s = "Hello "; s += "World";. Bạn đã thay đổi giá trị của biến s. Tuy nhiên, chuỗi "Hello ", "World""Hello World"là không thay đổi.
Billy ONeal

4

Tính không thay đổi có nghĩa là các hằng số được tổ chức bởi các lớp mà bạn không sở hữu không thể được sửa đổi. Các lớp mà bạn không sở hữu bao gồm các lớp nằm trong cốt lõi của việc triển khai Java và các chuỗi không nên sửa đổi bao gồm những thứ như mã thông báo bảo mật, địa chỉ dịch vụ, v.v. Bạn thực sự không thể sửa đổi các loại đó điều (và điều này áp dụng gấp đôi khi hoạt động ở chế độ hộp cát).

Nếu String không thay đổi, mỗi khi bạn lấy nó từ một số ngữ cảnh không muốn nội dung của chuỗi thay đổi dưới chân nó, bạn sẽ phải lấy một bản sao chỉ trong trường hợp Trực tiếp. Điều đó rất tốn kém.


4
Đối số chính xác này áp dụng cho bất kỳ loại nào , không chỉ với String. Nhưng, ví dụ, Arrays là đột biến. Vì vậy, tại sao Strings bất biến và Arrays không. Và nếu tính bất biến là rất quan trọng, thì tại sao Java lại khó tạo ra và làm việc với các đối tượng bất biến như vậy?
Jörg W Mittag

1
@ JörgWMittag: Tôi cho rằng về cơ bản đó là một câu hỏi về việc họ muốn trở nên cực đoan như thế nào. Có một Chuỗi bất biến là khá triệt để, trở lại trong Java 1.0 ngày. Có một bộ sưu tập bất biến (chủ yếu hoặc thậm chí độc quyền) là tốt, có thể đã quá triệt để để sử dụng rộng rãi ngôn ngữ.
Joachim Sauer

Thực hiện một khung bộ sưu tập bất biến hiệu quả khá khó để tạo ra hiệu suất, nói như một người đã viết một thứ như vậy (nhưng không phải bằng Java). Tôi cũng muốn hoàn toàn rằng tôi có mảng bất biến; điều đó sẽ giúp tôi tiết kiệm được một chút công việc.
Donal Fellows

@DonalFellows: pcollections nhằm mục đích làm điều đó (tuy nhiên không bao giờ sử dụng nó cho bản thân mình).
Joachim Sauer

3
@ JörgWMittag: Có những người (thường là từ quan điểm hoàn toàn chức năng) sẽ cho rằng tất cả các loại nên là bất biến. Tương tự như vậy, tôi nghĩ rằng nếu bạn thêm lên tất cả các vấn đề mà một giao dịch với làm việc với nhà nước có thể thay đổi trong phần mềm song song và đồng thời, bạn có thể đồng ý rằng làm việc với các đối tượng bất biến thường nhiều dễ dàng hơn so với những thể thay đổi.
Steven Evers

2

Hãy tưởng tượng một hệ thống nơi bạn chấp nhận một số dữ liệu, xác minh tính chính xác của nó và sau đó chuyển nó vào (để được lưu trữ trong DB chẳng hạn).

Giả sử rằng dữ liệu là một Stringvà nó phải dài ít nhất 5 ký tự. Phương pháp của bạn trông giống như thế này:

public void handle(String input) {
  if (input.length() < 5) {
    throw new IllegalArgumentException();
  }
  storeInDatabase(input);
}

Bây giờ chúng ta có thể đồng ý, rằng khi storeInDatabaseđược gọi ở đây, inputsẽ phù hợp với yêu cầu. Nhưng nếu Stringcó thể thay đổi, thì người gọi có thể thay đổi inputđối tượng (từ một luồng khác) ngay sau khi nó được xác minh và trước khi nó được lưu trữ trong cơ sở dữ liệu . Điều này sẽ yêu cầu thời gian tốt và có thể sẽ không hoạt động tốt mọi lúc, nhưng đôi khi, anh ấy có thể giúp bạn lưu trữ các giá trị không hợp lệ trong cơ sở dữ liệu.

Các kiểu dữ liệu không thay đổi là một giải pháp rất đơn giản cho vấn đề này (và rất nhiều vấn đề liên quan): bất cứ khi nào bạn kiểm tra một số giá trị, bạn có thể phụ thuộc vào thực tế là điều kiện được kiểm tra vẫn còn đúng sau này.


Cảm ơn đã giải thích. Điều gì xảy ra nếu tôi gọi phương thức xử lý như thế này; xử lý (Chuỗi mới (đầu vào + "naberlan")). Tôi đoán tôi có thể lưu trữ các giá trị không hợp lệ trong db như thế này.
yfklon

1
@blank: tốt, kể từ khi inputcác handlephương pháp đã quá dài (không có vấn đề gì ban đầu input là), nó sẽ chỉ đơn giản là ném một ngoại lệ. Bạn đang tạo một đầu vào mới trước khi gọi phương thức. Đó không phải là vấn đề.
Joachim Sauer

0

Nói chung, bạn sẽ gặp các loại giá trịcác loại tham chiếu . Với loại giá trị, bạn không quan tâm đến đối tượng đại diện cho nó, bạn quan tâm đến giá trị. Nếu tôi cho bạn một giá trị, bạn hy vọng giá trị đó sẽ giữ nguyên. Bạn không muốn nó thay đổi đột ngột. Số 5 là một giá trị. Bạn không mong đợi nó sẽ thay đổi thành 6 đột ngột. Chuỗi "Xin chào" là một giá trị. Bạn không mong đợi nó sẽ thay đổi thành "P *** off" đột ngột.

Với các kiểu tham chiếu bạn quan tâm đến đối tượng và bạn hy vọng nó sẽ thay đổi. Ví dụ, bạn sẽ thường mong đợi một mảng thay đổi. Nếu tôi đưa cho bạn một mảng và bạn muốn giữ nó chính xác như hiện tại, bạn phải tin tưởng tôi không thay đổi nó, hoặc bạn tạo một bản sao của nó.

Với lớp chuỗi Java, các nhà thiết kế phải đưa ra quyết định: Sẽ tốt hơn nếu các chuỗi hành xử giống như một loại giá trị, hay chúng nên hành xử như một loại tham chiếu? Trong trường hợp các chuỗi Java, quyết định được đưa ra là chúng phải là các loại giá trị, có nghĩa là vì chúng là các đối tượng, chúng phải là các đối tượng bất biến.

Quyết định ngược lại có thể đã được đưa ra, nhưng theo tôi sẽ gây ra nhiều đau đầu. Như đã nói ở những nơi khác, nhiều ngôn ngữ đã đưa ra quyết định tương tự và đi đến cùng một kết luận. Một ngoại lệ là C ++, có một lớp chuỗi và các chuỗi có thể là hằng hoặc không đổi, nhưng trong C ++, không giống như Java, các tham số đối tượng có thể được truyền dưới dạng giá trị và không phải là tham chiếu.


0

Tôi thực sự ngạc nhiên không ai chỉ ra điều này.

Trả lời: Nó sẽ không có lợi cho bạn đáng kể, ngay cả khi nó có thể thay đổi. Nó sẽ không có lợi cho bạn nhiều như điều đó gây thêm rắc rối. Hãy xem xét hai trường hợp đột biến phổ biến nhất:

Thay đổi một ký tự của chuỗi

Vì mỗi ký tự trong chuỗi Java mất 2 hoặc 4 byte, hãy tự hỏi, bạn có đạt được gì nếu bạn có thể thay đổi bản sao hiện tại không?

Trong kịch bản bạn đang thay thế một ký tự 2 byte bằng 4 byte một (hoặc ngược lại), bạn phải dịch chuyển phần còn lại của chuỗi bằng 2 byte sang trái hoặc sang phải. Điều này không khác gì so với việc sao chép toàn bộ chuỗi hoàn toàn từ quan điểm tính toán.

Đây cũng là một hành vi thực sự bất thường thường không mong muốn. Hãy tưởng tượng ai đó đang thử nghiệm một ứng dụng bằng văn bản tiếng Anh và khi ứng dụng được chấp nhận ở nước ngoài, chẳng hạn như Trung Quốc, toàn bộ sự việc bắt đầu thực hiện một cách kỳ lạ.

Nối một chuỗi (hoặc ký tự) khác vào chuỗi hiện có

Nếu bạn có hai chuỗi tùy ý, chúng nằm ở hai vị trí bộ nhớ riêng biệt. Nếu bạn muốn thay đổi cái đầu tiên bằng cách nối thêm cái thứ hai, bạn không thể chỉ yêu cầu bộ nhớ bổ sung ở cuối chuỗi thứ nhất, vì có lẽ nó đã bị chiếm dụng.

Bạn phải sao chép chuỗi nối vào một vị trí hoàn toàn mới, giống hệt như cả hai chuỗi là bất biến.

Nếu bạn muốn thực hiện nối thêm một cách hiệu quả, bạn có thể muốn sử dụng StringBuilder, dự trữ một lượng không gian khá lớn ở cuối chuỗi, chỉ với mục đích này là một phần bổ sung có thể trong tương lai.


-2
  1. chúng đắt tiền và giữ cho chúng không thay đổi cho phép những thứ như chuỗi phụ chia sẻ mảng byte của chuỗi chính. (tăng tốc độ cũng như không cần tạo một mảng byte mới và sao chép lại)

  2. bảo mật - sẽ không muốn gói hoặc mã lớp của bạn được đặt tên lại

    [loại bỏ 3 cũ đã xem StringBuilder src - nó không chia sẻ bộ nhớ với chuỗi (cho đến khi được sửa đổi) Tôi nghĩ rằng đó là trong 1.3 hoặc 1.4]

  3. mã băm

  4. đối với chuỗi biến đổi sử dụng SB (trình tạo hoặc bộ đệm khi cần)


2
1. Tất nhiên, có hình phạt là không thể phá hủy các phần lớn hơn của chuỗi nếu điều này xảy ra. Thực tập không miễn phí; mặc dù nó cải thiện hiệu suất cho nhiều chương trình trong thế giới thực. 2. Có thể dễ dàng có "chuỗi" và "ImmutableString" có thể đáp ứng yêu cầu đó. 3. Tôi không chắc là tôi hiểu điều đó ...
Billy ONeal

.3. nên đã được lưu mã băm. Điều này cũng có thể được thực hiện với một chuỗi có thể thay đổi. @ billy-oneal
tgkprog

-4

Các chuỗi phải là một kiểu dữ liệu nguyên thủy trong Java. Nếu chúng là như vậy, thì các chuỗi sẽ mặc định là có thể thay đổi và từ khóa cuối cùng sẽ tạo ra các chuỗi bất biến. Chuỗi có thể thay đổi là hữu ích và do đó, có nhiều hack cho các chuỗi có thể thay đổi trong chuỗi trình tạo chuỗi, trình tạo chuỗi và các lớp kết quả.


3
Điều này không trả lời khía cạnh "tại sao" của câu hỏi hiện tại. Ngoài ra, java cuối cùng không hoạt động theo cách đó. Chuỗi có thể thay đổi không phải là hack, mà là những cân nhắc thiết kế thực tế dựa trên việc sử dụng phổ biến nhất của chuỗi và tối ưu hóa có thể được thực hiện để cải thiện jvm.

1
Câu trả lời cho "tại sao" là một quyết định thiết kế ngôn ngữ kém. Ba cách hơi khác nhau để hỗ trợ các chuỗi có thể thay đổi là một hack mà trình biên dịch / JVM nên xử lý.
CWallach

3
String và StringBuffer là bản gốc. StringBuilder đã được thêm vào sau đó nhận ra một khó khăn thiết kế với StringBuffer. Các chuỗi có thể thay đổi và bất biến là các đối tượng khác nhau được tìm thấy trong nhiều ngôn ngữ khi việc xem xét thiết kế được thực hiện lặp đi lặp lại và quyết định rằng mỗi đối tượng là các đối tượng khác nhau mỗi lần. C # "Chuỗi là bất biến"Tại sao Chuỗi .NET là bất biến? , mục tiêu C NSString là bất biến trong khi NSMutableString là có thể thay đổi. stackoverflow.com/questions/9544182
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.