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


399

Мы все знаем, что 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?


394
Вы можете делать всевозможные глупые трюки с отражением. Но вы в основном разбиваете наклейку «гарантия аннулируется, если ее уберут», как только вы это делаете.
Цао

16
@DarshanPatel использует SecurityManager, чтобы отключить рефлексию
Шон Патрик Флойд

39
Если вы действительно хотите возиться с вещами, вы можете сделать так, чтобы (Integer)1+(Integer)2=42возиться с кэшированным автобоксом; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Ричард Тингл,

15
Возможно, вас удивит этот ответ, который я написал почти 5 лет назад stackoverflow.com/a/1232332/27423 - он касается неизменяемых списков в C #, но в основном это одно и то же: как я могу помешать пользователям изменять мои данные? И ответ, вы не можете; отражение делает это очень легко. Одним из основных языков, у которого нет этой проблемы, является JavaScript, так как он не имеет системы отражения, которая может обращаться к локальным переменным внутри замыкания, поэтому частный действительно означает частный (даже если для него нет ключевого слова!)
Daniel Earwicker

49
Кто-нибудь читает вопрос до конца ?? Вопрос, позвольте мне повторить: «Почему эта программа работает так? Почему значения s1 и s2 изменяются, а не изменяются для s3?» Вопрос НЕ в том, почему s1 и s2 изменились! Вопрос: ПОЧЕМУ s3 не изменился?
Роланд Пихлакас

Ответы:


403

String является неизменным *, но это только означает, что вы не можете изменить его с помощью его открытого API.

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

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

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

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

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


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

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


7
На самом деле, Stringинтернирование является частью JLS ( «строковый литерал всегда ссылается на один и тот же экземпляр класса String» ). Но я согласен, не стоит рассчитывать на детали реализации Stringкласса.
haraldK

3
Возможно, причина, по которой substringкопии вместо использования «секции» существующего массива, заключается в том, что в противном случае, если бы у меня была огромная строка, sи я вытащил tиз нее крошечную подстроку , а позже я отказался от нее, sно сохранил ее t, тогда огромный массив остался бы живым (не мусор). Так что, может быть, для каждого строкового значения более естественно иметь собственный связанный массив?
Джеппе Стиг Нильсен

10
Совместное использование массивов между строкой и ее подстроками также подразумевает, что каждый String экземпляр должен иметь переменные для запоминания смещения в указанном массиве и длине. Это накладные расходы, которые нельзя игнорировать, учитывая общее количество строк и типичное соотношение между обычными строками и подстроками в приложении. Поскольку их нужно было оценивать для каждой строковой операции, это означало замедление каждой строковой операции только в пользу одной операции - дешевой подстроки.
Хольгер

2
@Holger - Да, я понимаю, что поле смещения было удалено в недавних JVM. И даже когда он присутствовал, его не так часто использовали.
Hot Licks

2
@supercat: не имеет значения, есть ли у вас собственный код или нет, наличие разных реализаций для строк и подстрок в одной и той же JVM или наличие byte[]строк для строк ASCII, а char[]для других подразумевает, что каждая операция должна проверять, какой тип строки это раньше эксплуатации. Это препятствует встраиванию кода в методы с использованием строк, что является первым шагом дальнейшей оптимизации с использованием контекстной информации вызывающей стороны. Это большое влияние.
Хольгер

93

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

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

инициализация

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

подстрока

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

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

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

изменение


Это работает только для литералов и является оптимизацией во время компиляции.
SpacePrez

2
@ Zaphod42 Не правда. Вы также можете позвонить internвручную на не-буквальную строку и воспользоваться ее преимуществами.
Крис Хейс

Обратите внимание: вы хотите использовать internразумно. Интернирование всего не приносит вам большой пользы и может стать источником некоторых головокружительных моментов, когда вы добавляете отражение в микс.
cHao

Test1и Test1не соответствуют test1==test2и не соблюдают соглашения об именах Java.
c0der

50

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

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

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

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


30

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


24

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

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

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

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


2
На самом деле, модификаторы видимости являются (или , по крайней мере , были) предназначены в качестве againts защиты вредоносного кода - однако, вам потребуется задать SecurityManager (System.setSecurityManager ()) , чтобы активировать защиту. Насколько это безопасно на самом деле - это другой вопрос ...
sleske

2
Заслуживает одобрения, потому что вы подчеркиваете, что модификаторы доступа не предназначены для «защиты» кода. Похоже, это широко неправильно понимается как в Java, так и в .NET. Хотя предыдущий комментарий действительно противоречит этому; Я не знаю много о Java, но в .NET это, безусловно, верно. Ни на одном из языков пользователи не должны предполагать, что это делает их код взломанным.
Том W

Невозможно нарушить договор finalдаже через размышления. Кроме того, как уже упоминалось в другом ответе, начиная с Java 7u6, #substringне разделяет массивы.
ntoskrnl

На самом деле, поведение finalсо временем изменилось ...: -O В соответствии с докладом Хайнца "Reflection Madness", который я разместил в другой ветке, он finalозначал финал в JDK 1.1, 1.3 и 1.4, но мог быть изменен с использованием отражения всегда с 1.2 и в 1,5 и 6 в большинстве случаев ...
haraldK

