Czy ciąg Javy jest naprawdę niezmienny?

Wszyscy wiemy, że String jest niezmienna w Javie, ale sprawdź następujący kod:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

Dlaczego ten program działa w ten sposób? I dlaczego zmienia się wartość s1 i s2, a nie s3?

Author: Peter Mortensen, 2014-01-06

14 answers

String jest niezmienny*, ale oznacza to tylko, że nie można go zmienić za pomocą jego publicznego API.

To, co tutaj robisz, to ominięcie normalnego API, używając reflection. W ten sam sposób można zmienić wartości enum, zmienić tabelę wyszukiwania używaną w Integer autoboxing itd.

Powodem zmiany wartości s1 i s2 jest to, że obie odnoszą się do tego samego internowanego łańcucha. Robi to kompilator (o czym wspominają inne odpowiedzi).

Powód s3 robi not {[21] } było dla mnie trochę zaskakujące, ponieważ myślałem, że będzie współdzielić value array (to miało miejsce we wcześniejszej wersji Javy, przed Javą 7u6). Jednak patrząc na kod źródłowy String, widzimy, że tablica znaków value dla podłańcucha jest faktycznie kopiowana (używając Arrays.copyOfRange(..)). Dlatego pozostaje bez zmian.

Możesz zainstalować SecurityManager, aby uniknąć złośliwego kodu do robienia takich rzeczy. Należy jednak pamiętać, że niektóre biblioteki zależą od korzystania z tego rodzaju sztuczek odbicia (zazwyczaj narzędzia ORM, biblioteki AOP itp.).

*) początkowo napisałem, że String s nie są tak naprawdę niezmienne, tylko "skuteczne niezmienne". Może to być mylące w obecnej implementacji String, gdzie tablica value jest rzeczywiście oznaczona private final. Warto jednak zauważyć, że nie ma sposobu, aby zadeklarować tablicę w Javie jako niezmienną, więc należy uważać, aby nie ujawniać jej poza swoją klasą, nawet przy użyciu odpowiednich modyfikatorów dostępu.


Jak się wydaje ten temat przytłaczająco popularny, oto niektóre sugerowane dalsze czytanie: [[31]}Reflection Madness talk Heinza Kabutza Z JavaZone 2009, który obejmuje wiele kwestii w OP, wraz z innymi refleksjami... cóż... szaleństwo.

Wyjaśnia, dlaczego czasami jest to przydatne. I dlaczego w większości przypadków powinieneś tego unikać. :-)

 390
Author: haraldK,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-06 18:34:50

W języku Java, Jeśli dwie zmienne typu string są zainicjalizowane tym samym literałem, to przypisuje to samo odniesienie do obu zmiennych:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

inicjalizacja

Dlatego porównanie zwraca true. Trzeci ciąg jest tworzony za pomocą substring(), który tworzy nowy Ciąg zamiast wskazywać na ten sam.

sub string

Gdy uzyskasz dostęp do ciągu znaków za pomocą reflection, otrzymasz rzeczywisty wskaźnik:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

Więc zmiana tego spowoduje zmianę ciąg zawierający wskaźnik do niego, ale ponieważ {[3] } jest tworzony z nowym ciągiem z powodu substring(), nie zmieni się.

Zmień

 94
Author: Zaheer Ahmed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 00:24:30

Używasz odbicia, aby obejść niezmienność łańcucha-jest to forma "ataku".

Istnieje wiele przykładów, które możesz utworzyć w ten sposób (np. możesz nawet utworzyć instancję Void obiektu), ale to nie znaczy, że łańcuch nie jest "niezmienny".

Istnieją przypadki użycia tego typu kodu na Twoją korzyść i "dobre kodowanie", takie jak czyszczenie haseł z pamięci w najwcześniejszym możliwym momencie (przed GC) .

W zależności w Menedżerze zabezpieczeń możesz nie być w stanie wykonać kodu.

 50
Author: Bohemian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:26

Używasz reflection, aby uzyskać dostęp do" szczegółów implementacji " obiektu string. Niezmienność jest cechą publicznego interfejsu obiektu.

 30
Author: Ankur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-06 07:30:38

Modyfikatory widoczności i ostateczna (tj. niezmienność) nie są miarą złośliwego kodu w Javie; są jedynie narzędziami do ochrony przed błędami i uczynienia kodu łatwiejszym do utrzymania (jeden z dużych punktów sprzedaży systemu). Dlatego możesz uzyskać dostęp do wewnętrznych szczegółów implementacji, takich jak tablica znaków pomocniczych dla Strings poprzez reflection.

Drugi efekt, jaki widzisz, to to, że wszystkie Stringzmieniają się, podczas gdy wygląda na to, że zmieniasz tylko s1. Jest to pewna własność Literały ciągów Javy, które są automatycznie internowane, tj. buforowane. Dwa literały łańcuchowe o tej samej wartości będą rzeczywiście tym samym obiektem. Gdy tworzysz ciąg znaków z new, nie zostanie on automatycznie internowany i nie zobaczysz tego efektu.

