Dlaczego String jest niezmienny w Javie?

78

Nie mogłem zrozumieć przyczyny tego. Zawsze używam klasy String, podobnie jak innych programistów, ale kiedy modyfikuję jej wartość, tworzona jest nowa instancja String.

Co może być przyczyną niezmienności klasy String w Javie?

Wiem, że istnieje kilka alternatyw, takich jak StringBuffer lub StringBuilder. To tylko ciekawość.

yfklon
źródło
20
Technicznie rzecz biorąc, nie jest to duplikat, ale Eric Lippert daje świetną odpowiedź na to pytanie tutaj: programmers.stackexchange.com/a/190913/33843
Heinzi 16.04.13

Odpowiedzi:

105

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 substringmetody, 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ść barwynosi „mila”. Jednak zarówno fooi barmoż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 Stringnieuż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 NSStringi 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.


źródło
3
Java zapewnia zmienne i niezmienne ciągi znaków. Ta odpowiedź wyszczególnia niektóre zalety wydajności, które można uzyskać na niezmiennych ciągach, oraz niektóre powody, dla których można wybrać niezmienne dane; ale nie dyskutuje, dlaczego wersja niezmienna jest wersją domyślną.
Billy ONeal
3
@BillyONeal: bezpieczne domyślne i niezabezpieczona alternatywa prawie zawsze prowadzi do bezpieczniejszych systemów niż odwrotne podejście.
Joachim Sauer
4
@BillyONeal Gdyby niezmienne nie były domyślnie, problemy dotyczące współbieżności, bezpieczeństwa i skrótów byłyby bardziej powszechne. Projektanci języków wybrali (częściowo w odpowiedzi na C), aby stworzyć język, w którym skonfigurowane są ustawienia domyślne, aby zapobiec wielu typowym błędom, próbując poprawić wydajność programisty (nie martwiąc się już o te błędy). Jest mniej błędów (oczywistych i ukrytych) z niezmiennymi łańcuchami niż z modyfikowalnymi.
@Jachachim: Nie twierdzę inaczej.
Billy ONeal
1
Technicznie, Common Lisp ma zmienne ciągi, dla operacji „podobnych do łańcucha” i symbole z niezmiennymi nazwami dla niezmiennych identyfikatorów.
Vatine
21

Powody, które pamiętam:

  1. Funkcja puli ciągów bez uniezależnienia łańcucha nie jest w ogóle możliwa, ponieważ w przypadku puli ciągów jeden obiekt / literał ciągu, np. „XYZ” będzie odwoływał się do wielu zmiennych referencyjnych, więc jeśli jedna z nich się zmieni, automatycznie wpłynie to na inne .

  2. Ciąg jest szeroko stosowany jako parametr dla wielu klas Java, np. Do otwierania połączenia sieciowego, do otwierania połączenia z bazą danych, otwierania plików. Jeśli String nie jest niezmienny, prowadziłoby to do poważnego zagrożenia bezpieczeństwa.

  3. Niezmienność pozwala łańcuchowi buforować swój kod skrótu.

  4. Sprawia, że ​​jest wątkowo bezpieczny.

GŁUPEK
źródło
7

1) Pula ciągów

Projektant Java wie, że String będzie najczęściej używanym typem danych we wszelkiego rodzaju aplikacjach Java i dlatego od samego początku chcieli go zoptymalizować. Jednym z kluczowych kroków w tym kierunku był pomysł przechowywania literałów ciągów w puli ciągów znaków. Celem było ograniczenie tymczasowego obiektu String poprzez udostępnienie ich i aby je udostępnić, muszą być z klasy Niezmiennej. Nie można udostępniać modyfikowalnego obiektu dwóm nieznanym sobie stronom. Weźmy hipotetyczny przykład, w którym dwie zmienne odniesienia wskazują na ten sam obiekt String:

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

Teraz, jeśli s1 zmieni obiekt z „Java” na „C ++”, zmienna referencyjna również otrzyma wartość s2 = „C ++”, o której nawet nie wie. Dzięki temu, że String był niezmienny, to dzielenie się literałem String było możliwe. Krótko mówiąc, kluczowa idea puli ciągów nie może zostać zaimplementowana bez uczynienia łańcucha ostatecznym lub niezmiennym w Javie.

