Внутреннее устройство String, метод substring - 1

— Привет, Амиго!

— Привет, Элли!

— В этом уроке я тебе расскажу о двух вещах. А конкретно — о подстроках и об устройстве объектов класса String. Причём в этом вопросе нам придётся немного погрузиться в историю Java… Но это будет коротко, интересно и полезно, обещаю.

Как получить подстроку (или часть строки)?

Из поставленного вопроса ты уже, вероятно, понял, что подстрока – это просто часть строки. Знаешь, какие действия над строками самые популярные? Первое — склеивание нескольких строк (конкатенация), ты уже с ней сталкивался неоднократно. И второе — получение подстроки из строки.

Для этого в Java есть метод substring. Он возвращает часть строки. Существует два варианта этого метода.

Первый возвращает подстроку, заданную начальным и конечным номерами символов. Обрати внимание, первый символ при этом входит в подстроку, а последний — нет! То есть, если мы передаем номера (1,3) — с первого по третий, то в подстроке окажутся только первый и второй символы.

Во втором варианте методы мы указываем только начальный индекс подстроки, и он возвращает подстроку от этого индекса до конца строки.

Метод(ы) Пример(ы)
String substring(int beginIndex, int endIndex)
String s = "Good news everyone!";
s = s.substring(1,6);

Результат:

s == "ood n";
String substring(int beginIndex)
String s = "Good news everyone!";
s = s.substring(1);

Результат:

s == "ood news everyone!";

— Все достаточно просто. Спасибо, Элли.

Внутреннее устройство объектов String: экскурс в JDK 6

— Помнишь, я тебе обещала небольшой экскурс в историю Java? Разумеется, в контексте нашей темы, а именно — особенностей класса String.

Когда-то давно, в те времена тебя, возможно, ещё и не собрали, самой актуальной версией языка была JDK 6. С тех пор много воды утекло и число, обозначающее номер самой новой Java уже давно стало двузначным.

Как ты уже знаешь из первой лекции 2-го уровня этого квеста, String — неизменяемый класс (immutable). И вот эта самая неизменяемость дала возможность получать подстроку интересным способом в JDK 6.

Внутри объект типа String является массивом символов, а точнее — содержит массив символов. Это интуитивно понятно. А во времена JDK 6 там хранились еще две переменные: номер первого символа в символьном массиве и их количество. Таким образом, в JDK 6 у String было три поля char value[] (символьный массив), int offset (индекс первого символа в массиве) и int count (количество символов в массиве).

Это устройство и было использовано для создания подстроки с помощью метода substring(). Когда он вызван, он создает новую строку, то есть новый объект String.

Только вот вместо того, чтобы хранить ссылку на массив с новым набором символов, в JDK 6 этот объект хранил ссылку на старый массив символов и ещё две переменные offset и count. С их помощью он определяет, какая часть оригинального массива символов относится к новому подмассиву.

— Ничего не понял.

— Когда в JDK 6 создается подстрока, массив символов не копируется в новый объект String. Вместо этого оба объекта хранят ссылку на один и тот же массив символов. Но! Второй объект хранит еще две переменные, в первой из них записан индекс начала подмассива, во второй — сколько в подмассиве символов.

Вот смотри:

Получение подстроки Что хранится внутри подстроки
String s = "mama";
Что хранится в s:

char[] value = {'m','a','m','a'};
offset = 0;
count = 4;
String s2 = s.substring(1);
Что хранится в s2:

char[] value = {'m','a','m','a'};
offset = 1;
count = 3;
String s3 = s.substring(1, 3);
Что хранится в s3:

char[] value = {'m','a','m','a'};
offset = 1;
count = 2;

Все три строки хранят ссылку на один и тот же массив char, просто кроме этого они еще хранят номер первого и последнего символа этого массива, который относится непосредственно к их объекту. Вернее, номер первого символа и их количество.

— Теперь ясно.

— Поэтому, если ты в JDK 6 возьмешь строку длиной 10,000 символов и наделаешь из нее 10,000 подстрок любой длины, то эти «подстроки» будут занимать очень мало памяти, т.к. массив символов не дублируется. Строки, которые должны занимать кучу места, будут занимать буквально пару байт.

— А если бы строки можно было менять, это бы работало?

— Нет, кто-то мог поменять ту первую строку, и тогда поменялись бы все её подстроки.

— Крутой способ! Он экономит память, верно?

— Верно. Только вот такая экономия памяти в JDK 6 вылилась в одну проблему. Я попытаюсь тебе её описать. Допустим, у нас есть некая строка x, и мы создаём подстроки применяя substring.

String x = "myLongString";
String y = x.substring(2,6);
String z = x.substring(0,3);

Теперь у нас есть объект x (кстати, хранится он в специальной области памяти, называемой “куча” или heap) и два объекта y и z, ссылающихся на тот же объект x. Только x ссылается на элементы со второго по шестой, а z — с нулевого по третий. И вот, может сложится ситуация, когда об изначальном объекте x уже все забыли, никто на него не ссылается, и все работают только с объектами y и z. Знаешь, что будет в таком случае?

— Помнится, Риша что-то мне рассказывал о сборке мусора, но я мало что помню.

— Мало что помнишь, а мыслишь — правильно. В такой ситуации может прийти сборщик мусора и уничтожить объект x, при том, что массив в памяти останется, и его используют y и z.

— И что тогда?

— В таком случае может случится неприятная ситуация, называемая утечкой памяти.

— И что с этим делать?

— Уже все всё сделали. Начиная с JDK 7 метод substring работает по-другому. И сейчас я тебе расскажу, как. На этом наш экскурс в историю завершён...Кстати, нужно не забыть подготовить для тебя лекцию по работе сборщика мусора.

substring() в JDK 7 и более поздних версиях

В JDK 7 substring() уже не отсчитывает количество символов в создаваемом им символьном массиве, а просто создает новый массив в памяти (куче) и ссылается именно на него. Возьмём тот же пример:

String x = “myLongString”;
String y = x.substring(2,6);
String z = x.substring(0,3);

Итак, в JDK 7 и более поздних версиях объекты y и z, созданные в результате работы метода substring(), применённого к объекту x, будут ссылаться на два вновь созданных массива (в куче) — {L,o,n,g} для y и {m,y} для z.

В обновлёной версии метода эти две новых строки (то есть два новых символьных массива) будут храниться в памяти наряду с исходной строкой myLongString ({m,y,L,o,n,g,S,t,r,i,n,g}, если в виде массива). Вот и вся разница.

— Кажется, что стало только хуже…

— Скорее, более затратно с точки зрения памяти. Зато такой подход позволяет не допустить её, этой самой памяти, утечек. К тому же, сам метод работает быстрее, так как ему не приходится вычислять количество символов.

— А вдруг в последующих версиях снова что-то изменят? Что делать?

— Это вполне возможно. Поэтому я и провела этот небольшой экскурс в историю. Со временем ты привыкнешь заглядывать в официальную документацию Oracle, там ты всегда отыщешь актуальную информацию. А ещё — этот пример показателен тем, что демонстрирует, почему в реальных проектах не всегда легко перейти с одной версии языка на другую. Можно споткнуться о такие “подводные камни”.