Незмінний об'єкт - це об'єкт, де внутрішні поля (або принаймні всі внутрішні поля, які впливають на його зовнішню поведінку) не можуть бути змінені.
Є багато переваг непорушних рядків:
Продуктивність: виконайте таку операцію:
String substring = fullstring.substring(x,y);
Основний C для методу substring () - це, мабуть, щось подібне:
// 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;
}
Зауважте, що жоден символ не повинен бути скопійований! Якщо об'єкт String був змінним (символи могли змінитись пізніше), то вам доведеться скопіювати всі символи, інакше зміни в символах підрядки будуть відображені в іншому рядку пізніше.
Паралельність: Якщо внутрішня структура незмінного об'єкта є дійсною, вона завжди буде дійсною. Немає шансів, що різні потоки можуть створити недійсний стан всередині цього об’єкта. Отже, незмінні об’єкти є безпечними для потоків .
Збір сміття: сміттєзбірнику набагато простіше приймати логічні рішення щодо незмінних об'єктів.
Однак є й недоліки незмінності:
Продуктивність: Зачекайте, я думав, ви сказали, що продуктивність перевершує незмінність! Ну, буває, але не завжди. Візьміть наступний код:
foo = foo.substring(0,4) + "a" + foo.substring(5); // foo is a String
bar.replace(4,5,"a"); // bar is a StringBuilder
Два рядки замінюють четвертий символ буквою "а". Другий фрагмент коду не тільки читає, але й швидше. Подивіться, як вам доведеться робити базовий код для foo. Підрядки прості, але тепер, оскільки в просторі п'яти вже є символ, і щось інше може посилатися на foo, ви не можете просто змінити його; Ви повинні скопіювати весь рядок (звичайно, частина цієї функціональності абстрагується на функції в реальному нижньому C, але справа тут в тому, щоб показати код, який виконується всім в одному місці).
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));
Зауважте, що concatenate викликається двічі, що означає, що весь рядок повинен бути пропущений через цикл! Порівняйте це з кодом С для bar
операції:
bar->characters[4] = 'a';
Операція змінної струни, очевидно, набагато швидша.
На закінчення : у більшості випадків потрібно непорушний рядок. Але якщо вам потрібно зробити багато додавання та вставки в рядок, вам потрібна можливість зміни швидкості. Якщо ви хочете, щоб з нею були переваги безпеки одночасності та вивезення сміття, головним є збереження мінливих об’єктів локальним методом:
// 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();
}
Оскільки mutable
об'єкт є локальним посиланням, вам не потрібно турбуватися про безпеку одночасності (лише одна нитка коли-небудь торкається його). А оскільки на нього ніде не посилається, він виділяється лише у стеку, тому він розміщується, як тільки виклик функції закінчується (вам не доведеться турбуватися про збирання сміття). І ви отримуєте всі переваги від продуктивності як незмінність, так і незмінність.