Урок по треку Java 6 мин

String, пул строк и StringBuilder

Неизменяемость, пул строк и intern, сравнение строк, StringBuilder и конкатенация.

Строки спрашивают на каждом собесе по Java: почему String неизменяем, что такое пул, когда == даёт true и почему конкатенация в цикле плоха.

Неизменяемость

String объявлен final, его содержимое после создания не меняется. Все «изменяющие» методы (toUpperCase, replace, substring, concat) возвращают новую строку.

String s = "java";
s.toUpperCase();          // результат выброшен
System.out.println(s);    // java
s = s.toUpperCase();      // теперь JAVA

Зачем так сделано:

  • строку безопасно делить между потоками и кешировать;
  • можно кешировать hashCode (строки идеальные ключи HashMap);
  • работает пул строк;
  • безопасность: путь к файлу или URL нельзя подменить после проверки.

С Java 9 строки хранят байты (byte[]) и флаг кодировки: латиница занимает 1 байт на символ (Compact Strings), остальное UTF-16.

Пул строк

Литералы ("java") и константные выражения кладутся в пул строк (он живёт в куче). Одинаковые литералы указывают на один объект. new String("java") всегда создаёт новый объект в куче. intern() возвращает каноничный экземпляр из пула.

String a = "java";
String b = "java";
String c = new String("java");
String d = "ja" + "va";        // константа, склеена компилятором
String part = "ja";
String e = part + "va";        // склейка в рантайме: новый объект

a == b          // true
a == c          // false
a == d          // true
a == e          // false
a == e.intern() // true
a.equals(c)     // true

Если part объявить final String part = "ja", выражение станет константным и a == e даст true. Строки всегда сравнивают через equals (или equalsIgnoreCase), а == только как вопрос на собесе.

Конкатенация и StringBuilder

+ для строк с Java 9 компилируется в invokedynamic (StringConcatFactory), это быстро для одного выражения. Но в цикле каждая итерация создаёт новую строку и копирует всё накопленное: O(n²).

String res = "";
for (int i = 0; i < n; i++) res += i;   // плохо

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append(i);
String res2 = sb.toString();            // хорошо

StringBuilder не потокобезопасен и быстр, StringBuffer синхронизирован и сегодня почти не нужен. Для склейки коллекции есть String.join(", ", list) и Collectors.joining.

Полезные методы

  • isBlank(), strip(), lines(), repeat(n) (Java 11);
  • chars() отдаёт IntStream кодов символов;
  • String.format и "%d".formatted(x) (Java 15);
  • текстовые блоки """...""" для многострочного текста (Java 15).

length() считает UTF-16 единицы, а не символы: эмодзи занимает две. Для честного числа символов есть codePointCount.

Частые ошибки

  • Сравнение через ==.
  • Вызов метода без присваивания результата: s.trim();.
  • Конкатенация в цикле.
  • split("."): аргумент это регулярное выражение, точку надо экранировать split("\\.").

Что спросят на собесе

  • Почему String неизменяем и какие у этого плюсы?
  • Что такое пул строк, где он находится и что делает intern()?
  • Сколько объектов создаёт new String("abc")?
  • Чем StringBuilder отличается от StringBuffer и от +?
  • Что выведет сравнение строк через == в разных вариантах?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.