Java Basics: Strings

Строки

Необходимо следить за последовательностью выполнения операций со строками, поскольку допустима такая неочевидная запись:

1
2
3
int x = 1;
String y = "2";
System.out.println(1 + 1 + x + y); // ="32"

При конкатенации будет проверяться тип данных при каждой операции в отдельности и при каждой операции будет выполнено приведение типов если это требуется. Только в случае если один из операндов строка, тогда выполняется преобразование обоих операндов в строки. Так, в примере выше: 1+1 - int; 2+x - int; 3+y - String.

При конкатенации необходимо следить за последовательностью, поскольку они могут дать разный результат:

1
2
3
System.out.println("a" + 1 + 2);     // a123
System.out.println(1 + 2 + "a"); // 3a
System.out.println(1 + "a" + 2); // 1a2

Общее правило такое - складываются всегда 2 операнда. Если оба они цифры, то выполняется арифметическое сложение; если один из них строка, то второй переводится в строку и операнды “склеиваются” как строки.

Методы

Данный класс содержит большое количество методов и их число регулярно увеличивается. Некорые из них перечислены ниже.

strip()

Аналогичен trim, но поддерживает работу с Unicode, лучше использовать его.

indent(int numberSpaces)

Добавляет требуемое количество пробелов на каждую строку. Если количество отрицательно, то будет выполнена попытка убрать это количество пробелов. Так же выполняется нормализация всей строковой переменной: в конце каждой строки добавляется строковый разделитель (если отсутствует); перенос строки принудительно преобразуется в \\n, заменяя перенос, специфичный для операционной системы.

stripIndent()

Метод выравнивает строки, убирая максимально допустимое количество пробелов, но количество убираемых пробелов будет одинаковым для всех строк. Как и indent, метод нормализует строку, за исключением добавления в конец строкового разделителя.

translateEscapes()

Метод преобразует экранированный символ (‘\\‘) в его неэкранированный аналог:

1
2
3
var escaped = "A\\\\tB";
System.out.println(escaped); // A\\tB
System.out.println(escaped.translateEscapes()); // A B

Форматирование строк

Форматирование строк доступно не только через String.format(), но и посредством метода formatted у самого объекта строки:

1
var str = "Hello %s, welcome %s".formatted("username", "back");

Массивы

При добавлении в массив инвалидного типа ошибка не всегда будет возникать на этапе компиляции. Например:

1
2
3
var strs = new String\[\]{"this is string"};
Object\[\] objs = strs;
objs\[0\] = Integer.valueOf(1); // ошибка времени исполнения. попытка вставить объект типа Integer в массив типа String

Компилятор ошибки не выдаст, но во время выполнения появится ArrayStoreException.

strip()

Аналогичен trim, но поддерживает Unicode. Удаляет пустые символы типа \\u2000. trim такого не выполняет, соответственно, strip предпочтительнее.

Следует помнить что любая операция с String всегда создает новый объект строки (за исключением одного метода, о котором речь пойдет ниже).

Пул строк. String pool

В него попадают только литеральные строки и константы. Так, “name” попадет в пул строк, а myObject.toString() - нет.

Любое вычисление строки в runtime не считается литеральным, а следовательно, в пул строк не попадает. Чтобы принудительно занести строку в пул, достаточно выполнить:

1
myString.intern();

Данный метод проверяет строку в пуле и добавит ее, если ее там нет. Данный метод очень полезен если строка вычисляется в runtime и является ключом или значением для коллекции (Collection). Поскольку если строки все время являются, де-факто, новыми, то и поиск будет выполняться значительно медленнее, чем если бы он происходил просто по ссылкам на строку в пуле.

Попадание в пул не всегда очевидно по коду. Решение о попадании в пул (является ли строка литералом) определяется на этапе компиляции:

1
2
3
4
String x = "a" + "b" + "c" + 1;
String y = "abc" + 1;
System.out.println(x == y); // true
System.out.println(x == y.intern()); // true

Т.е. компилятор понимает что после компиляции он имеет, де-факто, литеральные значения x="abc1" и y="abc1", которые можно занести в пул поочередно (на 2ой раз она уже будет там).

1
2
3
String z = "a" + "b" + "c" + new String("1");
System.out.println(x == z); // false
System.out.println(x == y.intern()); // true

В данном случае, z будет вычисляться в runtime, но при intern() будет выполнена проверка на наличие точно такой же строки в пуле строк (а она там уже есть), поэтому, фактически, уже будет сравниваться литеральная строка, полученная при компиляции со строкой, идентичной той, которая получена из пула принудительно.