#substring do niedawna (Java 7u6) działała w podobny sposób, co wyjaśniałoby zachowanie w oryginalnej wersji twojego pytania. Nie utworzyła nowej tablicy znaków podkładu, ale użyła ponownie tej z oryginalnego łańcucha; to właśnie utworzono nowy obiekt String, który używał offsetu i długości, aby przedstawić tylko część tej tablicy. Zazwyczaj działa to jako ciągi są niezmienne-chyba, że obejdziesz to. Ta właściwość #substring oznaczała również, że cały oryginalny łańcuch nie może być zbierany jako śmieci, gdy krótszy podłańcuch utworzony z niego nadal istniał.

Od obecnej Javy i aktualnej wersji pytania nie ma dziwnego zachowania #substring.

 24
Author: Hauke Ingmar Schmidt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-06 14:18:12

Niezmienność ciągów jest z perspektywy interfejsu. Używasz reflection, aby ominąć interfejs i bezpośrednio zmodyfikować wewnętrzne instancje ciągu znaków.

s1 i s2 są zmieniane, ponieważ oba są przypisane do tej samej instancji łańcucha "intern". Możesz dowiedzieć się więcej o tej części z tego artykułu o równości łańcuchów i interning. Możesz być zaskoczony, aby dowiedzieć się, że w swoim przykładowym kodzie, s1 == s2 zwraca true!

 11
Author: Krease,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-06 07:39:59

Jakiej wersji Javy używasz? Od wersji Java 1.7.0_06 Oracle zmienił wewnętrzną reprezentację łańcucha znaków, zwłaszcza podłańcucha.

Cytowanie z Oracle Tunes Java ' s Internal String Representation :

W nowym paradygmacie pola offset I count zostały usunięte, więc podłańcuchy nie mają już tej samej wartości char [].

Z tą zmianą może się zdarzyć bez refleksji (???).

 10
Author: manikanta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-29 20:12:14

Są tu naprawdę dwa pytania:

  1. czy łańcuchy są niezmienne?
  2. Dlaczego s3 się nie zmienia?

Do punktu 1: poza ROM nie ma w komputerze pamięci niezmiennej. Obecnie nawet ROM jest czasami zapisywalny. Zawsze gdzieś jest jakiś kod (niezależnie od tego, czy jest to jądro, czy kod macierzysty pomijający zarządzane środowisko), który może zapisać się na adres pamięci. Tak więc w "rzeczywistości" nie są one absolutnie niezmienne.

Do punktu 2: dzieje się tak dlatego, że substring prawdopodobnie alokuje nową instancję string, która prawdopodobnie kopiuje tablicę. Można zaimplementować podłańcuch w taki sposób, że nie zrobi kopii, ale to nie znaczy, że tak. W grę wchodzą umowy handlowe.

Na przykład, czy trzymanie odniesienia do reallyLargeString.substring(reallyLargeString.length - 2) powinno spowodować, że duża ilość pamięci zostanie utrzymana przy życiu, czy tylko kilka bajtów?

To zależy od implementacji fragmentu. Głęboka Kopia utrzyma mniej pamięci przy życiu, ale będzie działać trochę wolniej. Płytka Kopia utrzyma więcej pamięci przy życiu, ale będzie szybsza. Użycie głębokiej kopii może również zmniejszyć fragmentację sterty, ponieważ obiekt string i jego bufor mogą być przydzielane w jednym bloku, w przeciwieństwie do dwóch oddzielnych alokacji sterty.

W każdym razie wygląda na to, że Twój JVM zdecydował się użyć głębokich kopii do wywołania fragmentu.

 7
Author: Scott Wisniewski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 00:35:54

Aby dodać do odpowiedzi @ haraldK - jest to hack bezpieczeństwa, który może prowadzić do poważnego wpływu w aplikacji.

Pierwszą rzeczą jest modyfikacja stałego łańcucha przechowywanego w Puli łańcuchów. Gdy string jest zadeklarowany jako String s = "Hello World";, jest umieszczany w specjalnej puli obiektów w celu dalszego potencjalnego ponownego użycia. Problem polega na tym, że kompilator umieści odniesienie do zmodyfikowanej wersji podczas kompilacji i gdy użytkownik zmodyfikuje ciąg przechowywany w tej puli w czasie wykonywania, wszystkie odniesienia w kodzie wskaże zmodyfikowaną wersję. Wynikałoby to z następującego błędu:

System.out.println("Hello World"); 

Wydrukuje:

Hello Java!

Był jeszcze jeden problem, którego doświadczyłem, gdy implementowałem ciężkie obliczenia na tak ryzykownych ciągach. Podczas obliczeń wystąpił błąd, który wystąpił 1 na 1000000 razy, co sprawiło, że wynik był nieokreślony. Udało mi się znaleźć problem, wyłączając JIT - zawsze otrzymywałem ten sam wynik z wyłączonym JIT. Domyślam się, że powodem czy to był haker, który złamał niektóre Kontrakty optymalizacji JIT.

 5