2) Bezpieczeństwo

Java ma wyraźny cel w zakresie zapewnienia bezpiecznego środowiska na każdym poziomie usług, a String ma krytyczne znaczenie w tych wszystkich kwestiach bezpieczeństwa. Ciąg jest szeroko stosowany jako parametr dla wielu klas Java, np. Do otwierania połączenia sieciowego, możesz przekazać hosta i port jako Ciąg, do odczytu plików w Javie możesz przekazać ścieżkę plików i katalogu jako Ciąg i do otwarcia połączenia z bazą danych, możesz przekazać adres URL bazy danych jako ciąg. Jeśli String nie był niezmienny, użytkownik mógł udzielić dostępu do określonego pliku w systemie, ale po uwierzytelnieniu może zmienić PATH na coś innego, co może powodować poważne problemy z bezpieczeństwem. Podobnie podczas łączenia się z bazą danych lub dowolnym innym komputerem w sieci mutowanie wartości ciągu może stanowić zagrożenie dla bezpieczeństwa. Zmienne łańcuchy mogą również powodować problemy z bezpieczeństwem w Reflection,

3) Zastosowanie sznurka w mechanizmie ładowania klasy

Kolejny powód, dla którego String był ostateczny lub niezmienny, wynikał z faktu, że był intensywnie wykorzystywany w mechanizmie ładowania klas. Ponieważ String nie był niezmienny, atakujący może skorzystać z tego faktu, a żądanie załadowania standardowych klas Java, np. Java.io.Reader, można zmienić na złośliwą klasę com.unknown.DataStolenReader. Zachowując ciąg Ostateczny i niezmienny, możemy przynajmniej mieć pewność, że JVM ładuje prawidłowe klasy.

4) Korzyści z wielowątkowości

Ponieważ współbieżność i wielowątkowość była kluczową ofertą Javy, sensowne było rozważenie bezpieczeństwa wątków obiektów String. Ponieważ oczekiwano, że String będzie powszechnie używany, co oznacza, że ​​niezmienny oznacza brak zewnętrznej synchronizacji, oznacza znacznie czystszy kod obejmujący współużytkowanie String między wieloma wątkami. Ta pojedyncza funkcja znacznie ułatwia komplikowanie, mylenie i podatność na błędy w kodowaniu współbieżności. Ponieważ String jest niezmienny i po prostu dzielimy go między wątkami, powoduje to, że kod jest bardziej czytelny.

5) Optymalizacja i wydajność

Teraz, kiedy tworzysz klasę Niezmienną, wiesz z góry, że ta klasa nie zmieni się po utworzeniu. To gwarantuje otwartą ścieżkę dla wielu optymalizacji wydajności, np. Buforowania. Sam String wie, że nie zamierzam się zmieniać, więc String buforuje swój kod skrótu. Nawet leniwie oblicza kod skrótu, a po utworzeniu wystarczy go buforować. W prostym świecie, kiedy po raz pierwszy wywołujesz metodę hashCode () dowolnego obiektu String, oblicza ona kod skrótu, a każde kolejne wywołanie hashCode () zwraca już obliczoną, buforowaną wartość. Powoduje to dobry wzrost wydajności, biorąc pod uwagę, że String jest intensywnie używany w Mapach opartych na haszowaniu, np. Hashtable i HashMap. Buforowanie kodu skrótu nie było możliwe bez uczynienia go niezmiennym i ostatecznym, ponieważ zależy od zawartości samego ciągu znaków.

saidesh kilaru
źródło
5

Wirtualna maszyna Java wykonuje kilka optymalizacji dotyczących operacji na łańcuchach, których inaczej nie można wykonać. Na przykład, jeśli miałeś ciąg znaków o wartości „Mississippi” i przypisałeś „Mississippi” .substring (0, 4) do innego ciągu, o ile wiesz, utworzono kopię pierwszych czterech znaków, aby utworzyć „Miss” . Nie wiesz, że oba mają ten sam oryginalny ciąg „Mississippi”, z których jeden jest właścicielem, a drugi jest odniesieniem do tego ciągu od pozycji 0 do 4. (Odwołanie do właściciela zapobiega gromadzeniu właściciela przez śmietnik, gdy właściciel wykracza poza zakres)

