Внутрішній пристрій String, метод substring - 1

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

— Привіт, Еллі!

— У цьому уроці я розповім тобі про дві речі. А саме - про підрядки і про об'єкт класу String. Причому в цьому питанні нам доведеться трохи поринути в історію Java... Але це буде коротко, цікаво і корисно, обіцяю. Як отримати підрядок (або частину рядка)?< /big>

З поставленого питання ти вже, мабуть, зрозумів, що підрядок – це просто частина рядка. Знаєш, які дії над рядками найпопулярніші? Перше - склеювання кількох рядків (конкатенація), ти вже з нею стикався неодноразово. І друге — отримання підрядка з рядка.

Для цього Java має метод substring. Він повертає частину рядка. Існує два варіанти цього методу.

Перший повертає підрядок, заданий початковим і кінцевим номерами символів. Зверніть увагу, перший символ при цьому входить у підрядок, а останній — ні! Тобто, якщо ми передаємо номери (1,3) — з першого по третій, то в підрядку виявляться тільки перший і другий символи. від цього індексу до кінця рядка.

String substring(int beginIndex , int endIndex)
< td rowspan="2">
Метод(и) Приклад(и)
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< /strong> (кількість символів у масиві).

Цей пристрій і був використаний для створення підрядка за допомогою методу substring(). Коли він викликаний, він створює новий рядок, тобто новий об'єкт String.

Тільки ось замість того, щоб зберігати посилання на масив з новим набором символів, в JDK 6 цей об'єкт зберігав посилання на старий масив символів і ще дві змінні offset та count. З їх допомогою він визначає, яка частина оригінального масиву символів відноситься до нового підмасиву. — Нічого не зрозумів.

— Коли JDK 6 створюється підрядок, масив символів не копіюється в новий об'єкт String. Натомість обидва об'єкти зберігають посилання на той самий масив символів. Але! Другий об'єкт зберігає ще дві змінні, в першій з них записано індекс початку підмасиву, у другій — скільки в підмасиві символів. Ось дивись:

Отримання підрядка Що зберігається всередині підрядка
String s = "mama";
Що зберігається в s:

char[] value< /strong> = {'m','a','m','a'}; offset = 0; count = 4;
String s2 = s.substring(1);< /code>
Що зберігається в 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;

Усі три рядки зберігають посилання на один і той же масив < strong>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. Тільки y посилається на елементи з другого по шосте, а z — з нульового до третього. І ось, може скластися ситуація, коли про первісний об'єкт x вже всі забули, ніхто на нього не посилається, і всі працюють тільки з об'єктами y та z. Знаєш, що буде в такому разі?

— Пам'ятається, Ріша щось розповідав мені про складання сміття, але я мало що пам'ятаю.

— Мало що пам'ятаєш, а мислиш правильно. У такій ситуації може прийти збирач сміття та знищити об'єкт x, при тому, що масив у пам'яті залишиться, і його використовують y та z.< /p> — І що тоді?

— У такому разі може статися неприємна ситуація, яка називається витоком пам'яті.

— І що з цим робити? Уже все все зробили. Починаючи з JDK 7, метод substring працює по-іншому. І зараз я тобі розповім, як. На цьому наш екскурс в історію завершено ... До речі, потрібно не забути підготувати для тебе лекцію з роботи збирача сміття. >

У 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,L} для z.

У оновленій версії методу ці два нових рядки (тобто два нові символьні масиви) зберігатимуться у пам'яті поряд із вихідним рядком myLongString ({m,y,L,o,n,g,S,t,r,i,n,g}, якщо у вигляді масиву). Ось і вся різниця.

— Здається, що стало лише гірше…

— Швидше, витратніше з погляду пам'яті. Зате такий підхід дозволяє не допустити її, цієї пам'яті, витоків. До того ж, сам метод працює швидше, тому що йому не доводиться обчислювати кількість символів.

— А раптом у наступних версіях знову щось змінять? Що робити?

— Це цілком можливо. Тому я провела цей невеликий екскурс в історію. З часом ти звикнеш заглядати в офіційну документацію Oracle, там ти завжди знайдеш актуальну інформацію. А ще цей приклад показовий тим, що демонструє, чому в реальних проектах не завжди легко перейти з однієї версії мови на іншу. Можна спіткнутися про такі “підводні камені”.