Мы все знаем, что 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
?
java
string
reflection
immutability
Даршан Патель
источник
источник
(Integer)1+(Integer)2=42
возиться с кэшированным автобоксом; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )Ответы:
String
является неизменным *, но это только означает, что вы не можете изменить его с помощью его открытого API.То, что вы делаете здесь, - это обход нормального API с использованием рефлексии. Таким же образом вы можете изменить значения перечислений, изменить таблицу поиска, используемую в автобоксах Integer и т. Д.
Теперь причина
s1
иs2
значение изменения в том, что они оба ссылаются на одну и ту же интернированную строку. Компилятор делает это (как указано в других ответах).Причина
s3
вовсе не было на самом деле немного удивительно для меня, как я думал , что это будет делитьvalue
массив ( это было в предыдущей версии Java до Java 7u6). Однако, глядя на исходный кодString
, мы видим, чтоvalue
массив символов для подстроки на самом деле копируется (используяArrays.copyOfRange(..)
). Вот почему он остается неизменным.Вы можете установить
SecurityManager
, чтобы избежать вредоносного кода, чтобы делать такие вещи. Но имейте в виду, что некоторые библиотеки зависят от использования таких трюков отражения (обычно это инструменты ORM, библиотеки AOP и т. Д.).*) Я изначально писал, что
String
s на самом деле не являются неизменяемыми, просто «эффективные неизменяемыми». Это может ввести в заблуждение в текущей реализацииString
, гдеvalue
массив действительно отмеченprivate final
. Тем не менее, все же стоит отметить, что в Java нет способа объявить массив неизменным, поэтому следует позаботиться о том, чтобы не раскрывать его вне своего класса, даже с соответствующими модификаторами доступа.Поскольку эта тема кажется невероятно популярной, вот некоторые из них, предлагаемые для дальнейшего чтения: доклад Хайнца Кабуца о безумии отражения от JavaZone 2009, который охватывает многие проблемы в OP, а также другие размышления ... ну ... безумие.
Это объясняет, почему это иногда полезно. И почему, в большинстве случаев, вам следует избегать этого. :-)
источник
String
интернирование является частью JLS ( «строковый литерал всегда ссылается на один и тот же экземпляр класса String» ). Но я согласен, не стоит рассчитывать на детали реализацииString
класса.substring
копии вместо использования «секции» существующего массива, заключается в том, что в противном случае, если бы у меня была огромная строка,s
и я вытащилt
из нее крошечную подстроку , а позже я отказался от нее,s
но сохранил ееt
, тогда огромный массив остался бы живым (не мусор). Так что, может быть, для каждого строкового значения более естественно иметь собственный связанный массив?String
экземпляр должен иметь переменные для запоминания смещения в указанном массиве и длине. Это накладные расходы, которые нельзя игнорировать, учитывая общее количество строк и типичное соотношение между обычными строками и подстроками в приложении. Поскольку их нужно было оценивать для каждой строковой операции, это означало замедление каждой строковой операции только в пользу одной операции - дешевой подстроки.byte[]
строк для строк ASCII, аchar[]
для других подразумевает, что каждая операция должна проверять, какой тип строки это раньше эксплуатации. Это препятствует встраиванию кода в методы с использованием строк, что является первым шагом дальнейшей оптимизации с использованием контекстной информации вызывающей стороны. Это большое влияние.В Java, если две строковые примитивные переменные инициализируются одним и тем же литералом, он назначает одну и ту же ссылку обеим переменным:
Вот почему сравнение возвращает истину. Третья строка создается с помощью
substring()
которой создает новую строку вместо того, чтобы указывать на то же самое.Когда вы получаете доступ к строке, используя отражение, вы получаете фактический указатель:
Поэтому изменение на это изменит строку, содержащую указатель на нее, но, поскольку
s3
она создается с новой строкой,substring()
она не изменится.источник
intern
вручную на не-буквальную строку и воспользоваться ее преимуществами.intern
разумно. Интернирование всего не приносит вам большой пользы и может стать источником некоторых головокружительных моментов, когда вы добавляете отражение в микс.Test1
иTest1
не соответствуютtest1==test2
и не соблюдают соглашения об именах Java.Вы используете отражение, чтобы обойти неизменность String - это форма «атаки».
Есть много примеров, которые вы можете создать подобным образом (например, вы можете даже создать экземпляр
Void
объекта ), но это не значит, что String не является «неизменяемым».Существуют случаи, когда этот тип кода может использоваться в ваших интересах и быть «хорошим кодом», например, удаление паролей из памяти в кратчайшие возможные сроки (до GC) .
В зависимости от менеджера безопасности, вы не сможете выполнить свой код.
источник
Вы используете отражение для доступа к «деталям реализации» строкового объекта. Неизменность - это особенность открытого интерфейса объекта.
источник
Модификаторы видимости и финал (то есть неизменяемость) не являются мерой против вредоносного кода в Java; это всего лишь инструменты для защиты от ошибок и повышения удобства сопровождения кода (один из главных преимуществ системы). Вот почему вы можете получить доступ к внутренним деталям реализации, например к массиву вспомогательных символов, для
String
s с помощью отражения.Второй эффект, который вы видите, заключается в том, что все
String
изменяются, а похоже, что вы меняетесьs1
. Это определенное свойство литералов Java String, что они автоматически интернируются, то есть кэшируются. Два строковых литерала с одинаковым значением фактически будут одним и тем же объектом. Когда вы создаете строку сnew
ней, она не будет автоматически интернирована, и вы не увидите этого эффекта.#substring
до недавнего времени (Java 7u6) работал аналогичным образом, что объясняло бы поведение в исходной версии вашего вопроса. Он не создавал новый массив вспомогательных символов, но использовал тот же, что и в исходной строке; он просто создал новый объект String, который использовал смещение и длину, чтобы представить только часть этого массива. Обычно это работает, так как строки неизменны - если вы не обойдете это. Это свойство#substring
также означало, что вся оригинальная строка не может быть собрана сборщиком мусора, когда еще существует более короткая подстрока, созданная из нее.Что касается текущей Java и вашей текущей версии вопроса, то здесь нет странного поведения
#substring
.источник
final
даже через размышления. Кроме того, как уже упоминалось в другом ответе, начиная с Java 7u6,#substring
не разделяет массивы.final
со временем изменилось ...: -O В соответствии с докладом Хайнца "Reflection Madness", который я разместил в другой ветке, онfinal
означал финал в JDK 1.1, 1.3 и 1.4, но мог быть изменен с использованием отражения всегда с 1.2 и в 1,5 и 6 в большинстве случаев ...final
Поля могут быть изменены с помощьюnative
кода, как это делается средой Serialization при чтении полей сериализованного экземпляра, а также приSystem.setOut(…)
изменении конечнойSystem.out
переменной. Последнее является наиболее интересной функцией, поскольку отражение с переопределением доступа не может изменятьstatic final
поля.Строковая неизменность с точки зрения интерфейса. Вы используете отражение, чтобы обойти интерфейс и напрямую изменить внутреннее содержимое экземпляров String.
s1
иs2
оба они изменены, потому что они оба назначены одному и тому же «внутреннему» экземпляру String. Вы можете узнать немного больше об этой части из этой статьи о равенстве строк и интернировании. Вы можете быть удивлены, узнав, что в вашем примере кодаs1 == s2
возвращаетсяtrue
!источник
Какую версию Java вы используете? Начиная с Java 1.7.0_06, Oracle изменила внутреннее представление String, особенно подстроку.
Цитирование из Oracle Tunes Внутреннее строковое представление Java :
С этим изменением это может произойти без размышлений (???).
источник
Здесь действительно два вопроса:
К пункту 1: Кроме ПЗУ на вашем компьютере нет неизменной памяти. В наше время даже ROM иногда доступен для записи. Где-то всегда есть какой-то код (будь то ядро или собственный код, обходящий вашу управляемую среду), который может записать ваш адрес памяти. Так что, в «реальности», нет, они не абсолютно неизменны.
К пункту 2: Это потому, что подстрока, вероятно, выделяет новый экземпляр строки, который, вероятно, копирует массив. Возможно реализовать подстроку так, чтобы она не делала копию, но это не значит, что она делает. Здесь есть компромиссы.
Например, должен содержать ссылку на
reallyLargeString.substring(reallyLargeString.length - 2)
вызывать сохранение большого объема памяти или всего несколько байтов?Это зависит от того, как подстрока реализована. Глубокая копия сохранит меньше памяти, но будет работать немного медленнее. Малая копия сохранит больше памяти, но будет быстрее. Использование глубокой копии также может уменьшить фрагментацию кучи, поскольку строковый объект и его буфер могут быть размещены в одном блоке, в отличие от 2 отдельных назначений кучи.
В любом случае, похоже, что ваша JVM решила использовать глубокие копии для вызовов подстроки.
источник
Чтобы добавить к ответу @ haraldK - это взлом безопасности, который может привести к серьезным последствиям в приложении.
Во-первых, это модификация константной строки, хранящейся в пуле строк. Когда строка объявлена как a
String s = "Hello World";
, она помещается в специальный пул объектов для дальнейшего потенциального повторного использования. Проблема в том, что компилятор поместит ссылку на измененную версию во время компиляции, и как только пользователь изменит строку, хранящуюся в этом пуле, во время выполнения, все ссылки в коде будут указывать на измененную версию. Это приведет к следующей ошибке:Распечатает:
Была еще одна проблема, с которой я столкнулся, когда выполнял тяжелые вычисления на таких рискованных строках. Была ошибка, которая произошла примерно 1 из 1000000 раз во время вычислений, что сделало результат неопределенным. Я смог найти проблему, отключив JIT - я всегда получал один и тот же результат с выключенным JIT. Я предполагаю, что причиной этого был взлом безопасности String, который нарушил некоторые контракты по оптимизации JIT.
источник
String.format("")
внутри одной из внутренних петель. Существует вероятность того, что это будет проблема, связанная с каким-то иным, чем JIT, отказом, но я считаю, что это была JIT, потому что эта проблема больше не воспроизводилась после добавления этого параметра.String
внутренности, тебе не пришло в голову?final
гарантии безопасности потока поля, которые нарушаются при изменении данных после построения объекта. Таким образом, вы можете рассматривать это как проблему JIT или проблему MT, как вам нравится. Реальная проблема заключается во взломеString
и изменении данных, которые, как ожидается, будут неизменными.Согласно концепции объединения, все переменные String, содержащие одно и то же значение, будут указывать на один и тот же адрес памяти. Следовательно, s1 и s2, содержащие одинаковое значение «Hello World», будут указывать на одну и ту же ячейку памяти (скажем, M1).
С другой стороны, s3 содержит «World», следовательно, он будет указывать на другое распределение памяти (скажем, M2).
Так что теперь происходит то, что значение S1 изменяется (используя значение char []). Таким образом, значение в ячейке памяти M1, на которую указывают s1 и s2, было изменено.
Следовательно, в результате ячейка памяти M1 была изменена, что вызывает изменение значений s1 и s2.
Но значение местоположения M2 остается неизменным, поэтому s3 содержит то же самое исходное значение.
источник
Причина, по которой s3 фактически не изменяется, заключается в том, что в Java при выполнении подстроки массив символов значения для подстроки копируется изнутри (с использованием Arrays.copyOfRange ()).
s1 и s2 одинаковы, потому что в Java они оба ссылаются на одну и ту же интернированную строку. Это по дизайну на Java.
источник
String.substring(int, int)
изменилась с Java 7u6. Перед 7u6, виртуальная машина будет просто держать указатель на оригиналString
«Schar[]
вместе с индексом и длиной. После 7u6 он копирует подстроку в новую.String
Есть плюсы и минусы.String является неизменным, но благодаря отражению вы можете изменить класс String. Вы только что переопределили класс String как изменяемый в режиме реального времени. При желании вы можете переопределить методы как общедоступные, частные или статические.
источник
[Отказ от ответственности, это намеренно самоуверенный стиль ответа, так как я чувствую, что ответ «не делай этого дома, дети» оправдан]
Грех это линия
field.setAccessible(true);
которая говорит о нарушении публичного API, разрешая доступ к приватному полю. Это гигантская дыра в безопасности, которую можно заблокировать, настроив менеджер безопасности.Феномен в вопросе - детали реализации, которые вы никогда не увидите, если не будете использовать эту опасную строку кода, чтобы нарушить модификаторы доступа посредством отражения. Ясно, что две (обычно) неизменяемые строки могут использовать один и тот же массив символов. Совместно ли подстрока использует один и тот же массив, зависит от того, может ли он и думал ли разработчик поделиться им. Обычно это невидимые детали реализации, о которых вам не нужно знать, если вы не выстрелите модификатор доступа через голову с этой строкой кода.
Это просто не очень хорошая идея полагаться на такие детали, которые не могут быть испытаны без нарушения модификаторов доступа с помощью отражения. Владелец этого класса поддерживает только обычный публичный API и может вносить изменения в реализацию в будущем.
Сказав все это, строка кода действительно очень полезна, когда у вас есть пистолет, который заставляет вас делать такие опасные вещи. Использование этой задней двери, как правило, является запахом кода, который необходимо обновить, чтобы улучшить код библиотеки, где вам не нужно грешить. Другое распространенное использование этой опасной строки кода - это написание "фреймворка вуду" (orm, инъекционный контейнер, ...). Многие люди проявляют религиозность в отношении таких фреймворков (как за, так и против них), поэтому я не буду призывать к пламенной войне, говоря, что ничему, кроме подавляющего большинства программистов, не нужно идти туда.
источник
Строки создаются в постоянной области памяти кучи JVM. Так что да, он действительно неизменен и не может быть изменен после создания. Потому что в JVM есть три типа кучи памяти: 1. Молодое поколение 2. Старое поколение 3. Постоянное поколение.
Когда любой объект создается, он попадает в область кучи молодого поколения и область PermGen, зарезервированную для пула строк.
Вот более подробная информация, из которой вы можете получить дополнительную информацию: Как работает сборщик мусора в Java .
источник
String является неизменным по своей природе, потому что нет способа изменить объект String. По этой причине они представили классы StringBuilder и StringBuffer.
источник