Jest to trywialne w przypadku łańcucha tak małego jak „Mississippi”, ale przy większych ciągach i wielu operacjach nie trzeba kopiować ciągu, co znacznie oszczędza czas! Gdyby łańcuchy były zmienne, nie można tego zrobić, ponieważ modyfikacja oryginału wpłynie również na „kopie” podłańcucha.

Ponadto, jak wspomina Donal, przewaga byłaby znacznie osłabiona przez jego wadę. Wyobraź sobie, że piszesz program zależny od biblioteki i używasz funkcji zwracającej ciąg znaków. Skąd możesz mieć pewność, że ta wartość pozostanie stała? Aby mieć pewność, że nic takiego się nie wydarzy, zawsze musisz przedstawić kopię.

Co zrobić, jeśli masz dwa wątki dzielące ten sam ciąg? Nie chciałbyś czytać napisu, który jest obecnie przepisywany przez inny wątek, prawda? Łańcuch musiałby zatem być bezpieczny dla wątków, co, jako że jest to powszechna klasa, spowodowałoby, że praktycznie każdy program Java byłby znacznie wolniejszy. W przeciwnym razie będziesz musiał wykonać kopię dla każdego wątku, który wymaga tego ciągu, lub będziesz musiał umieścić kod używający tego ciągu w bloku synchronizacji, które spowalniają twój program.

Z tych wszystkich powodów była to jedna z pierwszych decyzji podjętych dla Javy w celu odróżnienia się od C ++.

Neil
źródło
Teoretycznie możesz zarządzać buforami wielowarstwowymi, które umożliwiają kopiowanie przy mutacji, jeśli są współużytkowane, ale bardzo trudno jest sprawnie pracować w środowisku wielowątkowym.
Donal Fellows
@DonalFellows Po prostu założyłem, że ponieważ Java Virtual Machine nie jest napisana w Javie (oczywiście), jest zarządzana wewnętrznie za pomocą wspólnych wskaźników lub coś podobnego.
Neil
5

Przyczyną niezmienności łańcucha jest spójność z innymi pierwotnymi typami w języku. Jeśli masz wartość intzawierającą 42 i dodajesz do niej wartość 1, nie zmieniasz wartości 42. Otrzymujesz nową wartość, 43, która jest całkowicie niezwiązana z wartościami początkowymi. Mutowanie prymitywów innych niż łańcuch nie ma sensu pojęciowego; i jako takie programy, które traktują ciągi jako niezmienne, są często łatwiejsze do uzasadnienia i zrozumienia.

Co więcej, Java naprawdę zapewnia zarówno zmienne, jak i niezmienne ciągi, jak widać StringBuilder; tak naprawdę tylko domyślny jest niezmienny ciąg. Jeśli chcesz przekazywać odniesienia StringBuilderwszędzie, możesz to zrobić. Java używa osobnych typów ( Stringi StringBuilder) dla tych pojęć, ponieważ nie obsługuje wyrażania zmienności lub jej braku w systemie typów. W językach, które obsługują niezmienność w swoich systemach typów (np. C ++ const), często występuje jeden typ łańcucha, który służy obu celom.

Tak, posiadanie ciągów niezmiennych pozwala na wdrożenie niektórych optymalizacji specyficznych dla niezmiennych ciągów, takich jak internowanie, i pozwala na przekazywanie odniesień ciągów bez synchronizacji między wątkami. To jednak myli mechanizm z zamierzonym celem języka z prostym i spójnym systemem typów. Porównuję to do tego, jak wszyscy myślą w niewłaściwy sposób na śmieci; odśmiecanie nie jest „odzyskiwaniem nieużywanej pamięci”; „symuluje komputer z nieograniczoną pamięcią” . Omawiane optymalizacje wydajności to rzeczy, które są wykonywane, aby cel niezmiennych ciągów znaków działał dobrze na prawdziwych maszynach; nie powód, dla którego takie ciągi są niezmienne.

