Строки
Необходимо следить за последовательностью выполнения операций со строками, поскольку допустима такая неочевидная запись:
1 | int x = 1; |
При конкатенации будет проверяться тип данных при каждой операции в отдельности и при каждой операции будет выполнено приведение типов если это требуется. Только в случае если один из операндов строка, тогда выполняется преобразование обоих операндов в строки. Так, в примере выше: 1+1 - int; 2+x - int; 3+y - String
.
При конкатенации необходимо следить за последовательностью, поскольку они могут дать разный результат:
1 | System.out.println("a" + 1 + 2); // a123 |
Общее правило такое - складываются всегда 2 операнда. Если оба они цифры, то выполняется арифметическое сложение; если один из них строка, то второй переводится в строку и операнды “склеиваются” как строки.
strip()
Аналогичен trim
, но поддерживает работу с Unicode, лучше использовать его.
indent(int numberSpaces)
Добавляет требуемое количество пробелов на каждую строку. Если количество отрицательно, то будет выполнена попытка убрать это количество пробелов. Так же выполняется нормализация всей строковой переменной: в конце каждой строки добавляется строковый разделитель (если отсутствует); перенос строки принудительно преобразуется в \\n
, заменяя перенос, специфичный для операционной системы.
stripIndent()
Метод выравнивает строки, убирая максимально допустимое количество пробелов, но количество убираемых пробелов будет одинаковым для всех строк. Как и indent
, метод нормализует строку, за исключением добавления в конец строкового разделителя.
translateEscapes()
Метод преобразует экранированный символ (‘\\‘) в его неэкранированный аналог:
1 | var escaped = "A\\\\tB"; |
Форматирование строк
Форматирование строк доступно не только через String.format()
, но и посредством метода formatted
у самого объекта строки:
1 | var str = "Hello %s, welcome %s".formatted("username", "back"); |
Массивы
При добавлении в массив инвалидного типа ошибка не всегда будет возникать на этапе компиляции. Например:
1 | var strs = new String\[\]{"this is string"}; |
Компилятор ошибки не выдаст, но во время выполнения появится ArrayStoreException
.
strip()
Аналогичен trim
, но поддерживает Unicode. Удаляет пустые символы типа \\u2000
. trim
такого не выполняет, соответственно, strip предпочтительнее.
Следует помнить что любая операция с String
всегда создает новый объект строки (за исключением одного метода, о котором речь пойдет ниже).
Пул строк. String pool
В него попадают только литеральные строки и константы. Так, “name” попадет в пул строк, а myObject.toString()
- нет.
Любое вычисление строки в runtime не считается литеральным, а следовательно, в пул строк не попадает. Чтобы принудительно занести строку в пул, достаточно выполнить:
1 | myString.intern(); |
Данный метод проверяет строку в пуле и добавит ее, если ее там нет. Данный метод очень полезен если строка вычисляется в runtime и является ключом или значением для коллекции (Collection
). Поскольку если строки все время являются, де-факто, новыми, то и поиск будет выполняться значительно медленнее, чем если бы он происходил просто по ссылкам на строку в пуле.
Попадание в пул не всегда очевидно по коду. Решение о попадании в пул (является ли строка литералом) определяется на этапе компиляции:
1 | String x = "a" + "b" + "c" + 1; |
Т.е. компилятор понимает что после компиляции он имеет, де-факто, литеральные значения x="abc1"
и y="abc1"
, которые можно занести в пул поочередно (на 2ой раз она уже будет там).
1 | String z = "a" + "b" + "c" + new String("1"); |
В данном случае, z
будет вычисляться в runtime, но при intern()
будет выполнена проверка на наличие точно такой же строки в пуле строк (а она там уже есть), поэтому, фактически, уже будет сравниваться литеральная строка, полученная при компиляции со строкой, идентичной той, которая получена из пула принудительно.
Стоит отметить, что мнения относительно целесообразности использования метода
intern
расходятся и значимая часть разработчиков склоняется к мнению, что лучше собственная реализация кэша строк, чем использование стандартногоString
pool
Строковые блоки
Для простоты восприятия в Java 17 добавлены строковые блоки, которые позволяют добавлять многострочные символьные переменные. Ранее перенос строки обозначался спецсимволом \\n
, теперь же добиться подобного эффекта стало значительно проще с помощью тройных кавычек:
1 | String str = """ |
На примере выше переменная str
будет иметь значение: 1\\n 2\\n 3\\n
, т.е. состоять из 4 строк. Почему же перед “1” и “2” появились пробелы? Дело в том что Java для удобства восприятия игнорирует первые символы пробела в отформатированном блоке. В качестве начала отформатированного блока рассматривается первая строка, в которой присутствует не пробел:
1 | String str = """ |
Светлосерым выделен отступ, который считается отступом для строкового блока (для удобства чтения). Темно-серым будут признаны пробелы, которые Java будет рассматривать как обязательные пробелы для заполнения в строке.
Особенности строковых блоков
Строковые блоки имеют ряд особенностей, отличающих их от классических строк:
Символ пробела \\s
будет рассматриваться как 1 пробел в рамках строкового блока и с него будет считаться отступ (если он будет первым символом). В классическом определении строки, символ \\s
будет рассматриваться как 2 пробела:
1 | String str = """ |
Результатом выполнения будет: \\n First chars\\n
.
Если символ пробела - ‘ ‘ - будет в конце строкового блока, то он просто проигнорируется:
1 | String str = """ |
Врезультате str получит значение: “First chars”
Первая строка должна начинаться с новой строки, после определения блока:
1 | String str = "First chars"; // ошибка компиляции. строковый блок должен начинаться с новой строки |
Символ ‘\’ в строковом блоке не (!) будет рассматриваться как начало новой строки. В классическом определении строки, компилятор выдаст ошибку.
1 | String str = """ |
При объявлении errorStringBlock#
будут получены ошибки, поскольку после символа “\“ присутствует какой-то символ. В первом же случае после символа “\“ следует перенос строки, а в этом случае он не будет рассматриваться как символ экранирования (escape-symbol), а значит будет просто проигнорирован.
StringBuilder
Если ожидается большое количество операций над строкой, то рекомендуется использовать StringBuidler
(или его потокобезопасный аналог - StringBuffer
).
Конструктор StringBuilder(int)
задает начальный размер строки. Так, операция заполнения (конкатенации) будет выполняться быстрее (до указанной размерности, конечно).
insert(int, String)
Метод вставляет строку (символ, цифры, байт и т.д. - зависит от сигнатуры) в определенную позицию (не заменяет, а дополняет строку, сдвигая символ после позиции вставки вправо).
equals()
Для StringBuilder
он работает не так как можно ожидать - так же как и ==
, он просто проверяет ссылки, а не содержимое объектов.
1 | StringBuilder sb1 = new StringBuilder("a"); |
Чтобы проверить идентичность значений в разных StringBuilder
следует преобразовать их в строки путем вызова toString()
, а затем уже отдельные строки сравнивать через equals()
.
Определение имен переменных
Java позволяет вставлять символы валют в имена переменных. Не только доллар ‘$’, но и евро, рубль, йену и другие.
Java не дает возможность определять несколько переменных на одной строке с разными типами. Точнее, переопределять! Например, в примере ниже компилятор посчитает это действие переопределением и выдаст ошибку компиляции:
1 | double x=1D, double y=2D; // ошибка компиляции. смена типа определения переменных |
Инициализация переменных
Для локальных переменных (переменных в блоках), Java требует инициализации в случае их использования:
1 | { |
Но при этом:
1 | { |
Переменная z
, так же как и в примере ранее x
, не инициализирована, но компилятор не обратит на это внимание, поскольку z
нигде не используется!
Компилятор будет проверять условия инициализации для всех вероятных веток. Т.е. если будут присутствовать условия, которые не инициализируют переменные, но будет найдено их использование - будет выдана ошибка компиляции. Иначе говоря:
1 | int a = 1; |
Определение переменной с помощью var
Начиная с Java 11, появилась возможность опускать определение типа переменной при ее инициализации. В действительности, это не совсем так - Java все равно присваивает ей тип, но делает это скрытно от разработчика.
Так же, использование var
допустимо только для локальных переменных (переменных в блоках метода), но не для instance
переменных или параметров методов.
Выбор типа для var
происходит в момент объявления переменной. По этой причине присвоение значения должно быть выполнено в момент объявления:
1 | var username; // ошибка компиляции. при инициализации переменной с применением var она должна быть инициализирована сразу |
Если компилятор не сможет определить тип, который должен быть присвоен переменной, он выдаст ошибку. Такое может произойти, например, если значение при инициализации будет null
:
1 | var x = null; |
Но при этом следующая переменная будет инициализирована, поскольку ей передан тип:
1 | var x = (String) null; |
Переменные var
нельзя объявлять в качестве параметров в метод, поскольку они не являются локальными переменными.
И важное примечание - var
не является ключевым словом, т.е. его можно использовать и для имени переменной, и для имени метода, и для пакета. Но класс с именем var
создавать запрещено.
Чеклист
- Следить за конкатенацией и типом полученных данных в результате ее выполнения
- Учитывать String-pool при операциях со строками. Если по коду выполняется проверка на соответствие ссылок (==), то с высокой вероятностью, участвует String-pool
- При фигурировании строковых блоков и проверки значения, следует особенно обратить внимание на:
- - пробелы в начале строк в блоке (относительно расположения первого символа). особое внимание на “\s” - они выступают как принудительные пробелы
- - блок должен начинаться с новой строки
- - символ “\“ без переноса строки после него или escape-символа выдаст ошибку
- Для проверки
StringBuilder
на идентичность строкequals
не подходит, следует сначала преобразовать их в строки (toString
), а затем сравнить черезequals
- Локальная переменная потребует инициализации только если она может использоваться (возможно использование при каком-то условии)
- Следить за тем где объявляется переменная с помощью
var
- допустимо только локальная переменна - Для массивов следить за правильностью типов - может ли объект быть добавлен. Если добавляемый объект не соответствует типу массива, будет
ArrayStoreException