Как работает этот фрагмент кода 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().