Billy ONeal
źródło
@ Billy-Oneal .. Odnośnie „Jeśli masz liczbę całkowitą zawierającą wartość 42 i dodajesz do niej wartość 1, nie zmieniasz 42. Otrzymujesz nową wartość, 43, która jest całkowicie niezwiązana z początkiem wartości ”. Jesteś pewien?
Shamit Verma
@Shamit: Tak, jestem pewien. Dodanie 1 do 42 daje wynik 43. Nie oznacza to, że liczba 42 oznacza to samo, co liczba 43.
Billy ONeal
@Shamit: Podobnie, nie można zrobić czegoś podobnego 43 = 6i oczekiwać, że liczba 43 będzie oznaczać to samo co liczba 6.
Billy ONeal
int i = 42; i = i + 1; ten kod zapisze 42 w pamięci, a następnie zmieni wartości w tej samej lokalizacji na 43. W rzeczywistości zmienna „i” uzyskuje nową wartość 43.
Shamit Verma
@Shamit: W takim przypadku zmutowałeś i, a nie 42. Zastanów się string s = "Hello "; s += "World";. Zmieniono wartość zmiennej s. Ale struny "Hello ", "World"i "Hello World"są niezmienne.
Billy ONeal
4

Niezmienność oznacza, że ​​stałe przechowywane przez klasy, których nie posiadasz, nie mogą być modyfikowane. Klasy, których nie posiadasz, obejmują te, które są rdzeniem implementacji Java, a ciągi, których nie należy modyfikować, obejmują takie elementy, jak tokeny bezpieczeństwa, adresy usług itp. Naprawdę nie powinieneś być w stanie modyfikować tego rodzaju rzeczy (i dotyczy to podwójnie podczas pracy w trybie piaskownicy).

Jeśli String nie był niezmienny, za każdym razem, gdy pobierałeś go z jakiegoś kontekstu, który nie chciał, aby zawartość łańcucha była zmieniana pod stopami, musisz wziąć kopię „na wszelki wypadek”. To staje się bardzo drogie.

Donal Fellows
źródło
4
Ten sam argument dotyczy każdego typu, nie tylko String. Ale na przykład Arrays są jednak zmienne. Dlaczego więc są Stringniezmienne, a Arraynie są. A jeśli niezmienność jest tak ważna, to dlaczego Java utrudnia tworzenie i pracę z niezmiennymi obiektami?
Jörg W Mittag
1
@ JörgWMittag: Zakładam, że w zasadzie chodzi o to, jak radykalni byli. Posiadanie niezmiennego ciągu było dość radykalne, już w Javie 1.0 dni. Posiadanie (przede wszystkim lub nawet wyłącznie) niezmiennych ram kolekcji, może być zbyt radykalne, aby uzyskać szerokie zastosowanie tego języka.
Joachim Sauer
Wykonanie skutecznego niezmiennego frameworku kolekcji jest dość trudne, aby uczynić go wydajnym, mówiąc jak ktoś, kto napisał coś takiego (ale nie w Javie). Pragnę też całkowicie, że mam niezmienne tablice; zaoszczędziłoby mi to sporo pracy.
Donal Fellows
@DonalFellows: celem pcollections jest właśnie to (jednak nigdy sam tego nie użyłem).
Joachim Sauer
3
@ JörgWMittag: Są ludzie (zwykle z czysto funkcjonalnej perspektywy), którzy twierdzą, że wszystkie typy powinny być niezmienne. Podobnie myślę, że jeśli zsumujesz wszystkie problemy, które dotyczą pracy ze stanem zmiennym w równoległym i współbieżnym oprogramowaniu, możesz zgodzić się, że praca z obiektami niezmiennymi jest często znacznie łatwiejsza niż z modyfikowalnymi.
Steven Evers
2

Wyobraź sobie system, w którym akceptujesz niektóre dane, weryfikujesz ich poprawność, a następnie przekazujesz (na przykład do zapisania w bazie danych).

Zakładając, że dane są a Stringi muszą mieć co najmniej 5 znaków. Twoja metoda wygląda mniej więcej tak:

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

