Класс String в Java
Подробный обзор класса String в Java — устройство, внутренняя структура и основные методы.
String — наиболее используемый ссылочный тип в Java с большим отрывом. Вы встретили его в первый же день — String name = "Ada"; — и он незаметно вошёл в ваш код. Эта часть книги копает глубже. Класс устроен сложнее, чем кажется снаружи: фиксированная внутренняя структура, синтаксические привилегии, которых нет у других типов, пул строк в памяти, влияющий на идентичность, и намеренное архитектурное решение (неизменяемость), которое отражается на работе потоков, хэшировании и безопасности.
Эта глава — карта. Остальная часть раздела 9 заполняет каждую её область.
Что такое String?
String — обычный объект Java: java.lang.String, в том же пакете, что Object и Integer. Он объявлен как final, поэтому его нельзя наследовать, а каждый метод, который «изменяет» строку, на самом деле возвращает новую. Оригинал никогда не затрагивается.
String greeting = "hello";
greeting.toUpperCase(); // returns "HELLO" — discarded
System.out.println(greeting); // still prints "hello"
greeting = greeting.toUpperCase();
System.out.println(greeting); // now prints "HELLO"Привычка «вернуть новую строку» — самый важный факт об этом классе. Подробно она рассматривается в разделе Неизменяемость строк в Java.
Особое отношение на уровне языка
Два элемента синтаксиса зарезервированы специально для String и не могут быть распространены на другие типы:
- Строковые литералы —
"hello"создаёт объектStringнапрямую, безnew. Компилятор также дедуплицирует одинаковые литералы в пуле строк. - Оператор
+— перегружен для строк:"a" + "b"даёт"ab". Даже смешанные выражения вроде"score: " + 42работают, потому что Java приводит правую часть кString.
За кулисами современные компиляторы Java транслируют цепочки + с помощью StringBuilder или основанного на invokedynamic механизма StringConcatFactory, поэтому вам редко нужно писать конкатенацию вручную. Компилятор знает, что делать.
Внутренняя структура
До Java 9 каждый String хранил массив char[] — два байта на символ вне зависимости от содержимого. Java 9 ввела компактные строки: базовый массив теперь byte[] плюс однобайтовое поле coder, которое указывает, используется ли кодировка Latin-1 (один байт на символ) или UTF-16 (два байта). Для текста, умещающегося в Latin-1 — большинство кода, конфигурационных файлов, идентификаторов, обычного английского текста — это примерно вдвое уменьшает потребление памяти, не меняя API.
Вы не можете видеть это поле, не можете его изменить и не обязаны о нём думать. Но именно поэтому программы с большим количеством строк в JDK 9+ заметно меньше используют heap по сравнению с JDK 8.
Основные группы методов
API класса String велик, но организован в несколько легко узнаваемых групп:
Инспекция. length(), isEmpty(), isBlank(), charAt(i), codePointAt(i), hashCode().
Поиск. indexOf, lastIndexOf, contains, startsWith, endsWith, matches.
Извлечение. substring(start), substring(start, end), chars(), codePoints(), toCharArray().
Преобразование. toUpperCase(), toLowerCase(), trim(), strip(), replace, replaceAll, replaceFirst, concat.
Разбиение и объединение. split, String.join — рассматриваются в разделе split() и join().
Форматирование. String.format, метод экземпляра formatted и вывод в стиле printf — рассматриваются в разделе Форматирование строк.
Сравнение. equals, equalsIgnoreCase, compareTo, compareToIgnoreCase, contentEquals — рассматриваются в разделе Сравнение строк.
Конвертация. valueOf (статический), toString (метод экземпляра), вспомогательные методы разбора в Integer, Double и т. д. — рассматриваются в разделе Конвертация строк.
Полный список API находится в Javadoc JDK. Главное — понять, к какой группе обратиться, а не запоминать каждую перегрузку.
Строки — это последовательности кодовых единиц UTF-16
charAt(i) и length() считают кодовые единицы UTF-16, а не символы Unicode. Для текста внутри Базовой многоязычной плоскости (основная масса распространённых письменностей) один char = один символ, и различие не имеет значения. Для дополнительных символов — большинство эмодзи, некоторые расширения CJK, древние письменности — один видимый пользователю символ занимает два char, образуя суррогатную пару.
String emoji = "🙂";
System.out.println(emoji.length()); // 2 — two code units
System.out.println(emoji.codePointCount(0, emoji.length())); // 1 — one code pointЕсли нужно обходить строку по кодовым точкам Unicode, используйте codePoints() или codePointAt. Для большинства ASCII-ориентированных задач — разбор CSV, форматирование строк лога, сравнение идентификаторов — length() и charAt — это именно то, что нужно.
Изменяемые родственники: StringBuilder и StringBuffer
Когда нужно собирать строку по частям, повторное применение += выделяет новый String на каждом шаге. Стандартная библиотека поставляет два изменяемых компаньона для этого случая:
StringBuilder— быстрый, однопоточный.StringBuffer— тот же API, но с синхронизированными методами; полезен только когда несколько потоков пишут в один буфер.
У них параллельные API и параллельные главы в этой части книги.
Рабочий пример
Небольшое упражнение, затрагивающее наиболее распространённые группы — инспекцию, поиск, извлечение, преобразование и конвертацию — на одних и тех же входных данных. Читайте вывод построчно; каждый вызов иллюстрирует один инструмент из приведённого списка.
Последняя строка — главная мысль. После всех преобразований, которые мы применили, переменная line по-прежнему байт-в-байт совпадает с литералом, с которого мы начали — доказательство того, что модель «вернуть новую строку» реальна, а не просто примечание в документации.
Что дальше
Строки имеют уникальную для стандартной библиотеки модель памяти: одинаковые литералы разделяют хранилище, и вы можете включить произвольную строку в этот общий пул одним вызовом метода. Продолжайте читать в разделе Пул строк в Java.