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 abc astore_1 //one ldc #2; //String abc astore_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: 23583040 two.value: 23583040 one: 8918249 two: 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 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
передается в качестве аргумента. Это можно декомпилировать в:
1
|
String two = new String(one); |
Выход
Вывод немного удивителен. Вторая пара, представляющая ссылки на объект String
понятна — мы создали два объекта String
— один был создан для нас в пуле констант, а второй — вручную для two
. Но почему первая пара предполагает, что оба объекта String
указывают на один и тот же массив char[] value
?!
1
2
3
4
|
one.value: 41771 two.value: 41771 one: 8388097 two: 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; // abc const #3 = String #45; // ?abc const #44 = Asciz abc; const #45 = Asciz ?abc; |
Байт код
1
2
3
4
5
6
7
|
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[]
в памяти:
1
2
3
4
|
one.value: 27379847 two.value: 7615385 one: 8388097 two: 16585653 |
Ну, тексты на самом деле не отличаются , метод equals()
все равно даст true
. У нас есть две ненужные копии одного и того же текста.
Теперь нам нужно выполнить два упражнения. Сначала попробуйте запустить:
1
|
two = two.intern(); |
перед печатью хеш-кодов. Не только one
и two
указывают на one
и тот же текст, но они являются одной и той же ссылкой!
1
2
3
4
|
one.value: 11108810 two.value: 11108810 one: 15184449 two: 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: 11108810 two.value: 8918249 one: 23583040 two: 23583040 |
Я оставлю вам ответ. Он научит вас, как работает substring()
, каковы преимущества такого подхода и когда это может привести к большим неприятностям .
Уроки выучены
- Сам
String
объект довольно дешев. Это текст, на который он указывает, который занимает большую часть памяти -
String
— это просто обертка вокругchar[]
для сохранения неизменности -
new String("abc")
самом деле не так дорого, так как внутреннее текстовое представление используется повторно. Но все же избегайте такой конструкции. - Когда
String
объединяется из постоянных значений, известных во время компиляции, конкатенация выполняется компилятором, а не JVM -
substring()
сложно, но самое главное, это очень дешево, как с точки зрения используемой памяти, так и времени выполнения (постоянная в обоих случаях)
Справка: внутренняя память в виде строк от нашего партнера по JCG Томаша Нуркевича в блоге Java и соседях .