Как работает этот фрагмент кода Java? (Строковый пул и отражение)
Java-пул строк в сочетании с отражением может привести к некоторому невообразимому результату в Java:
import java.lang.reflect.Field;
class MessingWithString {
public static void main (String[] args) {
String str = "Mario";
toLuigi(str);
System.out.println(str + " " + "Mario");
}
public static void toLuigi(String original) {
try {
Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
stringValue.set(original, "Luigi".toCharArray());
} catch (Exception ex) {
// Ignore exceptions
}
}
}
Над кодом будет напечатан:
"Luigi Luigi"
Что случилось с Марио?
Ответы
Ответ 1
Что случилось с Марио?
Вы изменили его, в основном. Да, с отражением вы можете нарушить неизменность строк... и из-за интернирования строк, это означает, что любое использование "Марио" (кроме выражения большей константы строки, которое было бы разрешено во время компиляции) закончится как "Луиджи" в остальной части программы.
Это связано с тем, что для размышлений требуются разрешения безопасности...
Обратите внимание, что выражение str + " " + "Mario"
не выполняет конкатенацию времени компиляции из-за левой ассоциативности +
. Это эффективно (str + " ") + "Mario"
, поэтому вы все равно видите Luigi Luigi
. Если вы измените код на:
System.out.println(str + (" " + "Mario"));
... тогда вы увидите Luigi Mario
, поскольку компилятор будет интернирован " Mario"
для другой строки до "Mario"
.
Ответ 2
Он был настроен на Луиджи. Строки в Java неизменяемы; таким образом, компилятор может интерпретировать все упоминания "Mario"
как ссылки на тот же элемент пула констант String (примерно, "ячейка памяти" ). Вы использовали отражение, чтобы изменить этот элемент; поэтому все "Mario"
в вашем коде теперь как будто вы написали "Luigi"
.
Ответ 3
Чтобы объяснить существующие ответы немного больше, давайте взглянем на ваш сгенерированный байт-код (только здесь main()
).
![Байт-код]()
Теперь любые изменения в содержимом этого местоположения будут влиять как на ссылки (и любые другие, которые вы даете тоже).
Ответ 4
Строковые литералы хранятся в пуле строк и используются их канонические значения. Оба литерала "Mario"
- это не просто строки с одинаковым значением, они тот же объект. Манипуляция одним из них (с использованием отражения) изменит их "оба", поскольку они всего лишь две ссылки на один и тот же объект.
Ответ 5
Вы просто изменили String
пула констант String Mario
на Luigi
, на который ссылались несколько String
s, поэтому каждый литерал ссылки Mario
теперь Luigi
.
Field stringValue = String.class.getDeclaredField("value");
Вы выбрали поле char[]
named value
из класса String
stringValue.setAccessible(true);
Сделать доступным.
stringValue.set(original, "Luigi".toCharArray());
Вы изменили поле original
String
на Luigi
. Но оригинал Mario
литерал String
и литерал принадлежит пулу String
, и все они интернированы. Это означает, что все литералы, которые имеют одинаковый контент, относятся к одному и тому же адресу памяти.
String a = "Mario";//Created in String pool
String b = "Mario";//Refers to the same Mario of String pool
a == b//TRUE
//You changed 'a' to Luigi and 'b' don't know that
//'a' has been internally changed and
//'b' still refers to the same address.
В основном вы изменили пул Mario из String
, который получил отражение во всех ссылках. Если вы создадите String
Object
(т.е. new String("Mario")
) вместо литерала, вы не столкнетесь с этим поведением, потому что у вас будет два разных Mario
.
Ответ 6
Другие ответы адекватно объясняют, что происходит. Я просто хотел добавить, что это работает, только если не установлен менеджер безопасности. При запуске кода из командной строки по умолчанию нет, и вы можете делать такие вещи. Однако в среде, где доверенный код смешивается с ненадежным кодом, например сервером приложений в рабочей среде или изолированной песочницей в браузере, обычно присутствует диспетчер безопасности, и вам не разрешат использовать эти виды махинаций, поэтому это, пожалуй, менее страшная дыра в безопасности.
Ответ 7
Другой связанный момент: вы можете использовать постоянный пул для улучшения производительности сопоставлений строк в некоторых случаях, используя String.intern()
.
Этот метод возвращает экземпляр String с тем же содержимым, что и String, из которого он вызывается из пула констант String, добавляя его, если он еще не присутствует. Другими словами, после использования intern()
все строки с одинаковым содержимым гарантируются как один и тот же экземпляр String как и другие строковые константы с этим содержимым, то есть вы можете использовать оператор equals (==
) на их.
Это просто пример, который не очень полезен сам по себе, но он иллюстрирует точку:
class Key {
Key(String keyComponent) {
this.keyComponent = keyComponent.intern();
}
public boolean equals(Object o) {
// String comparison using the equals operator allowed due to the
// intern() in the constructor, which guarantees that all values
// of keyComponent with the same content will refer to the same
// instance of String:
return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent);
}
public int hashCode() {
return keyComponent.hashCode();
}
boolean isSpecialCase() {
// String comparison using equals operator valid due to use of
// intern() in constructor, which guarantees that any keyComponent
// with the same contents as the SPECIAL_CASE constant will
// refer to the same instance of String:
return keyComponent == SPECIAL_CASE;
}
private final String keyComponent;
private static final String SPECIAL_CASE = "SpecialCase";
}
Этот небольшой трюк не стоит разрабатывать ваш код, но стоит помнить о том дне, когда вы заметите, что немного быстрее может быть извлечено из некоторого бита чувствительного к производительности кода с помощью оператора ==
на строке с разумным использованием intern()
.