Konkurencja
Java została zdefiniowana od samego początku z uwzględnieniem współbieżności. Jak często wspomniano, wspólne zmienne są problematyczne. Jedna rzecz może zmienić drugą za grzbietem innego wątku, nie wiedząc o tym.
Istnieje wiele wielowątkowych błędów C ++, które pojawiły się z powodu wspólnego ciągu - w którym jeden moduł uważał, że można go bezpiecznie zmienić, gdy inny moduł w kodzie zapisał do niego wskaźnik i oczekiwał, że pozostanie taki sam.
„Rozwiązaniem” tego jest to, że każda klasa tworzy obronną kopię modyfikowalnych obiektów, które są do niej przekazywane. W przypadku ciągów zmiennych jest to O (n), aby wykonać kopię. W przypadku ciągów niezmiennych tworzenie kopii to O (1), ponieważ nie jest to kopia, to ten sam obiekt, którego nie można zmienić.
W środowisku wielowątkowym niezmienne obiekty mogą być zawsze bezpiecznie dzielone między sobą. Prowadzi to do ogólnego zmniejszenia zużycia pamięci i usprawnia buforowanie pamięci.
Bezpieczeństwo
Wiele razy łańcuchy są przekazywane jako argumenty do konstruktorów - połączenia sieciowe i protokoły to dwa, które najłatwiej przychodzą na myśl. Możliwość zmiany tego w nieokreślonym czasie później może spowodować problemy z bezpieczeństwem (funkcja myślała, że łączy się z jedną maszyną, ale została przekierowana na inną, ale wszystko w obiekcie wygląda tak, jakby było połączone z pierwszym ... nawet ten sam ciąg).
Java pozwala na użycie refleksji - a parametrami tego są łańcuchy. Niebezpieczeństwo, że ktoś przejdzie przez łańcuch, który można zmodyfikować w drodze do innej metody, która odzwierciedla. To jest bardzo złe.
Klucze do hasha
Tabela skrótów jest jedną z najczęściej używanych struktur danych. Klucze do struktury danych są bardzo często łańcuchami. Posiadanie niezmiennych ciągów oznacza, że (jak wyżej) tabela skrótów nie musi za każdym razem tworzyć kopii klucza skrótu. Gdyby ciągi były zmienne, a tablica skrótów nie spowodowała tego, możliwe byłoby, aby coś zmieniło klucz skrótu na odległość.
Sposób działania obiektu w java polega na tym, że wszystko ma klucz skrótu (dostępny za pomocą metody hashCode ()). Posiadanie niezmiennego ciągu oznacza, że hashCode może być buforowany. Biorąc pod uwagę, jak często ciągi są używane jako klucze do skrótu, zapewnia to znaczny wzrost wydajności (zamiast konieczności ponownego obliczania kodu skrótu za każdym razem).
Podciągi
Ponieważ ciąg jest niezmienny, podstawowa tablica znaków, która wspiera strukturę danych, jest również niezmienna. Pozwala to na pewne optymalizacje substring
metody, którą należy wykonać ( niekoniecznie są one wykonane - wprowadza również możliwość pewnych wycieków pamięci).
Jeśli zrobisz:
String foo = "smiles";
String bar = foo.substring(1,5);
Wartość bar
wynosi „mila”. Jednak zarówno foo
i bar
może być wsparte przez tę samą tablicę znaków, zmniejszając konkretyzacji więcej tablic znakowych lub skopiowanie go - po prostu stosując różne punkty początkowe i końcowe w obrębie łańcucha.
foo | | (0, 6)
vv
uśmiecha się
^ ^
bar | | (1, 5)
Wadą tego (wyciek pamięci) jest to, że jeśli ktoś miałby łańcuch o długości 1k i wziął podłańcuch pierwszego i drugiego znaku, byłby również wspierany przez tablicę znaków o długości 1k. Ta tablica pozostanie w pamięci, nawet jeśli oryginalny ciąg znaków, który miał wartość całej tablicy znaków, został wyrzucony na śmieci.
Można to zobaczyć w String z JDK 6b14 (poniższy kod pochodzi ze źródła GPL v2 i został użyty jako przykład)
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);
}
Zwróć uwagę, w jaki sposób podciąg używa konstruktora String na poziomie pakietu, który nie wymaga kopiowania tablicy i byłby znacznie szybszy (kosztem utrzymywania niektórych dużych tablic - choć nie powielania dużych tablic).
Pamiętaj, że powyższy kod dotyczy Java 1.6. Sposób implementacji konstruktora podciągów został zmieniony w Javie 1.7, co zostało udokumentowane w Wewnętrznej reprezentacji zmian w łańcuchu napisanym w Javie 1.7.0_06
- problem związany z przeciekiem pamięci, o którym wspomniałem powyżej. Java prawdopodobnie nie była postrzegana jako język z dużą ilością manipulacji Stringami, więc zwiększenie wydajności podłańcucha było dobrą rzeczą. Teraz, gdy ogromne dokumenty XML są przechowywane w ciągach, które nigdy nie są gromadzone, staje się to problemem ... a zatem zmiana na String
nieużywanie tej samej podstawowej tablicy z podciągiem, dzięki czemu większa tablica znaków może być zebrana szybciej.
Nie nadużywaj stosu
One mogłyby przekazać wartość łańcucha wokół zamiast odniesienia do niezmiennej ciąg, aby uniknąć problemów z zmienności. Jednak przy dużych ciągach przekazywanie tego na stos byłoby ... obraźliwe dla systemu (umieszczanie całych dokumentów xml jako ciągów na stosie, a następnie ich zdejmowanie lub dalsze przekazywanie ...).
Możliwość deduplikacji
To prawda, że nie była to początkowa motywacja dla tego, dlaczego Ciągi powinny być niezmienne, ale kiedy patrzy się na racjonalne uzasadnienie, dlaczego ciągi niezmienne są dobrą rzeczą, jest to z pewnością coś do rozważenia.
Każdy, kto trochę pracował z Strings, wie, że może ssać pamięć. Jest to szczególnie prawdziwe, gdy robisz takie rzeczy, jak pobieranie danych z baz danych, które pozostają przez jakiś czas. Wiele razy z tymi użądleniami są one ciągle tym samym ciągiem (raz dla każdego rzędu).
Wiele dużych aplikacji Java ma obecnie wąskie gardło w zakresie pamięci. Pomiary wykazały, że około 25% zestawu danych na żywo stosu Java w tego typu aplikacjach jest zużywanych przez obiekty String. Co więcej, mniej więcej połowa tych obiektów String to duplikaty, gdzie duplikaty oznaczają, że string1.equals (string2) jest prawdziwy. Posiadanie zduplikowanych obiektów String na stosie jest w zasadzie marnowaniem pamięci. ...
Wraz z aktualizacją Java 8, aktualizacja 20, JEP 192 (motywacja cytowana powyżej) jest wdrażana w celu rozwiązania tego problemu. Bez wchodzenia w szczegóły, jak działa deduplikacja ciągów, istotne jest, aby same ciągi były niezmienne. Nie możesz deduplikować StringBuilders, ponieważ mogą się zmieniać i nie chcesz, aby ktoś zmieniał coś spod ciebie. Niezmienne ciągi (powiązane z tą pulą ciągów) oznaczają, że możesz przejść, a jeśli znajdziesz dwa ciągi, które są takie same, możesz wskazać jedno odwołanie do drugiego ciągu i pozwolić śmieciarzowi wykorzystać nowo nieużywany.
Inne języki
Cel C (wcześniejszy niż Java) ma NSString
i NSMutableString
.
C # i .NET dokonały tych samych wyborów projektowych, że ciąg domyślny jest niezmienny.
Struny Lua są również niezmienne.
Python również.
Historycznie rzecz biorąc, Lisp, Scheme, Smalltalk wszystkie internalizują ciąg, dzięki czemu jest niezmienny. Bardziej nowoczesne języki dynamiczne często używają ciągów w sposób, który wymaga, aby były niezmienne (może nie być ciągiem , ale jest niezmienne).
Wniosek
Te rozważania projektowe były wielokrotnie powtarzane w wielu językach. Panuje ogólna zgoda co do tego, że niezmienne łańcuchy, pomimo całej swojej niezręczności, są lepsze niż alternatywy i prowadzą do lepszego kodu (mniej błędów) i ogólnie szybszych plików wykonywalnych.