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().

Определение имен переменных

Java позволяет вставлять символы валют в имена переменных. Не только доллар ‘$’, но и евро, рубль, йену и другие.

Java не дает возможность определять несколько переменных на одной строке с разными типами. Точнее, переопределять! Например, в примере ниже компилятор посчитает это действие переопределением и выдаст ошибку компиляции:

1
double x=1D, double y=2D;    //  ошибка компиляции. смена типа определения переменных

Инициализация переменных

Для локальных переменных (переменных в блоках), Java требует инициализации в случае их использования:

1
2
3
4
5
{
int x = 1;
int y;
x = x + y; // ошибка компиляции. переменная y не инициализирована
}

Но при этом:

1
2
3
4
5
{
int x = 1;
int y = 2, z;
x = x + y;
}

Переменная z, так же как и в примере ранее x, не инициализирована, но компилятор не обратит на это внимание, поскольку z нигде не используется!

Компилятор будет проверять условия инициализации для всех вероятных веток. Т.е. если будут присутствовать условия, которые не инициализируют переменные, но будет найдено их использование - будет выдана ошибка компиляции. Иначе говоря: 

1
2
3
4
5
6
7
8
int a = 1;
int b = 2, c, d;
if(a == 1) {
    d = a \* c; // ошибка компиляции. переменная c не инициализирована
else if(a == 2) {
    d = a \* b;
}
int e = d; // ошибка компиляции. переменная d не инициализирована (добление инициализации в else блоке исправит ошибку)

Определение переменной с помощью var

Начиная с Java 11, появилась возможность опускать определение типа переменной при ее инициализации. В действительности, это не совсем так - Java все равно присваивает ей тип, но делает это скрытно от разработчика. 

Так же, использование var допустимо только для локальных переменных (переменных в блоках метода), но не для instance переменных или параметров методов.

Выбор типа для var происходит в момент объявления переменной. По этой причине присвоение значения должно быть выполнено в момент объявления:

1
2
var username;  // ошибка компиляции. при инициализации переменной с применением var она должна быть инициализирована сразу
username = x == 1 ? "admin" : "anonymous";

Если компилятор не сможет определить тип, который должен быть присвоен переменной, он выдаст ошибку. Такое может произойти, например, если значение при инициализации будет null:

1
var x = null;

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

1
var x = (String) null;

Переменные var нельзя объявлять в качестве параметров в метод, поскольку они не являются локальными переменными.

И важное примечание - var не является ключевым словом, т.е. его можно использовать и для имени переменной, и для имени метода, и для пакета. Но класс с именем var создавать запрещено.

Чеклист

  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