String хранит тексты, как работает интернирование и постоянный пул.
Главное, что нужно понять, это различие между Java-объектом String и его содержимым — char[] в поле private value . String — это обертка вокруг массива char[] , инкапсулирующая его и делающая невозможным изменение, так что String может оставаться неизменным. Также класс String запоминает, какие части этого массива фактически используются (см. Ниже). Все это означает, что у вас может быть два разных объекта String (достаточно легких), указывающих на один и тот же char[] .
Я покажу вам несколько примеров, вместе с hashCode() каждого String и hashCode() внутреннего поля char[] value (я назову его text, чтобы отличить его от string). Наконец, я покажу javap -c -verbose вместе с константным пулом для моего тестового класса. Пожалуйста, не путайте пул констант класса с пулом строковых литералов. Они не совсем одинаковые. Смотрите также Понимание вывода javap для константного пула .
Предпосылки
Для тестирования я создал такой служебный метод, который нарушает инкапсуляцию String :
|
1
2
3
4
5
|
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-код
|
1
2
|
String one = "abc";String two = "abc"; |
Кстати, если вы просто напишите "ab" + "c" , компилятор Java выполнит конкатенацию во время компиляции, и сгенерированный код будет точно таким же. Это работает, только если все строки известны во время компиляции.
Класс постоянный пул
Каждый класс имеет свой собственный пул констант — список постоянных значений, которые можно использовать повторно, если они встречаются в исходном коде несколько раз. Включает общие строки, числа, имена методов и т. Д.
Вот содержимое пула констант в нашем примере выше:
|
1
2
3
|
const #2 = String #38; // abc//...const #38 = Asciz abc; |
Важно отметить различие между константным объектом String ( #2 ) и текстом в кодировке Unicode "abc" ( #38 ), на который указывает строка.
Байт код
Здесь генерируется байт-код. Обратите внимание, что и one и two ссылки назначаются с одинаковой константой #2 указывающей на строку "abc" :
|
1
2
3
4
|
ldc #2; //String abcastore_1 //oneldc #2; //String abcastore_2 //two |
Выход
Для каждого примера я печатаю следующие значения:
|
1
2
3
4
|
System.out.println("one.value: " + showInternalCharArrayHashCode(one));System.out.println("two.value: " + showInternalCharArrayHashCode(two));System.out.println("one" + System.identityHashCode(one));System.out.println("two" + System.identityHashCode(two)); |
Не удивительно, что обе пары равны:
|
1
2
3
4
|
one.value: 23583040two.value: 23583040one: 8918249two: 8918249 |
Это означает, что не только оба объекта указывают на один и тот же char[] (один и тот же текст внизу), поэтому тест equals() пройдет. Но даже больше, one и two — это one и те же ссылки! Так что one == two тоже верно. Очевидно, что если one и two указывают на one и тот же объект, то one.value и two.value должны быть равны.
Буквально и new String()
Java-код
Теперь пример, который мы все ждали — один строковый литерал и одна новая String использующие один и тот же литерал. Как это будет работать?
|
1
2
|
String one = "abc";String two = new String("abc"); |
Тот факт, что константа "abc" используется в исходном коде два раза, должен дать вам некоторую подсказку…
Класс постоянный пул То же, что и выше.
Байт код
|
1
2
3
4
5
6
7
8
|
ldc #2; //String abcastore_1 //onenew #3; //class java/lang/Stringdupldc #2; //String abcinvokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)Vastore_2 //two |
Смотри внимательно! Первый объект создан так же, как и выше, что неудивительно. Он просто принимает постоянную ссылку на уже созданную String ( #2 ) из пула констант. Однако второй объект создается с помощью обычного вызова конструктора. Но! Первая String передается в качестве аргумента. Это можно декомпилировать в:
|
1
|
String two = new String(one); |
Выход
Вывод немного удивителен. Вторая пара, представляющая ссылки на объект String понятна — мы создали два объекта String — один был создан для нас в пуле констант, а второй — вручную для two . Но почему первая пара предполагает, что оба объекта String указывают на один и тот же массив char[] value ?!
|
1
2
3
4
|
one.value: 41771two.value: 41771one: 8388097two: 16585653 |
Это становится понятным, когда вы посмотрите на то, как работает конструктор String(String) (здесь это сильно упрощено):
|
1
2
3
4
5
|
public String(String original) { this.offset = original.offset; this.count = original.count; this.value = original.value;} |
Увидеть? Когда вы создаете новый объект String на основе существующего, он повторно использует значение char[] value . String s являются неизменяемыми, нет необходимости копировать структуру данных, о которой известно, что она никогда не изменялась. Более того, поскольку new String(someString) создает точную копию существующей строки, и строки являются неизменяемыми, очевидно, что нет причин одновременно существовать двум.
Я думаю, что это подсказка некоторых недоразумений: даже если у вас есть два объекта String , они все равно могут указывать на одно и то же содержимое. И, как вы можете видеть, сам объект String довольно мал.
Модификация времени выполнения и intern()
Java-код
Допустим, вы изначально использовали две разные строки, но после некоторых модификаций они все одинаковые:
|
1
2
|
String one = "abc";String two = "?abc".substring(1); //also two = "abc" |
Компилятор Java (по крайней мере, мой) не достаточно умен, чтобы выполнить такую операцию во время компиляции, посмотрите:
Класс постоянный пул
Внезапно мы получили две постоянные строки, указывающие на два разных постоянных текста:
|
1
2
3
4
|
const #2 = String #44; // abcconst #3 = String #45; // ?abcconst #44 = Asciz abc;const #45 = Asciz ?abc; |
Байт код
|
1
2
3
4
5
6
7
|
ldc #2; //String abcastore_1 //oneldc #3; //String ?abciconst_1invokevirtual #4; //Method String.substring:(I)Ljava/lang/String;astore_2 //two |
Первая строка построена как обычно. Второе создается сначала загрузкой константы "?abc" а затем вызывая ее на substring(1) .
Выход
Здесь нет ничего удивительного — у нас есть две разные строки, указывающие на два разных текста char[] в памяти:
|
1
2
3
4
|
one.value: 27379847two.value: 7615385one: 8388097two: 16585653 |
Ну, тексты на самом деле не отличаются , метод equals() все равно даст true . У нас есть две ненужные копии одного и того же текста.
Теперь нам нужно выполнить два упражнения. Сначала попробуйте запустить:
|
1
|
two = two.intern(); |
перед печатью хеш-кодов. Не только one и two указывают на one и тот же текст, но они являются одной и той же ссылкой!
|
1
2
3
4
|
one.value: 11108810two.value: 11108810one: 15184449two: 15184449 |
Это означает, что оба one.equals(two) и one == two теста пройдут. Также мы сохранили немного памяти, потому что текст "abc" появляется в памяти только один раз (вторая копия будет собираться мусором).
Второе упражнение немного отличается, проверьте это:
|
1
2
|
String one = "abc";String two = "abc".substring(1); |
Очевидно, что one и two — это два разных объекта, указывающих на два разных текста. Но почему результат показывает, что они оба указывают на один и тот же массив char[] ?!?
|
1
2
3
4
|
one.value: 11108810two.value: 8918249one: 23583040two: 23583040 |
Я оставлю вам ответ. Он научит вас, как работает substring() , каковы преимущества такого подхода и когда это может привести к большим неприятностям .
Уроки выучены
- Сам
Stringобъект довольно дешев. Это текст, на который он указывает, который занимает большую часть памяти -
String— это просто обертка вокругchar[]для сохранения неизменности -
new String("abc")самом деле не так дорого, так как внутреннее текстовое представление используется повторно. Но все же избегайте такой конструкции. - Когда
Stringобъединяется из постоянных значений, известных во время компиляции, конкатенация выполняется компилятором, а не JVM -
substring()сложно, но самое главное, это очень дешево, как с точки зрения используемой памяти, так и времени выполнения (постоянная в обоих случаях)
Справка: внутренняя память в виде строк от нашего партнера по JCG Томаша Нуркевича в блоге Java и соседях .