Teraz możemy się zgodzić, że kiedy storeInDatabasezostanie tu wywołany, inputspełni wymagania. Ale jeśli Stringbyłyby zmienne, wówczas program wywołujący mógłby zmienić inputobiekt (z innego wątku) zaraz po jego weryfikacji i przed zapisaniem w bazie danych . Wymagałoby to dobrego wyczucia czasu i prawdopodobnie nie za każdym razem szło dobrze, ale od czasu do czasu byłby w stanie zmusić cię do przechowywania nieprawidłowych wartości w bazie danych.

Niezmienne typy danych są bardzo prostym rozwiązaniem tego (i wielu powiązanych) problemów: za każdym razem, gdy sprawdzasz jakąś wartość, możesz polegać na tym, że sprawdzony warunek jest nadal prawdziwy.

Joachim Sauer
źródło
Dziękuję za wyjaśnienie. Co jeśli wywołam taką metodę obsługi; uchwyt (nowy ciąg (wejście + „naberlan”)). Chyba mogę przechowywać niepoprawne wartości w db w ten sposób.
yfklon 16.04.13
1
@blank: dobrze, ponieważ inputw handlemetodzie jest już zbyt długo (bez względu na to, co oryginalne input jest), to po prostu wyjątek. Tworzysz nowe dane wejściowe przed wywołaniem metody. To nie jest problem.
Joachim Sauer
0

Zasadniczo można spotkać typy wartości i typy referencyjne . Dzięki typowi wartości nie obchodzi cię obiekt, który go reprezentuje, zależy ci na wartości. Jeśli dam ci wartość, oczekujesz, że ta wartość pozostanie taka sama. Nie chcesz, żeby to się nagle zmieniło. Liczba 5 jest wartością. Nie spodziewasz się, że zmieni się nagle na 6. Ciąg „Hello” jest wartością. Nie spodziewasz się, że nagle zmieni się na „P *** off”.

W przypadku typów referencyjnych zależy Ci na obiekcie i spodziewasz się, że się zmieni. Na przykład często oczekuje się zmiany tablicy. Jeśli dam ci tablicę i chcesz zachować ją dokładnie taką, jaka jest, albo musisz mi zaufać, że jej nie zmienię, albo zrobisz jej kopię.

W przypadku klasy ciągów Java projektanci musieli podjąć decyzję: czy lepiej jest, jeśli ciągi zachowują się jak typ wartości, czy powinny zachowywać się jak typ odniesienia? W przypadku ciągów Java podjęto decyzję, że powinny one być typami wartości, co oznacza, że ​​ponieważ są obiektami, muszą być obiektami niezmiennymi.

Mogła zostać podjęta odwrotna decyzja, ale moim zdaniem spowodowałaby wiele bólów głowy. Jak powiedziano gdzie indziej, wiele języków podjęło tę samą decyzję i doszło do tego samego wniosku. Wyjątkiem jest C ++, który ma jedną klasę łańcuchów, a łańcuchy mogą być stałe lub niestałe, ale w C ++, w przeciwieństwie do Javy, parametry obiektu mogą być przekazywane jako wartości, a nie jako referencje.

gnasher729
źródło
0

Jestem naprawdę zaskoczony, że nikt tego nie zauważył.

Odpowiedź: Nie przyniosłoby ci to znaczących korzyści, nawet gdyby było zmienne. Nie przyniesie ci to tyle korzyści, ile spowoduje dodatkowe problemy. Przeanalizujmy dwa najczęstsze przypadki mutacji:

Zmiana jednego znaku ciągu

Ponieważ każdy znak w ciągu Java zajmuje 2 lub 4 bajty, zadaj sobie pytanie, czy zyskałbyś coś, gdybyś mógł zmutować istniejącą kopię?

W scenariuszu, w którym zamieniasz znak 2-bajtowy na 4-bajtowy (lub odwrotnie), musisz przesunąć pozostałą część ciągu o 2 bajty w lewo lub w prawo. Co nie różni się tak bardzo od skopiowania całego łańcucha z obliczeniowego punktu widzenia.