1
finalПоля могут быть изменены с помощью nativeкода, как это делается средой Serialization при чтении полей сериализованного экземпляра, а также при System.setOut(…)изменении конечной System.outпеременной. Последнее является наиболее интересной функцией, поскольку отражение с переопределением доступа не может изменять static finalполя.
Хольгер

11

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

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


10

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

Цитирование из Oracle Tunes Внутреннее строковое представление Java :

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

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


2
Если OP использовал более старую Sun / Oracle JRE, последний оператор вывел бы «Java!» (как он случайно написал). Это влияет только на совместное использование массива значений между строками и вложенными строками. Вы все еще не можете изменить значение без уловок, как отражение.
haraldK

7

Здесь действительно два вопроса:

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

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

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

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

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

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


3
Настоящий ROM так же неизменен, как и фотопечать в пластиковом корпусе. Шаблон устанавливается постоянно, когда пластина (или принт) химически развивается. Электрически изменяемые запоминающие устройства, в том числе микросхемы ОЗУ , могут вести себя как «истинное» ПЗУ, если на управляющие сигналы, необходимые для его записи, не может быть подано питание без добавления дополнительных электрических соединений в цепь, в которой оно установлено. На самом деле встроенные устройства нередко включают в себя ОЗУ, которое устанавливается на заводе и поддерживается резервной батареей, и содержимое которого необходимо будет перезагружать на заводе в случае отказа батареи.
суперкат

3
@supercat: Хотя ваш компьютер не входит в эти встроенные системы. :) Настоящие аппаратные ПЗУ не были распространены в ПК в течение десятилетия или двух; все ЭСППЗУ и вспышка в эти дни. В основном каждый видимый пользователю адрес, который относится к памяти, относится к потенциально доступной для записи памяти.
cHao

@cHao: Многие флеш-чипы позволяют защитить части от записи способом, который, если его вообще можно отменить, потребовал бы применения напряжений, отличных от того, который потребовался бы для нормальной работы (что материнские платы не будут оборудованы). Я ожидаю, что материнские платы будут использовать эту функцию. Кроме того, я не уверен насчет сегодняшних компьютеров, но исторически некоторые компьютеры имели область ОЗУ, которая была защищена от записи на этапе загрузки и могла быть незащищена только сбросом (который заставлял запускаться с ПЗУ).
суперкат

2
@supercat Я думаю, что вы упускаете смысл темы, которая заключается в том, что строки, хранящиеся в ОЗУ, никогда не будут по-настоящему неизменными.
Скотт Вишневски

5

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

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

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

Распечатает:

Hello Java!

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


Возможно, это была проблема безопасности потока, которая была замаскирована медленным временем выполнения и меньшим количеством параллелизма без JIT.
Тед Пеннингс

@TedPennings Из моего описания это могло, я просто не хотел вдаваться в подробности. На самом деле я потратил пару дней, пытаясь его локализовать. Это был однопоточный алгоритм, который вычислял расстояние между двумя текстами, написанными на двух разных языках. Я нашел два возможных решения этой проблемы - одно - отключить JIT, а второе - добавить буквально неактивные String.format("")внутри одной из внутренних петель. Существует вероятность того, что это будет проблема, связанная с каким-то иным, чем JIT, отказом, но я считаю, что это была JIT, потому что эта проблема больше не воспроизводилась после добавления этого параметра.
Андрей Чащев

Я делал это с ранней версией JDK ~ 7u9, так что это могло быть.
Андрей Чащев

1
@ Андрей Чащев: «Я нашел два возможных решения проблемы»… третье возможное решение, не взламывать Stringвнутренности, тебе не пришло в голову?
Хольгер

1
@Ted Pennings: проблемы безопасности потоков и проблемы JIT часто совпадают. JIT разрешено генерировать код, который опирается на finalгарантии безопасности потока поля, которые нарушаются при изменении данных после построения объекта. Таким образом, вы можете рассматривать это как проблему JIT или проблему MT, как вам нравится. Реальная проблема заключается во взломе Stringи изменении данных, которые, как ожидается, будут неизменными.
Хольгер

5

Согласно концепции объединения, все переменные String, содержащие одно и то же значение, будут указывать на один и тот же адрес памяти. Следовательно, s1 и s2, содержащие одинаковое значение «Hello World», будут указывать на одну и ту же ячейку памяти (скажем, M1).

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

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

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

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


5

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

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


2
Как этот ответ добавил что-либо к ответам перед вами?
Серый,

Также обратите внимание, что это довольно новое поведение, которое не гарантируется никакими спецификациями.
Паŭло Эберманн

Реализация String.substring(int, int)изменилась с Java 7u6. Перед 7u6, виртуальная машина будет просто держать указатель на оригинал String«S char[]вместе с индексом и длиной. После 7u6 он копирует подстроку в новую. StringЕсть плюсы и минусы.
Эрик Жаблоу

2

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


2
Если вы изменяете видимость полей / методов, это бесполезно, потому что во время компиляции они являются частными
Богемский

1
Вы можете изменить доступность методов, но вы не можете изменить их публичный / приватный статус и не можете сделать их статичными.
Серый,

1

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

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

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

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

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


1

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

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

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.