Является ли строка Java действительно неизменной?

Мы все знаем, что String неизменен в Java, но проверьте следующий код:

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  

Почему эта программа работает так? И почему изменилось значение s1 и s2, но не s3?

Ответ 1

String является неизменным *, но это означает, что вы не можете изменить его, используя свой общедоступный API.

Что вы здесь делаете, обходите обычный API, используя отражение. Точно так же вы можете изменить значения перечислений, изменить таблицу поиска, используемую в автооблоке Integer и т.д.

Теперь причина изменения s1 и s2 заключается в том, что они оба относятся к одной и той же интернированной строке. Компилятор делает это (как упоминалось в других ответах).

Причина s3 на самом деле не удивительно для меня, так как я думал, что она будет делиться массивом value (как это было в более ранней версии Java, перед Java 7u6). Однако, глядя на исходный код String, мы видим, что массив символов value для подстроки фактически копируется (используя Arrays.copyOfRange(..)). Вот почему он не меняется.

Вы можете установить SecurityManager, чтобы избежать вредоносного кода, чтобы делать такие вещи. Но имейте в виду, что некоторые библиотеки зависят от использования таких трюков для отражения (обычно это инструменты ORM, библиотеки AOP и т.д.).

*) Я изначально писал, что String не являются действительно неизменными, просто "эффективными неизменяемыми". Это может ввести в заблуждение в текущей реализации String, где массив value действительно отмечен private final. Однако все же стоит отметить, что невозможно объявить массив в Java неизменным, поэтому следует проявлять осторожность, чтобы не выставлять его вне его класса даже с соответствующими модификаторами доступа.


Поскольку эта тема кажется чрезвычайно популярной, здесь некоторые предложили дальнейшее чтение: Хайнц Кабуц Беседа о безумном размышлении от JavaZone 2009, которая охватывает множество проблем в OP, наряду с другими отражениями... ну... безумие.

Он объясняет, почему это иногда полезно. И почему, в большинстве случаев, вы должны избегать этого.: -)

Ответ 2

В Java, если две строковые примитивные переменные инициализируются в один и тот же литерал, он присваивает ту же ссылку обеим переменным:

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

initialization

Вот почему сравнение возвращает true. Третья строка создается с помощью substring(), которая создает новую строку вместо того, чтобы указывать на то же самое.

sub string

Когда вы получаете доступ к строке с использованием отражения, вы получаете фактический указатель:

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

Таким образом, изменение этого параметра изменит строку, содержащую указатель на нее, но поскольку s3 создается с новой строкой из-за substring(), это не изменится.

change

Ответ 3

Вы используете отражение, чтобы обойти неизменность String - это форма "атаки".

Есть много примеров, которые вы можете создать таким образом (например, вы даже можете создать экземпляр объекта Void), но это не значит, что String не "непреложный" .

Существуют случаи, когда этот тип кода может быть использован в ваших интересах и быть "хорошим кодированием" , например очистка паролей из памяти в самый ранний возможный момент (до GC).

В зависимости от менеджера безопасности вы не сможете выполнить свой код.

Ответ 4

Вы используете рефлексию для доступа к "деталям реализации" строкового объекта. Неизменность - это функция открытого интерфейса объекта.

Ответ 5

Модификаторы видимости и окончательные (т.е. неизменяемость) не являются мерой против вредоносного кода на Java; это всего лишь инструменты для защиты от ошибок и обеспечения более удобного обслуживания кода (одной из больших точек продажи системы). Вот почему вы можете получить доступ к внутренним деталям реализации, например массиву char для String через отражение.

Второй эффект, который вы видите, заключается в том, что все String изменяются, хотя похоже, что вы меняете только s1. Это определенное свойство литералов Java String, что они автоматически интернированы, т.е. Кэшируются. Два строковых литерала с одинаковым значением фактически будут одним и тем же объектом. Когда вы создаете String с new, он не будет интернирован автоматически и вы не увидите этого эффекта.

#substring до недавнего времени (Java 7u6) работал аналогичным образом, что объясняло бы поведение в исходной версии вашего вопроса. Он не создавал новый массив char, но повторно использовал один из исходной строки; он просто создал новый объект String, который использовал смещение и длину, чтобы представить только часть этого массива. Это обычно работало как Строки неизменны - если вы не обходите это. Это свойство #substring также означало, что вся исходная строка не могла быть сборкой мусора, когда была создана более короткая подстрока, созданная из нее.

С текущей Java и вашей текущей версией вопроса нет странного поведения #substring.

Ответ 6

Непрерывность строк от перспективы интерфейса. Вы используете отражение, чтобы обходить интерфейс и напрямую изменять внутренние элементы экземпляров String.

s1 и s2 оба изменены, поскольку оба они назначены одному и тому же экземпляру String. Вы можете узнать немного больше об этой части из этой статьи о равенстве строк и интернировании. Вы можете быть удивлены, узнав, что в вашем примере кода s1 == s2 возвращает true!

Ответ 7

Какую версию Java вы используете? Из Java 1.7.0_06 Oracle изменил внутреннее представление String, особенно подстроку.

Цитата из Oracle Tunes Java Internal String Representation:

В новой парадигме поля смещения и количества строк удалены, поэтому подстроки больше не разделяют базовое значение char [].

С этим изменением это может произойти без отражения (???).

Ответ 8

Здесь есть два вопроса:

  • Строки действительно неизменяемы?
  • Почему s3 не изменен?

В точку 1: за исключением ПЗУ на вашем компьютере нет неизменяемой памяти. В настоящее время даже ПЗУ иногда доступно для записи. Всегда есть какой-то код (будь то ядро ​​или собственный код, обойдя вашу управляемую среду), которые могут писать на ваш адрес памяти. Таким образом, в "реальности" они не являются абсолютно неизменяемыми.