Jest to również naprawdę nieregularne zachowanie, które jest na ogół niepożądane. Wyobraź sobie, że ktoś testuje aplikację z tekstem w języku angielskim, a kiedy aplikacja zostanie zaadaptowana do innych krajów, takich jak Chiny, wszystko zaczyna działać dziwnie.

Dołączanie kolejnego ciągu (lub znaku) do istniejącego

Jeśli masz dwa dowolne ciągi, znajdują się one w dwóch różnych lokalizacjach pamięci. Jeśli chcesz zmienić pierwszy przez dodanie drugiego, nie możesz po prostu poprosić o dodatkową pamięć na końcu pierwszego ciągu, ponieważ prawdopodobnie jest już zajęty.

Musisz skopiować skonkatenowany ciąg do zupełnie nowej lokalizacji, która jest dokładnie taka sama, jakby oba ciągi były niezmienne.

Jeśli chcesz efektywnie dodawać dodatki, możesz z nich skorzystać StringBuilder, która rezerwuje całkiem sporą ilość miejsca na końcu łańcucha, tylko na ten cel ewentualnego dołączenia w przyszłości.

Rok Kralj
źródło
-2
  1. są drogie, a utrzymanie ich niezmienności pozwala na takie rzeczy, jak podłańcuchy dzielące tablicę bajtów głównego ciągu. (zwiększenie prędkości również, ponieważ nie trzeba tworzyć nowej tablicy bajtów i kopiować)

  2. bezpieczeństwo - nie chce, aby nazwa pakietu lub kod klasy została zmieniona

    [usunięty stary 3 spojrzał na StringBuilder src - nie dzieli pamięci z ciągiem (dopóki nie zostanie zmodyfikowany) Myślę, że był w 1.3 lub 1.4]

  3. hashcode pamięci podręcznej

  4. w przypadku łańcuchów zmiennych można użyć SB (konstruktor lub bufor w razie potrzeby)

tgkprog
źródło
2
1. Oczywiście karą jest niemożność zniszczenia większych części sznurka, jeśli tak się stanie. Staż nie jest bezpłatny; chociaż poprawia wydajność wielu programów w świecie rzeczywistym. 2. Może być łatwo „ciąg” i „ImmutableString”, które mogłyby spełnić to wymaganie. 3. Nie jestem pewien, czy rozumiem, że ...
Billy ONeal
.3 powinien był buforować kod skrótu. To również można zrobić za pomocą ciągu zmiennego. @ billy-oneal
tgkprog
-4

Ciągi powinny być prymitywnym typem danych w Javie. Gdyby tak było, łańcuchy domyślnie byłyby zmienne, a końcowe słowo kluczowe generowałoby łańcuchy niezmienne. Zmienny ciąg jest użyteczny, dlatego istnieje wiele hacków dla zmiennych ciągów w klasach bufora ciągów, konstruktora ciągów i znaków.

CWallach
źródło
3
To nie odpowiada na pytanie „dlaczego” tego, co jest teraz, kiedy pytanie zadaje. Ponadto java final nie działa w ten sposób. Zmienne ciągi nie są hackami, ale raczej rzeczywistymi rozważaniami projektowymi opartymi na najczęstszych zastosowaniach ciągów i optymalizacjach, które można wykonać w celu ulepszenia Jvm.
1
Odpowiedź na „dlaczego” to kiepska decyzja dotycząca projektowania języka. Trzy nieco inne sposoby obsługi łańcuchów zmiennych to hack, którym powinien zająć się kompilator / JVM.
CWallach 16.04.13
3
String i StringBuffer były oryginalnymi. StringBuilder został później dodany, rozpoznając trudności projektowe z StringBuffer. Zmienne i niezmienne ciągi będące różnymi obiektami można znaleźć w wielu językach, ponieważ rozważania projektowe były wielokrotnie rozważane i zdecydowano, że każdy z nich jest za każdym razem innym przedmiotem. C # „Ciągi są niezmienne” i dlaczego ciąg .NET jest niezmienny? , cel C NSString jest niezmienny, podczas gdy NSMutableString jest mutowalny. stackoverflow.com/questions/9544182