Ответ 1
Я думаю, что основной смысл здесь понять различие между String
объектом Java и его содержимым - char[]
в частном value
поле. String
является в основном оболочкой вокруг массива char[]
, инкапсулируя его и делая невозможным изменение, поэтому String
может оставаться неизменным. Также класс String
запоминает, какие части этого массива фактически используются (см. Ниже). Все это означает, что вы можете иметь два разных объекта String
(довольно легкий), указывающий на тот же char[]
.
Я покажу вам несколько примеров вместе с hashCode()
каждого String
и hashCode()
внутреннего поля char[] value
(я буду называть его текстом, чтобы отличить его от строки). Наконец, я покажу вывод javap -c -verbose
вместе с постоянным пулом для моего тестового класса. Не путайте пул констант класса с пулом строковых литералов. Они не совсем то же самое. См. Также Понимание выхода javap для пула констант.
Предварительные условия
В целях тестирования я создал такой метод утилиты, который разбивает String
инкапсуляцию:
private int showInternalCharArrayHashCode(String s) {
final Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
return value.get(s).hashCode();
}
Он напечатает hashCode()
из char[] value
, эффективно помогая нам понять, указывает ли этот конкретный String
на тот же текст char[]
.
Два строковых литерала в классе
Давайте начнем с простейшего примера.
Код Java
String one = "abc";
String two = "abc";
Кстати, если вы просто пишете "ab" + "c"
, компилятор Java будет выполнять конкатенацию во время компиляции, и сгенерированный код будет точно таким же. Это работает только в том случае, если все строки известны во время компиляции.
Пул констант класса
Каждый класс имеет свой собственный пул констант - список постоянных значений, которые могут быть повторно использованы, если они происходят несколько раз в исходном коде. Он включает общие строки, числа, имена методов и т.д.
Вот содержимое пула констант в нашем примере выше.
const #2 = String #38; // abc
//...
const #38 = Asciz abc;
Важно отметить различие между String
постоянным объектом (#2
) и кодированным в Юникоде текстом "abc"
(#38
), на который указывает строка.
Байт-код
Здесь генерируется код байта. Обратите внимание, что обе ссылки one
и two
назначаются с той же константой #2
, указывающей на строку "abc"
:
ldc #2; //String abc
astore_1 //one
ldc #2; //String abc
astore_2 //two
Выход
Для каждого примера я печатаю следующие значения:
System.out.println(showInternalCharArrayHashCode(one));
System.out.println(showInternalCharArrayHashCode(two));
System.out.println(System.identityHashCode(one));
System.out.println(System.identityHashCode(two));
Не удивительно, что обе пары равны:
23583040
23583040
8918249
8918249
Это означает, что не только оба объекта указывают на тот же char[]
(тот же текст внизу), поэтому тест equals()
пройдет. Но даже больше, one
и two
- это те же самые ссылки! Таким образом, one == two
также является истинным. Очевидно, что если one
и two
указывают на один и тот же объект, тогда one.value
и two.value
должны быть равны.
Литерал и new String()
Код Java
Теперь пример, который мы все ждали - один строковый литерал и один новый String
, используя один и тот же литерал. Как это будет работать?
String one = "abc";
String two = new String("abc");
Тот факт, что константа "abc"
используется два раза в исходном коде, должен дать вам некоторый намек...
Пул констант класса
То же, что и выше.
Байт-код
ldc #2; //String abc
astore_1 //one
new #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
astore_2 //two
Посмотрите внимательно! Первый объект создается так же, как и выше, не удивительно. Он просто принимает постоянную ссылку на уже созданный String
(#2
) из пула констант. Однако второй объект создается через обычный вызов конструктора. Но! Первый String
передается как аргумент. Это можно декомпилировать для:
String two = new String(one);
Выход
Результат немного удивителен. Вторая пара, представляющая ссылки на объект String
, понятна - мы создали два объекта String
- один из них был создан для нас в постоянном пуле, а второй был создан вручную для two
. Но почему на земле первая пара предполагает, что оба объекта String
указывают на тот же массив char[] value
?!
41771
41771
8388097
16585653
Становится понятно, когда вы смотрите на то, как String(String)
работает конструктор (здесь значительно упрощается):
public String(String original) {
this.offset = original.offset;
this.count = original.count;
this.value = original.value;
}
См? Когда вы создаете новый объект String
на основе существующего, он повторно использует char[] value
. String
являются неизменяемыми, нет необходимости копировать структуру данных, которая, как известно, никогда не изменяется.
Я думаю, что это ключ к вашей проблеме: даже если у вас есть два объекта String
, они могут по-прежнему указывать на одно и то же содержимое. И, как вы видите, сам объект String
довольно мал.
Изменение времени выполнения и intern()
Код Java
Предположим, вы сначала использовали две разные строки, но после некоторых изменений они все одинаковы:
String one = "abc";
String two = "?abc".substring(1); //also two = "abc"
Компилятор Java (по крайней мере, мой) недостаточно умен, чтобы выполнить такую операцию во время компиляции, посмотрите:
Пул констант класса
Внезапно мы закончили с двумя постоянными строками, указывающими на два разных константных текста:
const #2 = String #44; // abc
const #3 = String #45; // ?abc
const #44 = Asciz abc;
const #45 = Asciz ?abc;
Байт-код
ldc #2; //String abc
astore_1 //one
ldc #3; //String ?abc
iconst_1
invokevirtual #4; //Method String.substring:(I)Ljava/lang/String;
astore_2 //two
Первая строка построена, как обычно. Вторая создается путем первой загрузки константы "?abc"
, а затем вызова substring(1)
на ней.
Выход
Не удивительно, что у нас есть две разные строки, указывающие на два разных текста char[]
в памяти:
27379847
7615385
8388097
16585653
Ну, тексты не совсем разные, equals()
метод все равно даст true
. У нас есть две ненужные копии одного и того же текста.
Теперь мы должны выполнить два упражнения. Сначала попробуйте запустить:
two = two.intern();
перед печатью хэш-кодов. Не только оба one
и two
указывают на один и тот же текст, но они являются одной и той же ссылкой!
11108810
11108810
15184449
15184449
Это означает, что пройдут тесты one.equals(two)
и one == two
. Также мы сохранили некоторую память, потому что текст "abc"
появляется только один раз в памяти (вторая копия будет собрана мусором).
Второе упражнение несколько отличается, проверьте это:
String one = "abc";
String two = "abc".substring(1);
Очевидно, что one
и two
- это два разных объекта, указывающие на два разных текста. Но как результат выводит, что они оба указывают на тот же массив char[]
?!?
23583040
23583040
11108810
8918249
Я оставлю вам ответ. Он научит вас, как работает substring()
, каковы преимущества такого подхода и когда он может привести к большим неприятностям.