Стоит отметить, что мнения относительно целесообразности использования метода intern расходятся и значимая часть разработчиков склоняется к мнению, что лучше собственная реализация кэша строк, чем использование стандартного String pool

Строковые блоки

Для простоты восприятия в Java 17 добавлены строковые блоки, которые позволяют добавлять многострочные символьные переменные. Ранее перенос строки обозначался спецсимволом \\n, теперь же добиться подобного эффекта стало значительно проще с помощью тройных кавычек:

1
2
3
4
5
String str = """
1
2
3
""";

На примере выше переменная str будет иметь значение:   1\\n  2\\n 3\\n, т.е. состоять из 4 строк. Почему же перед “1” и “2” появились пробелы? Дело в том что Java для удобства восприятия игнорирует первые символы пробела в отформатированном блоке. В качестве начала отформатированного блока рассматривается первая строка, в которой присутствует не пробел:

1
2
3
4
5
String str = """
1
2
3
""";

Светлосерым выделен отступ, который считается отступом для строкового блока (для удобства чтения). Темно-серым будут признаны пробелы, которые Java будет рассматривать как обязательные пробелы для заполнения в строке.

Особенности строковых блоков

Строковые блоки имеют ряд особенностей, отличающих их от классических строк:

Символ пробела \\s будет рассматриваться как 1 пробел в рамках строкового блока и с него будет считаться отступ (если он будет первым символом). В классическом определении строки, символ \\s будет рассматриваться как 2 пробела:

1
2
3
4
String str = """
\\s\\s
First chars
""";

Результатом выполнения будет: \\n First chars\\n.

Если символ пробела - ‘ ‘ - будет в конце строкового блока, то он просто проигнорируется:

1
2
String str = """
First chars """;

Врезультате str получит значение: “First chars”

Первая строка должна начинаться с новой строки, после определения блока:

1
String str = "First chars";  //  ошибка компиляции. строковый блок должен начинаться с новой строки

Символ ‘\’ в строковом блоке не (!) будет рассматриваться как начало новой строки. В классическом определении строки, компилятор выдаст ошибку.

1
2
3
4
5
6
7
8
String str = """
First\\
chars""";
String errorStringBlock1 = """
First\\ chars""";
String errorStringBlock2 = """
First\\
chars""";

При объявлении errorStringBlock# будут получены ошибки, поскольку после символа “\“ присутствует какой-то символ. В первом же случае после символа “\“ следует перенос строки, а в этом случае он не будет рассматриваться как символ экранирования (escape-symbol), а значит будет просто проигнорирован.

StringBuilder

Если ожидается большое количество операций над строкой, то рекомендуется использовать StringBuidler (или его потокобезопасный аналог  - StringBuffer).

Конструктор StringBuilder(int) задает начальный размер строки. Так, операция заполнения (конкатенации) будет выполняться быстрее (до указанной размерности, конечно).

insert(int, String)

Метод вставляет строку (символ, цифры, байт и т.д. - зависит от сигнатуры) в определенную позицию (не заменяет, а дополняет строку, сдвигая символ после позиции вставки вправо).

equals()

Для StringBuilder он работает не так как можно ожидать - так же как и ==, он просто проверяет ссылки, а не содержимое объектов.

1
2
3
StringBuilder sb1 = new StringBuilder("a");
StringBuilder sb2 = new StringBuilder("a");
boolean isEqual = sb1.equals(sb2); // =false

Чтобы проверить идентичность значений в разных StringBuilder следует преобразовать их в строки путем вызова toString(), а затем уже отдельные строки сравнивать через equals().

Чеклист

  1. Следить за конкатенацией и типом полученных данных в результате ее выполнения
  2. Учитывать String-pool при операциях со строками. Если по коду выполняется проверка на соответствие ссылок (==), то с высокой вероятностью, участвует String-pool
  3. При фигурировании строковых блоков и проверки значения, следует особенно обратить внимание на:
  • - пробелы в начале строк в блоке (относительно расположения первого символа). особое внимание на “\s” - они выступают как принудительные пробелы
  • - блок должен начинаться с новой строки
  • - символ “\“ без переноса строки после него или escape-символа выдаст ошибку
  1. Для проверки StringBuilder на идентичность строк equals не подходит, следует сначала преобразовать их в строки (toString), а затем сравнить через equals
  2. Локальная переменная потребует инициализации только если она может использоваться (возможно использование при каком-то условии)
  3. Следить за тем где объявляется переменная с помощью var - допустимо только локальная переменна
  4. Для массивов следить за правильностью типов - может ли объект быть добавлен. Если добавляемый объект не соответствует типу массива, будет ArrayStoreException
 Comments
Comment plugin failed to load
Loading comment plugin