В точку 2: это потому, что подстрока, вероятно, выделяет новый экземпляр строки, который, вероятно, копирует массив. Можно реализовать подстроку таким образом, что она не будет делать копию, но это не значит, что она делает. Существуют компромиссы.

Например, если ссылка на reallyLargeString.substring(reallyLargeString.length - 2) приведет к тому, что большой объем памяти будет сохранен в живых или всего несколько байтов?

Это зависит от того, как реализована подстрока. Глубокая копия будет сохранять меньше памяти, но она будет работать немного медленнее. Неглубокая копия сохранит больше памяти, но она будет быстрее. Использование глубокой копии также может уменьшить фрагментацию кучи, поскольку строковый объект и его буфер могут быть выделены в одном блоке, в отличие от двух отдельных распределений кучи.

В любом случае, похоже, ваш JVM решил использовать глубокие копии для вызовов подстроки.

Ответ 9

В соответствии с концепцией объединения всех переменных String, содержащих одно и то же значение, указывается один и тот же адрес памяти. Поэтому s1 и s2, оба из которых содержат одно и то же значение "Hello World", указывают на ту же ячейку памяти (скажем, M1).

С другой стороны, s3 содержит "Мир", поэтому он будет указывать на другое распределение памяти (скажем, M2).

Итак, теперь случается, что значение S1 изменяется (используя значение char []). Таким образом, значение в ячейке памяти M1, обозначенное как s1, так и s2, было изменено.

Следовательно, в результате была изменена ячейка памяти M1, которая вызывает изменение значений s1 и s2.

Но значение местоположения M2 остается неизменным, поэтому s3 содержит одно и то же исходное значение.

Ответ 10

Чтобы добавить к ответу @haraldK - это взлом безопасности, который может привести к серьезному воздействию на приложение.

Прежде всего, это изменение постоянной строки, хранящейся в пуле строк. Когда строка объявляется как String s = "Hello World";, она помещается в специальный пул объектов для дальнейшего потенциального повторного использования. Проблема заключается в том, что компилятор разместит ссылку на модифицированную версию во время компиляции, и после изменения пользователем строки, хранящейся в этом пуле во время выполнения, все ссылки в коде указывают на измененную версию. Это приведет к следующей ошибке:

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

Будет напечатан:

Hello Java!

Была другая проблема, которую я испытал, когда я выполнял тяжелые вычисления над такими рискованными строками. В ходе вычислений произошла ошибка, которая произошла как 1 из 1000000 раз, что сделало результат недетерминированным. Я смог найти проблему, отключив JIT - я всегда получал тот же результат, когда JIT отключился. Я предполагаю, что причиной этого был взлом безопасности String, который нарушил некоторые из контрактов оптимизации JIT.

Ответ 11

Причина s3 фактически не меняется, потому что в Java, когда вы выполняете подстроку, массив символов значения для подстроки внутренне скопирован (используя Arrays.copyOfRange()).

s1 и s2 совпадают, поскольку в Java оба они ссылаются на одну и ту же интернированную строку. Он по дизайну в Java.

Ответ 12

Строка неизменна, но через отражение вы можете изменить класс String. Вы только что переопределили класс String как изменяемый в режиме реального времени. Вы можете переопределить методы, которые должны быть общедоступными или частными или статичными, если вы захотите.

Ответ 13

[Отказ от ответственности это сознательно упрямый стиль ответа, поскольку я чувствую, что "не делайте этого дома дети" ответ оправдан)

Грех - это строка field.setAccessible(true);, которая гласит, что нарушает public api, разрешая доступ к закрытому полю. Это гигантское отверстие безопасности, которое можно заблокировать, настроив менеджера безопасности.

Явление в вопросе - это детали реализации, которые вы никогда не увидите, когда не используете эту опасную строку кода, чтобы нарушать модификаторы доступа посредством отражения. Очевидно, что две (обычно) неизменяемые строки могут совместно использовать один и тот же массив char. Независимо от того, зависит ли подстрока с одним и тем же массивом, может ли разработчик подумать об этом. Обычно это невидимые детали реализации, которые вам не нужно знать, если вы не стреляете в модификатор доступа через голову с помощью этой строки кода.

Просто не стоит полагаться на такие детали, которые не могут быть испытаны без нарушения модификаторов доступа с использованием отражения. Владелец этого класса поддерживает только обычный публичный API и может вносить изменения в реализацию в будущем.

Сказав все, что строка кода действительно очень полезна, когда у вас есть пистолет, вы держите голову, заставляя вас делать такие опасные вещи. Использование этой задней двери обычно является запахом кода, который вам нужно обновить до лучшего кода библиотеки, где вам не нужно грешить. Другим распространенным применением этой опасной строки кода является запись "рамки voodoo" (orm, контейнер для инъекций,...). Многие люди становятся религиозными в таких рамках (как для них, так и против них), поэтому я избегу приглашать пламенную войну, говоря ничего, кроме подавляющего большинства программистов, которые не должны туда идти.

Ответ 14

Строки создаются в постоянной области памяти кучи JVM. Так что да, это действительно неизменно и не может быть изменено после создания. Поскольку в JVM существует три типа памяти кучи:  1. Молодое поколение  2. Старое поколение  3. Постоянное поколение.

Когда какой-либо объект создается, он переходит в область кучи молодого поколения и область PermGen, зарезервированную для объединения строк.

Ниже вы можете перейти и получить дополнительную информацию от: Как работает сборка мусора в Java.

Ответ 15

Вы можете получить четкое представление о вопросе "Почему класс String предназначен для неизменяемости", подробно прочитав причину из здесь

Изучение класса String даст вам четкое представление о том, как он создан, чтобы стать неизменным Щелкните здесь, чтобы изучить класс строк