Một đối tượng bất biến là một đối tượng trong đó các trường bên trong (hoặc ít nhất, tất cả các trường bên trong ảnh hưởng đến hành vi bên ngoài của nó) không thể thay đổi.
Có rất nhiều lợi thế cho chuỗi bất biến:
Hiệu suất: Thực hiện các thao tác sau:
String substring = fullstring.substring(x,y);
C cơ bản cho phương thức chuỗi con () có lẽ giống như thế này:
// Assume string is stored like this:
struct String { char* characters; unsigned int length; };
// Passing pointers because Java is pass-by-reference
struct String* substring(struct String* in, unsigned int begin, unsigned int end)
{
struct String* out = malloc(sizeof(struct String));
out->characters = in->characters + begin;
out->length = end - begin;
return out;
}
Lưu ý rằng không ai trong số các nhân vật phải được sao chép! Nếu đối tượng Chuỗi có thể thay đổi (các ký tự có thể thay đổi sau này) thì bạn sẽ phải sao chép tất cả các ký tự, nếu không, các thay đổi thành các ký tự trong chuỗi con sẽ được phản ánh trong chuỗi khác sau đó.
Đồng thời: Nếu cấu trúc bên trong của một đối tượng bất biến là hợp lệ, nó sẽ luôn có hiệu lực. Không có khả năng các luồng khác nhau có thể tạo trạng thái không hợp lệ trong đối tượng đó. Do đó, các đối tượng bất biến là Thread Safe .
Thu gom rác thải: Người thu gom rác dễ dàng đưa ra quyết định hợp lý về các đối tượng bất biến.
Tuy nhiên, cũng có những nhược điểm đối với sự bất biến:
Hiệu suất: Đợi đã, tôi nghĩ bạn nói hiệu suất là một mặt trái của sự bất biến! Vâng, đôi khi, nhưng không phải luôn luôn. Lấy mã sau:
foo = foo.substring(0,4) + "a" + foo.substring(5); // foo is a String
bar.replace(4,5,"a"); // bar is a StringBuilder
Cả hai dòng đều thay thế ký tự thứ tư bằng chữ "a". Không chỉ là đoạn mã thứ hai dễ đọc hơn, nó còn nhanh hơn. Nhìn vào cách bạn sẽ phải làm mã cơ bản cho foo. Các chuỗi con rất dễ dàng, nhưng bây giờ vì đã có một nhân vật trong không gian năm và một cái gì đó khác có thể đang tham chiếu foo, bạn không thể thay đổi nó; bạn phải sao chép toàn bộ chuỗi (tất nhiên một số chức năng này được trừu tượng hóa thành các hàm trong C bên dưới thực sự, nhưng vấn đề ở đây là hiển thị mã được thực thi tất cả ở một nơi).
struct String* concatenate(struct String* first, struct String* second)
{
struct String* new = malloc(sizeof(struct String));
new->length = first->length + second->length;
new->characters = malloc(new->length);
int i;
for(i = 0; i < first->length; i++)
new->characters[i] = first->characters[i];
for(; i - first->length < second->length; i++)
new->characters[i] = second->characters[i - first->length];
return new;
}
// The code that executes
struct String* astring;
char a = 'a';
astring->characters = &a;
astring->length = 1;
foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));
Lưu ý rằng concatenate được gọi hai lần có nghĩa là toàn bộ chuỗi phải được lặp qua! So sánh mã này với mã C cho bar
hoạt động:
bar->characters[4] = 'a';
Các hoạt động chuỗi đột biến rõ ràng là nhanh hơn nhiều.
Trong kết luận: Trong hầu hết các trường hợp, bạn muốn một chuỗi bất biến. Nhưng nếu bạn cần thực hiện nhiều thao tác nối và chèn vào một chuỗi, bạn cần khả năng biến đổi cho tốc độ. Nếu bạn muốn các lợi ích an toàn và thu gom rác đồng thời với nó, điều quan trọng là giữ các đối tượng có thể thay đổi của bạn cục bộ thành một phương thức:
// This will have awful performance if you don't use mutable strings
String join(String[] strings, String separator)
{
StringBuilder mutable;
boolean first = true;
for(int i = 0; i < strings.length; i++)
{
if(!first) first = false;
else mutable.append(separator);
mutable.append(strings[i]);
}
return mutable.toString();
}
Vì mutable
đối tượng là một tham chiếu cục bộ, bạn không phải lo lắng về an toàn đồng thời (chỉ có một luồng từng chạm vào nó). Và vì nó không được tham chiếu ở bất kỳ nơi nào khác, nên nó chỉ được phân bổ trên ngăn xếp, vì vậy nó được giải quyết ngay khi cuộc gọi chức năng kết thúc (bạn không phải lo lắng về việc thu gom rác). Và bạn nhận được tất cả các lợi ích hiệu suất của cả khả năng biến đổi và bất biến.