Author: Andrey Chaschev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-07 14:30:09

Zgodnie z koncepcją poolingu, wszystkie zmienne łańcuchowe zawierające tę samą wartość będą wskazywały na ten sam adres pamięci. Dlatego s1 i S2, oba zawierające tę samą wartość "Hello World", będą wskazywać na to samo miejsce pamięci (powiedzmy M1).

Z drugiej strony, s3 zawiera "świat", stąd wskazuje na inny przydział pamięci (powiedzmy M2).

Więc teraz dzieje się to, że wartość S1 jest zmieniana (używając wartości char []). Więc wartość w lokalizacja pamięci M1 wskazywana zarówno przez s1, jak i s2 została zmieniona.

Stąd w rezultacie, lokalizacja pamięci M1 została zmodyfikowana, co powoduje zmianę wartości s1 i s2.

Ale wartość lokalizacji M2 pozostaje niezmieniona, stąd s3 zawiera tę samą pierwotną wartość.

 5
Author: AbhijeetMishra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 00:28:46

Powodem, dla którego s3 tak naprawdę się nie zmienia, jest to, że w Javie podczas wykonywania fragmentu łańcucha wartości tablica znaków dla fragmentu jest wewnętrznie kopiowana(przy użyciu tablic.copyOfRange ()).

S1 i s2 są takie same, ponieważ w Javie oba odnoszą się do tego samego internowanego ciągu znaków. To projekt w Javie.

 4
Author: Maurizio In denmark,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 00:39:11

String jest niezmienny, ale poprzez odbicie możesz zmienić klasę String. Właśnie przedefiniowałeś klasę String jako zmienną w czasie rzeczywistym. Możesz na nowo zdefiniować metody, aby były publiczne, prywatne lub statyczne, jeśli chcesz.

 2
Author: SpacePrez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-03 00:30:15

[zastrzeżenie jest to celowo opiniowany styl odpowiedzi, ponieważ czuję, że odpowiedź bardziej "nie rób tego w domu dzieci" jest uzasadniona]

Sin to linia field.setAccessible(true);, która mówi o naruszeniu publicznego api poprzez umożliwienie dostępu do prywatnego pola. To gigantyczna dziura bezpieczeństwa, którą można zablokować, konfigurując menedżera zabezpieczeń.

Zjawiskiem w pytaniu są szczegóły implementacji, których nigdy nie zobaczysz, gdy nie użyjesz tej niebezpiecznej linii kodu do naruszania modyfikatory dostępu poprzez odbicie. Wyraźnie dwa (normalnie) niezmienne łańcuchy znaków mogą współdzielić tę samą tablicę znaków. To, czy podłańcuch ma tę samą tablicę, zależy od tego, czy może i czy deweloper chciał ją udostępnić. Zwykle są to niewidoczne szczegóły implementacji, o których nie powinieneś wiedzieć, chyba że strzelasz modyfikatorem dostępu przez głowę za pomocą tej linii kodu.

Po prostu nie jest dobrym pomysłem poleganie na takich szczegółach, których nie można doświadczyć bez naruszania modyfikatory dostępu za pomocą reflection. Właściciel tej klasy obsługuje tylko normalne publiczne API i może swobodnie wprowadzać zmiany w implementacji w przyszłości.

Powiedziawszy wszystko, że linijka kodu jest naprawdę bardzo przydatna, gdy broń trzyma cię w głowie, zmuszając cię do robienia tak niebezpiecznych rzeczy. Korzystanie z tych tylnych drzwi jest zwykle zapachem kodu, który musisz uaktualnić do lepszego kodu biblioteki, gdzie nie musisz grzeszyć. Innym powszechnym zastosowaniem tej niebezpiecznej linii kodu jest napisanie "voodoo framework" (orm, injection container, ...). Wielu ludzi jest przekonanych o takich frameworkach (zarówno za, jak i przeciw), więc uniknę wywołania wojny płomiennej, mówiąc nic innego, jak tylko zdecydowana większość programistów nie musi tam iść.

 1
Author: simbo1905,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-08 23:25:45

Ciągi znaków są tworzone w stałym obszarze pamięci JVM heap. Więc tak, jest to naprawdę niezmienne i nie można go zmienić po utworzeniu. Ponieważ w JVM są trzy rodzaje pamięci sterty: 1. Młode pokolenie 2. Old generation 3. Trwałe pokolenie.

Po utworzeniu dowolnego obiektu, przechodzi on do obszaru sterty young generation i obszaru PermGen zarezerwowanego dla poolingu łańcuchów.

Oto więcej szczegółów, z których możesz pobrać więcej informacji: Jak Śmieć Kolekcja działa w Javie .

 1
Author: Yasir Shabbir Choudhary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-05 18:10:52