Wszyscy wiemy, że String
jest niezmienny 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 jest wartość s1
i s2
zmieniane, ale nie s3
?
java
string
reflection
immutability
Darshan Patel
źródło
źródło
(Integer)1+(Integer)2=42
buforowanym autoboxingiem; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )Odpowiedzi:
String
jest niezmienny *, ale oznacza to tylko, że nie można go zmienić za pomocą jego publicznego interfejsu API.To, co tu robisz, to obchodzenie normalnego API za pomocą odbicia. W ten sam sposób możesz zmienić wartości wyliczeń, zmienić tabelę odnośników używaną w autoboksowaniu liczb całkowitych itp.
Powodem
s1
is2
wartością zmiany jest to, że oba odnoszą się do tego samego łańcucha wewnętrznego. Kompilator to robi (jak wspomniano w innych odpowiedziach).Powodem
s3
jest nie był faktycznie nieco zaskakujące dla mnie, jak myślałem, że dzielićvalue
array ( miało to miejsce w poprzedniej wersji Java , zanim 7u6 Java). Jednak patrząc na kod źródłowyString
, widzimy, żevalue
tablica znaków dla podłańcucha jest faktycznie kopiowana (za pomocąArrays.copyOfRange(..)
). Dlatego pozostaje niezmieniony.Możesz zainstalować
SecurityManager
, aby uniknąć złośliwego kodu, aby robić takie rzeczy. Pamiętaj jednak, że niektóre biblioteki zależą od korzystania z tego rodzaju sztuczek refleksyjnych (zazwyczaj narzędzia ORM, biblioteki AOP itp.).*) Początkowo napisałem, że
String
tak naprawdę nie są niezmienne, tylko „skuteczne niezmienne”. Może to wprowadzać w błąd w obecnej implementacjiString
, gdzievalue
tablica jest rzeczywiście oznaczonaprivate final
. Warto jednak zauważyć, że nie ma sposobu, aby zadeklarować tablicę w Javie jako niezmienną, dlatego należy zachować ostrożność, aby nie ujawniać jej poza klasą, nawet przy odpowiednich modyfikatorach dostępu.Ponieważ ten temat wydaje się niezwykle popularny, oto kilka sugerowanych lektur: Dyskusja Heinza Kabutza Reflection Madness z JavaZone 2009, która obejmuje wiele problemów w OP, wraz z innymi refleksjami ... cóż ... szaleństwem.
Obejmuje to, dlaczego czasami jest to przydatne. I dlaczego w większości przypadków należy tego unikać. :-)
źródło
String
internowanie jest częścią JLS ( „literał łańcuchowy zawsze odnosi się do tego samego wystąpienia klasy Łańcuch” ). Ale zgadzam się, że nie jest dobrą praktyką poleganie na szczegółach implementacjiString
klasy.substring
kopie zamiast używać „sekcji” istniejącej tablicy, jest inaczej, gdybym miał ogromny ciągs
i wyjął z niego niewielki podciągt
, a potem porzuciłem,s
ale utrzymałemt
, wtedy ogromna tablica byłaby przy życiu (nie zbierane są śmieci). Więc może bardziej naturalne jest, aby każda wartość łańcucha miała własną powiązaną tablicę?String
instancja musiała przenosić zmienne w celu zapamiętania przesunięcia w określonej tablicy i długości. Jest to narzut, którego nie należy ignorować, biorąc pod uwagę całkowitą liczbę łańcuchów i typowy stosunek normalnych łańcuchów do podciągów w aplikacji. Ponieważ musieli być oceniani dla każdej operacji łańcuchowej, oznaczało to spowolnienie każdej operacji łańcuchowej tylko dla korzyści jednej operacji, taniego podłańcucha.byte[]
ciągów dla ciągów ASCII ichar[]
innych oznacza, że każda operacja musi sprawdzić, jaki rodzaj ciągu jest wcześniej operacyjny. Utrudnia to wstawianie kodu do metod wykorzystujących ciągi, co jest pierwszym krokiem do dalszych optymalizacji z wykorzystaniem informacji kontekstowej osoby dzwoniącej. To duży wpływ.W Javie, jeśli dwie pierwotne zmienne łańcuchowe są inicjowane na ten sam literał, przypisuje to samo odwołanie do obu zmiennych:
Z tego powodu porównanie zwraca wartość true. Trzeci ciąg jest tworzony za pomocą,
substring()
który tworzy nowy ciąg zamiast wskazywać na to samo.Gdy uzyskujesz dostęp do ciągu za pomocą odbicia, otrzymujesz rzeczywisty wskaźnik:
Tak więc zmiana na to spowoduje zmianę łańcucha przytrzymującego do niego wskaźnik, ale tak jak
s3
został utworzony z nowym ciągiem, ponieważsubstring()
nie zmieni się.źródło
intern
ręcznie na nieliterowym ciągu znaków i czerpać korzyści.intern
rozsądku. Internowanie wszystkiego nie daje wiele korzyści i może być źródłem chwil pochłaniających uwagę, gdy dodasz refleksji do miksu.Test1
iTest1
są niezgodne ztest1==test2
konwencjami nazewnictwa Java i nie są z nimi zgodne.Używasz refleksji, aby obejść niezmienność Sznurka - jest to forma „ataku”.
Istnieje wiele przykładów, które możesz stworzyć w ten sposób (np. Możesz nawet utworzyć instancję
Void
obiektu ), ale to nie znaczy, że String nie jest „niezmienny”.Istnieją przypadki użycia, w których ten rodzaj kodu może być wykorzystany na Twoją korzyść i być „dobrym kodowaniem”, na przykład usuwanie haseł z pamięci w najwcześniejszym możliwym momencie (przed GC) .
W zależności od menedżera bezpieczeństwa wykonanie kodu może nie być możliwe.
źródło
Używasz refleksji, aby uzyskać dostęp do „szczegółów implementacji” obiektu string. Niezmienność jest cechą publicznego interfejsu obiektu.
źródło
Modyfikatory widoczności i końcowa (tj. Niezmienność) nie są miarą złośliwego kodu w Javie; są jedynie narzędziami służącymi do ochrony przed błędami i ułatwienia zarządzania kodem (jedna z głównych zalet systemu). Dlatego możesz uzyskać dostęp do wewnętrznych szczegółów implementacji, takich jak tablica charcentów dla
String
s poprzez odbicie.Drugim efektem, który widzisz, jest to, że wszystko się
String
zmienia, podczas gdy wygląda na to, że się zmieniaszs1
. Jest to pewna właściwość literałów Java String, które są automatycznie internowane, tj. Buforowane. Dwa literały łańcuchowe o tej samej wartości będą faktycznie tym samym obiektem. Gdy utworzysz znew
nim Łańcuch, nie zostanie on automatycznie internowany i nie zobaczysz tego efektu.#substring
do niedawna (Java 7u6) działał w podobny sposób, co wyjaśniałoby zachowanie w oryginalnej wersji twojego pytania. Nie stworzył on nowej tablicy char, ale użył ponownie tej z oryginalnego String; właśnie utworzył nowy obiekt String, który wykorzystał przesunięcie i długość, aby przedstawić tylko część tej tablicy. Ogólnie działało to, ponieważ ciągi są niezmienne - chyba że je obejdziesz. Ta właściwość#substring
oznaczała również, że cały oryginalny Ciąg nie mógł zostać wyrzucony przez śmieci, gdy jeszcze utworzono z niego krótszy podciąg.W obecnej Javie i twojej obecnej wersji pytania nie ma dziwnego zachowania
#substring
.źródło
final
nawet poprzez refleksję. Ponadto, jak wspomniano w innej odpowiedzi, ponieważ Java 7u6#substring
nie udostępnia tablic.final
zmieniło się z czasem ...: -O Zgodnie z przemówieniem Heinza „Reflection Madness”, które zamieściłem w innym wątku,final
oznaczonym jako ostatecznym w JDK 1.1, 1.3 i 1.4, ale można je modyfikować za pomocą odbicia za pomocą 1.2 zawsze , aw większości przypadków 1,5 i 6 ...final
pola mogą być zmieniane za pomocąnative
kodu, tak jak robi to środowisko Serialization podczas odczytywania pól serializowanej instancji, a takżeSystem.setOut(…)
modyfikuje ostatniąSystem.out
zmienną. Ta ostatnia jest najbardziej interesującą funkcją, ponieważ odbicie z zastąpieniem dostępu nie może zmieniaćstatic final
pól.Niezmienność łańcucha jest z perspektywy interfejsu. Używasz refleksji, aby ominąć interfejs i bezpośrednio zmodyfikować elementy wewnętrzne instancji String.
s1
is2
oba zostały zmienione, ponieważ oba są przypisane do tej samej instancji ciągu „intern”. Więcej informacji na temat tej części można znaleźć w tym artykule na temat równości łańcuchów i internowania. Możesz być zaskoczony, gdy dowiesz się, że w przykładowym kodzies1 == s2
powracatrue
!źródło
Jakiej wersji Java używasz? Z Java 1.7.0_06 Oracle zmieniło wewnętrzną reprezentację String, szczególnie podłańcuch.
Cytowanie z wewnętrznej reprezentacji ciągów Java Oracle Tunes :
Przy tej zmianie może się zdarzyć bez refleksji (???).
źródło
Są tutaj naprawdę dwa pytania:
Do punktu 1: Z wyjątkiem ROM, nie ma niezmiennej pamięci w twoim komputerze. W dzisiejszych czasach nawet ROM jest czasami zapisywalny. Zawsze gdzieś jest jakiś kod (czy to jądro, czy kod natywny omijający środowisko zarządzane), który może zapisać na adres pamięci. Tak więc w „rzeczywistości” nie, nie są one absolutnie niezmienne.
Do punktu 2: Jest tak, ponieważ podciąg prawdopodobnie przydziela nową instancję ciągu, która prawdopodobnie kopiuje tablicę. Możliwe jest zaimplementowanie podciągów w taki sposób, że nie zrobi ono kopii, ale to nie znaczy, że tak. W grę wchodzą kompromisy.
Na przykład, czy posiadanie odwołania powinno
reallyLargeString.substring(reallyLargeString.length - 2)
powodować utrzymywanie dużej ilości pamięci przy życiu, czy tylko kilka bajtów?To zależy od sposobu implementowania podciągów. Głęboka kopia utrzyma mniej pamięci przy życiu, ale będzie działać nieco wolniej. Płytka kopia zachowa więcej pamięci, ale będzie szybsza. Korzystanie z głębokiej kopii może również zmniejszyć fragmentację sterty, ponieważ obiekt łańcucha i jego bufor można przydzielić w jednym bloku, w przeciwieństwie do 2 oddzielnych przydziałów sterty.
W każdym razie wygląda na to, że JVM wybrał głębokie kopie do wywołań podłańcuchów.
źródło
Aby dodać do odpowiedzi @ haraldK - jest to hack bezpieczeństwa, który może prowadzić do poważnego wpływu na aplikację.
Pierwszą rzeczą jest modyfikacja stałego ciągu przechowywanego w puli ciągów. Kiedy ciąg znaków jest zadeklarowany jako
String s = "Hello World";
, jest umieszczany w specjalnej puli obiektów w celu dalszego potencjalnego ponownego wykorzystania. Problem polega na tym, że kompilator umieści odniesienie do zmodyfikowanej wersji w czasie kompilacji, a gdy użytkownik zmodyfikuje ciąg przechowywany w tej puli w czasie wykonywania, wszystkie odniesienia w kodzie będą wskazywać na zmodyfikowaną wersję. Spowodowałoby to następujący błąd:Wydrukuje:
Był jeszcze jeden problem, który napotkałem, gdy wdrażałem ciężkie obliczenia w odniesieniu do tak ryzykownych ciągów. Wystąpił błąd, który wystąpił mniej więcej 1 na 1000000 razy podczas obliczeń, co spowodowało, że wynik był nieokreślony. Znalazłem problem, wyłączając JIT - zawsze otrzymywałem ten sam rezultat przy wyłączonym JIT. Domyślam się, że powodem był włamanie do systemu String, które złamało niektóre kontrakty optymalizacyjne JIT.
źródło
String.format("")
w jednej z wewnętrznych pętli. Jest szansa, że będzie to problem z innym niepowodzeniem niż JIT, ale uważam, że był to JIT, ponieważ ten problem nigdy nie został powtórzony po dodaniu tego braku działania.String
wnętrza, nie przyszła Ci do głowy?final
gwarancjach bezpieczeństwa wątku pola, które psują się podczas modyfikowania danych po zbudowaniu obiektu. Możesz więc postrzegać go jako problem JIT lub MT, tak jak chcesz. Prawdziwym problemem jest włamanie sięString
i modyfikacja danych, które mają być niezmienne.Zgodnie z koncepcją pulowania wszystkie zmienne String 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ły na tę samą lokalizację pamięci (powiedzmy M1).
Z drugiej strony s3 zawiera „Świat”, stąd będzie wskazywał na inny przydział pamięci (powiedzmy M2).
Teraz dzieje się tak, że wartość S1 jest zmieniana (przy użyciu wartości char []). Tak więc wartość w miejscu pamięci M1 wskazanym zarówno przez s1, jak i s2 została zmieniona.
W rezultacie zmieniono lokalizację pamięci M1, co powoduje zmianę wartości s1 i s2.
Ale wartość położenia M2 pozostaje niezmieniona, dlatego s3 zawiera tę samą pierwotną wartość.
źródło
Powodem, dla którego s3 tak naprawdę się nie zmienia, jest to, że w Javie podczas wykonywania podłańcucha tablica znaków wartości podłańcucha jest wewnętrznie kopiowana (przy użyciu Arrays.copyOfRange ()).
s1 i s2 są takie same, ponieważ w Javie oba odnoszą się do tego samego łańcucha wewnętrznego. Jest to zaprojektowane w Javie.
źródło
String.substring(int, int)
zmieniona w Javie 7u6. Przed 7u6, JVM będzie tylko utrzymać wskaźnik do oryginałuString
„schar[]
wraz z indeksem i długości. Po 7u6 kopiuje podłańcuch do nowego.String
Są plusy i minusy.String jest niezmienny, ale poprzez odbicie możesz zmienić klasę String. Właśnie zdefiniowałeś klasę String jako zmienną w czasie rzeczywistym. Jeśli chcesz, możesz przedefiniować metody, aby były publiczne, prywatne lub statyczne.
źródło
[Uwaga: jest to celowo wyrażony styl odpowiedzi, ponieważ uważam, że odpowiedź „nie rób tego w domu” jest uzasadniona]
Grzech jest linią,
field.setAccessible(true);
która mówi, że narusza publiczny interfejs API, umożliwiając dostęp do prywatnego pola. To gigantyczna dziura w zabezpieczeniach, którą można zamknąć, konfigurując menedżera bezpieczeństwa.Zjawiskiem w pytaniu są szczegóły implementacji, których nigdy nie zobaczysz, gdy nie użyjesz tego niebezpiecznego wiersza kodu do naruszenia modyfikatorów dostępu poprzez odbicie. Oczywiście dwa (normalnie) niezmienne ciągi mogą dzielić tę samą tablicę znaków. To, czy podciąg dzieli tę samą tablicę, zależy od tego, czy może i czy deweloper pomyślał o udostępnieniu go. Zwykle są to niewidoczne szczegóły implementacji, których nie powinieneś znać, chyba że strzelisz do modyfikatora dostępu za pomocą tego wiersza kodu.
Po prostu nie jest dobrym pomysłem poleganie na takich szczegółach, których nie można doświadczyć bez naruszenia modyfikatorów dostępu za pomocą odbicia. Właściciel tej klasy obsługuje tylko normalny publiczny interfejs API i może wprowadzać zmiany implementacyjne w przyszłości.
Powiedziawszy wszystko, że linia kodu jest naprawdę bardzo przydatna, gdy masz pistolet, który trzyma cię za głowę, zmuszając cię do robienia tak niebezpiecznych rzeczy. Korzystanie z tych tylnych drzwi jest zwykle zapachem kodu, który należy uaktualnić do lepszego kodu biblioteki, w którym nie trzeba grzeszyć. Innym powszechnym zastosowaniem tego niebezpiecznego wiersza kodu jest napisanie „frameworka voodoo” (orm, kontener wstrzykiwania, ...). Wielu ludzi wierzy w takie ramy (zarówno za, jak i przeciw nim), więc uniknę zaproszenia do wojny z płomieniami, mówiąc, że znaczna większość programistów nie musi tam iść.
źródło
Ciągi są tworzone w stałym obszarze pamięci sterty JVM. Tak, to jest naprawdę niezmienne i nie można go zmienić po utworzeniu. Ponieważ w JVM istnieją trzy typy pamięci sterty: 1. Młoda generacja 2. Stara generacja 3. Generacja stała.
Po utworzeniu dowolnego obiektu przechodzi on w obszar sterty młodego pokolenia i obszar PermGen zarezerwowany dla puli ciągów.
Oto więcej szczegółów, z których możesz przejść i pobrać więcej informacji: Jak działa Garbage Collection w Javie .
źródło
Ciąg jest niezmienny z natury, ponieważ nie ma metody modyfikowania obiektu String. Dlatego wprowadzili klasy StringBuilder i StringBuffer
źródło