Паралельність
Java визначалася з самого початку з міркувань одночасності. Як часто згадувалося, спільні мутації є проблематичними. Одна річ може змінити іншу за тильною стороною іншої нитки, не знаючи про це.
Є безліч багатопотокових помилок C ++, які з'явилися через спільну рядок - де один модуль вважав, що це безпечно змінити, коли інший модуль у коді врятував на нього вказівник і очікував, що він залишиться таким же.
"Рішення" цього полягає в тому, що кожен клас робить захисну копію змінних об'єктів, які передаються йому. Для змінних рядків це копія O (n). Для незмінних рядків створення копії є O (1), оскільки це не копія, це той самий об'єкт, який не може змінити.
У багатопотоковому середовищі незмінні предмети завжди можна безпечно ділити між собою. Це призводить до загального скорочення використання пам'яті та покращує кешування пам'яті.
Безпека
Багато разів рядки передаються навколо як аргументи конструкторам - мережеві з'єднання та протоколи - це два, які найлегше приходять в голову. Можливість змінити це у невизначений час пізніше при виконанні може призвести до проблем із безпекою (функція вважала, що вона підключається до однієї машини, але була перенаправлена на іншу, але все в об'єкті виглядає так, як це підключено до першої ... його навіть однаковий рядок).
Java дозволяє використовувати відображення - і параметри для цього - це рядки. Небезпека переходу рядка, який може бути модифікований шляхом до іншого відображення. Це дуже погано.
Ключі до хешу
Хеш-таблиця - одна з найбільш використовуваних структур даних. Клавіші структури даних дуже часто є рядками. Наявність незмінних рядків означає, що (як зазначено вище) хеш-таблиці не потрібно робити копію хеш-ключа кожен раз. Якби рядки були змінними, а хеш-таблиця цього не зробила, можна було б щось змінити хеш-ключ на відстані.
Те, як працює об’єкт в Java, полягає в тому, що все має хеш-ключ (доступ до якого здійснюється методом hashCode ()). Мати незмінний рядок означає, що хеш-код може бути кешований. Зважаючи на те, як часто Strings використовуються як ключі до хешу, це забезпечує значне підвищення продуктивності (а не щоразу перераховувати хеш-код).
Підрядки
Маючи String бути незмінним, масив символів, що підтримує структуру даних, також є незмінним. Це дозволяє зробити певні оптимізації щодо substring
методу, який слід виконати (вони не обов'язково робляться - це також вводить можливість деяких витоків пам'яті).
Якщо ти зробиш:
String foo = "smiles";
String bar = foo.substring(1,5);
Значення bar
"миля". Однак обидва foo
і bar
можуть бути підкріплені одним і тим же масивом символів, зменшуючи інстанціювання більшої кількості символьних масивів або копіюючи її - просто використовуючи різні початкові та кінцеві точки в рядку.
foo | | (0, 6)
vv
посміхається
^ ^
бар | | (1, 5)
Тепер недоліком цього (витоку пам’яті) є те, що якби у одного рядка було довжиною 1 кб і було взято підрядку першого та другого символу, він також підтримувався б довгим символом масиву 1 к. Цей масив залишиться в пам'яті, навіть якщо початковий рядок, який мав значення для всього масиву символів, був зібраний сміттям.
Це можна побачити в String від JDK 6b14 (наступний код - з джерела GPL v2 і використовується як приклад)
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);
}
Зверніть увагу, як підрядка використовує String-конструктор на рівні пакету, який не передбачає жодної копіювання масиву і був би набагато швидшим (за рахунок можливо збереження навколо деяких великих масивів - хоча і не дублювання великих масивів).
Зверніть увагу, що наведений вище код призначений для Java 1.6. Спосіб реалізації конструктора підрядків був змінений з Java 1.7, як це зафіксовано у розділі Зміни в рядку внутрішнього представлення, зробленому на Java 1.7.0_06
- проблема, пов’язана з витоком пам'яті, про який я згадував вище. Напевно, Java не сприймалася як мова з великою кількістю маніпуляцій з String, тому підвищення продуктивності для підрядків було гарною справою. Тепер, з величезними документами XML, що зберігаються у рядках, які ніколи не збираються, це стає проблемою ... і, отже, зміна на String
не використання того самого базового масиву з підрядком, щоб швидше збирати масив символів швидше.
Не зловживайте стеком
Один може передати значення рядка навколо замість посилання на непорушну рядок , щоб уникнути проблем з мінливістю. Однак з великими рядками передача цього на стек буде ... образливою для системи (розміщення цілих документів XML як рядків у стеку, а потім їх зняття або продовження передачі їх разом ...).
Можливість дедуплікації
Зрозуміло, це не було початковою мотивацією того, чому струни повинні бути незмінні, але коли дивиться на раціональне, чому непорушні струни - це добра річ, це, безумовно, щось, що слід враховувати.
Кожен, хто трохи працював зі Strings, знає, що вони можуть висмоктувати пам'ять. Це особливо актуально, коли ви робите такі речі, як витягування даних із баз даних, які тримаються на деякий час. Багато разів з цими струнами вони повторюються один і той же рядок (один раз для кожного ряду).
Наразі багато масштабних програм Java перебувають у вузькому місці в пам'яті. Виміри показали, що приблизно 25% живих даних Java купи в цих типах програм споживаються об'єктами String. Крім того, приблизно половина цих об'єктів String - це дублікати, де duplicates означає string1.equals (string2). Наявність дублікатів об'єктів String на купі - це, по суті, лише витрата пам'яті. ...
З оновленням Java 8, для вирішення цього питання реалізується JEP 192 (мотивація, цитована вище). Не вникаючи в подробиці того, як працює дедуплікація рядків, важливо, щоб самі рядки були непорушними. Ви не можете видалити копію StringBuilders, оскільки вони можуть змінюватися, і ви не хочете, щоб хтось міняв щось під вами. Незмінні рядки (пов’язані з цим пулом String) означають, що ви можете пройти, і якщо ви знайдете два однакових рядка, ви можете вказати одну посилання рядка на іншу і дозволити збору сміття споживати щойно невикористаний.
Інші мови
Завдання C (яке передує Java) має NSString
і NSMutableString
.
C # і .NET зробили однаковий вибір дизайну, коли рядки за замовчуванням незмінні.
Струни Луа також непорушні.
Пітон також.
Історично, Lisp, Scheme, Smalltalk усі інтернують рядок і, таким чином, вважають його незмінним. Більш сучасні динамічні мови часто використовують рядки певним чином, що вимагає, щоб вони були незмінні (це може бути не рядком , але воно незмінне).
Висновок
Ці міркування щодо дизайну були зроблені знову і знову на багатьох мовах. Це загальний консенсус, що незмінні рядки, при всій їх незграбності, кращі за альтернативи та призводять до кращого коду (менше помилок) та швидшого виконання файлів загалом.