Работа со строками (String, StringBuilder)
Строки — это, пожалуй, самый используемый тип данных в любой Java-программе. Вы будете работать с ними постоянно: читать ввод пользователя, формировать ответы, обрабатывать тексты, парсить файлы. В этом уроке мы досконально разберём работу со строками: неизменяемость String, мощный StringBuilder для эффективной работы и все основные методы, которые вы будете использовать каждый день.
📖 Что такое String в Java?
String в Java — это не просто «строка текста». Это полноценный класс, который представляет последовательность символов (char). Каждая строка, которую вы когда-либо создавали в Java — будь то "Привет", "" (пустая строка) или огромный JSON на миллион символов — это объект класса java.lang.String.
В отличие от многих других языков программирования, где строка — это просто массив символов (как в C или C++), в Java строка — это объект первого класса со своим собственным API. Это даёт огромное преимущество: вам не нужно писать функции для поиска подстроки, замены символов или разбиения текста — всё уже встроено.
Внутри себя String хранит массив byte[] (начиная с Java 9 — компактное представление) или char[] (в более старых версиях). Но вы никогда не работаете с этим массивом напрямую — вы используете методы класса String.
public class StringExample {
public static void main(String[] args) {
String greeting = "Привет, мир!"; // создание строки через литерал
String empty = ""; // пустая строка
String fromNew = new String("Hello"); // создание через конструктор (редко)
String fromChars = new String(new char[]{'J', 'a', 'v', 'a'});
System.out.println(greeting); // Привет, мир!
System.out.println(empty); // (ничего)
System.out.println(fromNew); // Hello
System.out.println(fromChars); // Java
}
}
💡 Важно: String литералы (строки в двойных кавычках) хранятся в специальном месте памяти — String Pool (пул строк). Если вы дважды напишете "Привет", Java не будет создавать два объекта — она возьмёт один из пула. Это экономит память. Конструктор new String("...") этого пула не использует и всегда создаёт новый объект — поэтому его почти никогда не применяют.
🔒 Неизменяемость String (Immutability)
Это, пожалуй, самое важное, что нужно понять про String в Java. Объекты класса String неизменяемы (immutable). Это означает, что после создания строки вы никак не можете изменить её содержимое. Никак. Совсем.
Представьте себе книгу. Если вы хотите изменить слово в книге — вы не можете стереть его и написать новое поверх. Вам придётся выпустить новое издание книги. Так же и со String: любая операция, которая «изменяет» строку, на самом деле создаёт новый объект String.
public class ImmutabilityDemo {
public static void main(String[] args) {
String s = "Hello";
String t = s.toUpperCase(); // метод toUpperCase НЕ меняет s
System.out.println(s); // Hello — исходная строка не изменилась!
System.out.println(t); // HELLO — создана новая строка
// Ещё пример:
String a = "Java";
String b = a.concat("!"); // concat тоже создаёт новую строку
System.out.println(a); // Java
System.out.println(b); // Java!
}
}
Почему String сделали неизменяемым? У этого решения есть несколько веских причин:
Строки используются для хранения паролей, URL, путей к файлам, SQL-запросов. Неизменяемость гарантирует, что никто не изменит строку «за вашей спиной» — например, в многопоточной программе.
Чтобы пул строк работал, строки должны быть неизменяемыми. Если бы одну и ту же строку из пула мог изменить кто-то другой, это привело бы к хаосу.
String автоматически потокобезопасен — его не нужно синхронизировать, потому что его никто не может изменить. Это упрощает многопоточное программирование.
Раз строка не меняется, её хеш-код можно вычислить один раз и закешировать. String так и делает: hashCode вычисляется при первом вызове и сохраняется. Это ускоряет работу HashMap и HashSet.
⚠️ Последствие неизменяемости: если в цикле много раз «изменять» строку через конкатенацию (+), каждый раз будет создаваться новый объект. Это очень медленно. Для таких случаев придуман StringBuilder — о нём чуть ниже.
🔧 Основные методы String
Класс String содержит десятки методов. Мы разберём самые важные, которые вы будете использовать постоянно. Разделим их на группы.
📌 Получение символов и длины
public class StringBasicMethods {
public static void main(String[] args) {
String text = "Привет, Java!";
// length() — длина строки
int len = text.length();
System.out.println("Длина: " + len); // 13
// charAt(int index) — символ по индексу (индексация с 0)
char first = text.charAt(0);
char last = text.charAt(text.length() - 1);
System.out.println("Первый: " + first); // П
System.out.println("Последний: " + last); // !
// toCharArray() — получить массив символов
char[] chars = text.toCharArray();
System.out.println(Arrays.toString(chars));
// [П, р, и, в, е, т, ,, ,, J, a, v, a, !]
}
}
☝️ Индексация: в Java, как и в большинстве языков, индексы начинаются с 0. Первый символ — индекс 0, последний — length() - 1. Если вы попытаетесь обратиться к индексу за пределами строки, получите StringIndexOutOfBoundsException.
📌 Поиск подстроки: indexOf, lastIndexOf, contains
public class SearchMethods {
public static void main(String[] args) {
String phrase = "Java — это круто! Java — язык №1.";
// indexOf — первое вхождение (возвращает индекс или -1)
int pos1 = phrase.indexOf("Java");
System.out.println("Первое 'Java' на позиции: " + pos1); // 0
int pos2 = phrase.indexOf("Java", 5); // поиск с позиции 5
System.out.println("Следующее 'Java' на позиции: " + pos2); // 18
int pos3 = phrase.indexOf("Python");
System.out.println("'Python' найден? " + (pos3 != -1)); // false
// lastIndexOf — последнее вхождение
int lastPos = phrase.lastIndexOf("Java");
System.out.println("Последнее 'Java' на позиции: " + lastPos); // 18
// contains — проверяет, содержит ли строка подстроку
boolean hasJava = phrase.contains("Java");
boolean hasPython = phrase.contains("Python");
System.out.println("Содержит 'Java': " + hasJava); // true
System.out.println("Содержит 'Python': " + hasPython); // false
// startsWith / endsWith — проверка начала и конца
System.out.println(phrase.startsWith("Java")); // true
System.out.println(phrase.endsWith("№1.")); // true
}
}
💡 Жизненная аналогия: indexOf — это как поиск по документу (Ctrl+F). Вы ищете слово, и программа показывает, где оно встречается. Если слово не найдено — возвращается -1. Всегда проверяйте результат indexOf на -1, иначе можете получить ошибку!
📌 Извлечение части строки: substring
public class SubstringDemo {
public static void main(String[] args) {
String text = "Hello, World!";
// substring(beginIndex) — от индекса до конца
String sub1 = text.substring(7);
System.out.println("С 7-го индекса: " + sub1); // World!
// substring(beginIndex, endIndex) — от begin до end (endIndex НЕ включается)
String sub2 = text.substring(0, 5);
System.out.println("С 0 по 5 (не включая 5): " + sub2); // Hello
String sub3 = text.substring(7, 12);
System.out.println("С 7 по 12: " + sub3); // World
// Практический пример: извлечение домена из email
String email = "user@gmail.com";
String domain = email.substring(email.indexOf("@") + 1);
System.out.println("Домен: " + domain); // gmail.com
// Извлечение имени пользователя
String username = email.substring(0, email.indexOf("@"));
System.out.println("Имя: " + username); // user
}
}
⚠️ Важно: substring(begin, end) не включает символ с индексом end. То есть substring(0, 5) вернёт символы с 0 по 4 — всего 5 символов. Это правило «начало включительно, конец исключительно» (в классической Java-терминологии). Такое же правило используется в Arrays.copyOfRange, List.sublist и многих других API.
📌 Сравнение строк: equals, equalsIgnoreCase, compareTo
Это одна из самых частых «ловушек» для новичков в Java. Строки нельзя сравнивать через ==! Ну, точнее, можно, но это сравнивает ссылки (адреса в памяти), а не содержимое строк.
public class StringComparison {
public static void main(String[] args) {
String a = "Java";
String b = "Java";
String c = new String("Java");
// == сравнивает ССЫЛКИ, не содержимое!
System.out.println(a == b); // true (оба из пула строк — один объект)
System.out.println(a == c); // false (c — новый объект, другая ссылка!)
// equals сравнивает содержимое — ВСЕГДА ИСПОЛЬЗУЙТЕ ЕГО
System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // true
// equalsIgnoreCase — без учёта регистра
String d = "JAVA";
System.out.println(a.equals(d)); // false (регистр разный)
System.out.println(a.equalsIgnoreCase(d)); // true
// compareTo — лексикографическое сравнение (для сортировки)
// Возвращает: 0 — равны, <0 — строка меньше, >0 — строка больше
System.out.println("apple".compareTo("banana")); // отрицательное (a < b)
System.out.println("apple".compareTo("apple")); // 0
System.out.println("banana".compareTo("apple")); // положительное
// compareToIgnoreCase — без учёта регистра
System.out.println("JAVA".compareToIgnoreCase("java")); // 0
}
}
🚫 Золотое правило: Всегда используйте equals() для сравнения строк! Никогда == (если только вы намеренно не сравниваете, указывают ли две переменные на один и тот же объект, что бывает крайне редко). == может вернуть true для одинаковых литералов (из-за пула), но new String("Java") — уже false. Не попадайтесь в эту ловушку!
📌 Модификация строк: toLowerCase, toUpperCase, trim, replace
public class StringModification {
public static void main(String[] args) {
String text = " Java Программирование ";
// toLowerCase / toUpperCase — смена регистра
System.out.println(text.toLowerCase()); // java программирование
System.out.println(text.toUpperCase()); // JAVA ПРОГРАММИРОВАНИЕ
// trim — удаляет пробелы и другие пробельные символы с начала и конца
System.out.println(text.trim()); // "Java Программирование"
// replace(char old, char new) — замена символа
String phone = "+7-123-456-78-90";
String clean = phone.replace("-", "");
System.out.println(clean); // +71234567890
// replace(CharSequence target, CharSequence replacement) — замена подстроки
String message = "Hello, %user%! Your balance is %amount%.";
String filled = message
.replace("%user%", "Иван")
.replace("%amount%", "1500");
System.out.println(filled); // Hello, Иван! Your balance is 1500.
// replaceAll — с поддержкой regex (регулярных выражений)
String dirty = " много пробелов ";
String clean2 = dirty.trim().replaceAll("\\s+", " ");
System.out.println(clean2); // много пробелов
// repeat (Java 11+) — повторение строки
String stars = "*".repeat(10);
System.out.println(stars); // **********
}
}
💡 Аналогия метода replace: представьте, что у вас есть документ, и вы хотите заменить одно слово на другое во всём тексте. replace — это как функция «Найти и заменить» в текстовом редакторе. Все вхождения будут заменены, и вы получите новую строку (исходная не изменится).
📌 Разбиение и соединение: split, join
public class SplitJoinDemo {
public static void main(String[] args) {
// split(String regex) — разбивает строку на массив строк по разделителю
String csv = "яблоко,груша,банан,апельсин";
String[] fruits = csv.split(",");
System.out.println(Arrays.toString(fruits));
// [яблоко, груша, банан, апельсин]
// split с лимитом
String data = "a:b:c:d";
String[] parts1 = data.split(":", 2);
System.out.println(Arrays.toString(parts1)); // [a, b:c:d]
String[] parts2 = data.split(":", 3);
System.out.println(Arrays.toString(parts2)); // [a, b, c:d]
// split по точке — нужно экранировать!
String ip = "192.168.1.1";
String[] octets = ip.split("\\."); // точка — спецсимвол в regex
System.out.println(Arrays.toString(octets)); // [192, 168, 1, 1]
// split по нескольким разделителям
String messy = "яблоко,груша;банан апельсин";
String[] items = messy.split("[,; ]");
System.out.println(Arrays.toString(items));
// [яблоко, груша, банан, апельсин]
// join (статический метод) — соединяет массив строк с разделителем
String joined = String.join(", ", fruits);
System.out.println(joined); // яблоко, груша, банан, апельсин
// join с Iterable
List<String> list = List.of("один", "два", "три");
String result = String.join(" | ", list);
System.out.println(result); // один | два | три
}
}
🔑 Обратите внимание: split принимает регулярное выражение, а не просто строку. Поэтому символы вроде точки (.), вертикальной черты (|), звёздочки (*) нужно экранировать обратной косой чертой. В Java вам нужно писать "\\." (сначала экранирование для Java-строки, потом для regex). Это один из самых частых источников ошибок у новичков.
📌 Проверка и форматирование: isEmpty, isBlank, valueOf, format
public class StringUtilsMethods {
public static void main(String[] args) {
// isEmpty — проверяет, пустая ли строка (length == 0)
System.out.println("".isEmpty()); // true
System.out.println(" ".isEmpty()); // false (пробелы — не пустота)
// isBlank (Java 11+) — проверяет, содержит ли строка только пробелы
System.out.println("".isBlank()); // true
System.out.println(" ".isBlank()); // true
System.out.println(" a ".isBlank()); // false
// valueOf — статический метод, преобразует разные типы в строку
String s1 = String.valueOf(123); // "123"
String s2 = String.valueOf(45.67); // "45.67"
String s3 = String.valueOf(true); // "true"
String s4 = String.valueOf(new char[]{'A', 'B'}); // "AB"
// format — создаёт строку по шаблону (как printf)
String name = "Иван";
int age = 25;
double salary = 75000.50;
String info = String.format(
"Имя: %s, Возраст: %d, Зарплата: %.2f",
name, age, salary
);
System.out.println(info);
// Имя: Иван, Возраст: 25, Зарплата: 75000.50
// %s — строка, %d — целое, %f — число с плавающей точкой
// %n — перевод строки, %t — дата/время
}
}
💡 Совет: String.valueOf() безопаснее, чем конкатенация с пустой строкой ("" + number). valueOf корректно обрабатывает null и возвращает строку "null", в то время как "" + null даёт "null" — но это менее очевидный код. Впрочем, на практике чаще используют String.format() или современный str.formatted() (Java 15+).
🔗 Конкатенация строк: оператор + и её цена
Самый простой способ соединить строки — оператор +. Но у него есть скрытая цена, особенно в циклах.
public class ConcatenationDemo {
public static void main(String[] args) {
// Простая конкатенация — всё хорошо
String hello = "Привет, " + "мир!";
System.out.println(hello); // Привет, мир!
// Конкатенация с разными типами
String message = "Возраст: " + 30;
System.out.println(message); // Возраст: 30
// Компилятор умный: это преобразуется в StringBuilder
String a = "Java";
String b = "версия";
int c = 21;
String result = a + " " + b + " " + c;
// Компилятор сгенерирует что-то вроде:
// new StringBuilder().append(a).append(" ").append(b).append(" ").append(c).toString()
}
}
Проблема возникает, когда конкатенация происходит в цикле:
public class BadConcatenation {
public static void main(String[] args) {
// ⚠️ ПЛОХОЙ КОД: конкатенация в цикле
String result = "";
for (int i = 0; i < 1000; i++) {
result = result + i + " "; // Каждая итерация создаёт НОВЫЙ объект String!
}
// Это создаёт ~1000 промежуточных строк — очень медленно и расточительно
// ✅ ХОРОШИЙ КОД: StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i).append(" ");
}
String goodResult = sb.toString();
// Создаётся один StringBuilder и одна итоговая строка
}
}
⚠️ Аналогия: конкатенация в цикле — это как если бы вы каждый раз при добавлении слова переписывали всю книгу заново. StringBuilder — это как текстовый редактор: вы просто дописываете текст в конец, не переписывая всё сначала. Разница в скорости может быть в сотни и тысячи раз!
⚡ StringBuilder — изменяемый String
StringBuilder — это класс, который представляет изменяемую последовательность символов. В отличие от String, StringBuilder можно менять: добавлять символы, вставлять, удалять, переворачивать — и всё это без создания новых объектов.
💡 Аналогия из жизни: String — это книга в твёрдой обложке. Чтобы изменить текст, нужно выпустить новое издание. StringBuilder — это текстовый файл в редакторе: вы открыли, отредактировали, сохранили. Никаких промежуточных копий.
Как устроен StringBuilder внутри? У него есть внутренний массив byte[] или char[], который автоматически расширяется при необходимости. Когда вы вызываете append(), символы просто записываются в этот массив. Когда нужно получить итоговую строку — вызываете toString(), который создаёт новый String из этого массива.
📌 Создание StringBuilder
public class StringBuilderCreate {
public static void main(String[] args) {
// Пустой StringBuilder с ёмкостью по умолчанию (16 символов)
StringBuilder sb1 = new StringBuilder();
// StringBuilder с заданной начальной ёмкостью
StringBuilder sb2 = new StringBuilder(100);
// StringBuilder из существующей строки
StringBuilder sb3 = new StringBuilder("Hello");
System.out.println("Ёмкость: " + sb1.capacity()); // 16
System.out.println("Ёмкость: " + sb2.capacity()); // 100
System.out.println(sb3.toString()); // Hello
}
}
📌 append — добавление в конец
public class StringBuilderAppend {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(' ');
sb.append("World");
sb.append(2024); // можно добавлять любые типы
sb.append(true);
sb.append(3.14);
System.out.println(sb.toString());
// Hello World2024true3.14
// Цепочка вызовов (fluent interface) — очень удобно!
StringBuilder sb2 = new StringBuilder()
.append("INSERT INTO users (name, age) VALUES ('")
.append("Иван")
.append("', ")
.append(30)
.append(")");
System.out.println(sb2.toString());
// INSERT INTO users (name, age) VALUES ('Иван', 30)
}
}
Почему fluent interface (цепочка вызовов) так удобен? Каждый метод append возвращает сам StringBuilder, поэтому вы можете вызывать методы подряд. Это делает код компактным и читаемым. С String так не получится — каждый метод возвращает новую строку, а не саму себя.
📌 insert — вставка в произвольную позицию
public class StringBuilderInsert {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello World!");
// insert(offset, значение) — вставляет в указанную позицию
sb.insert(5, ",");
System.out.println(sb.toString()); // Hello, World!
// insert в начало
sb.insert(0, "Java says: ");
System.out.println(sb.toString()); // Java says: Hello, World!
// insert с другими типами
StringBuilder sb2 = new StringBuilder("Age: ");
sb2.insert(5, 25);
System.out.println(sb2.toString()); // Age: 25
}
}
📌 delete и deleteCharAt — удаление
public class StringBuilderDelete {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello, World!");
// delete(start, end) — удаляет символы с start по end (end не включается)
sb.delete(5, 7); // удаляем ", "
System.out.println(sb.toString()); // HelloWorld!
// deleteCharAt(index) — удаляет один символ по индексу
sb.deleteCharAt(5);
System.out.println(sb.toString()); // Helloorld!
// Очистка StringBuilder — удалить всё
sb.delete(0, sb.length());
System.out.println("Пусто: '" + sb.toString() + "'"); // Пусто: ''
}
}
📌 reverse — переворот строки
public class StringBuilderReverse {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Java");
sb.reverse();
System.out.println(sb.toString()); // avaJ
// Практический пример: проверка на палиндром
String word = "топот";
StringBuilder reversed = new StringBuilder(word).reverse();
boolean isPalindrome = word.equals(reversed.toString());
System.out.println("Палиндром? " + isPalindrome); // true
}
}
📌 replace, substring, и другие методы
public class StringBuilderOtherMethods {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello, World!");
// replace(start, end, str) — заменяет диапазон символов строкой
sb.replace(7, 12, "Java");
System.out.println(sb.toString()); // Hello, Java!
// substring — возвращает подстроку (сам StringBuilder не меняется)
String sub = sb.substring(0, 5);
System.out.println(sub); // Hello
// charAt, length, indexOf — работают как в String
System.out.println(sb.charAt(0)); // H
System.out.println(sb.length()); // 12
System.out.println(sb.indexOf("Java")); // 7
// setCharAt — заменяет символ по индексу (аналога в String нет!)
sb.setCharAt(0, 'h');
System.out.println(sb.toString()); // hello, Java!
}
}
💡 Когда использовать StringBuilder? В трёх случаях: (1) конкатенация в цикле, (2) построение сложной строки из многих частей (SQL-запрос, HTML, JSON), (3) когда нужно часто модифицировать строку (вставлять, удалять). Для простой конкатенации 2-5 строк — обычный + вполне подходит, компилятор сам оптимизирует.
🔄 StringBuffer vs StringBuilder
У Java есть два класса для изменяемых строк: StringBuffer (появился в Java 1.0) и StringBuilder (появился в Java 5). Они делают одно и то же, но с одним важным отличием.
| Характеристика | StringBuffer | StringBuilder |
|---|---|---|
| Появился в версии | Java 1.0 | Java 5 |
| Потокобезопасность | ✅ Да (синхронизирован) | ❌ Нет |
| Производительность | 🐢 Медленнее (из-за синхронизации) | ⚡ Быстрее |
| Рекомендуется | Только для многопоточного доступа | ✅ Для всех остальных случаев |
public class BufferVsBuilder {
public static void main(String[] args) {
// StringBuilder — быстрее, не синхронизирован
StringBuilder builder = new StringBuilder();
builder.append("Hello").append(" ").append("World");
// StringBuffer — медленнее, синхронизирован
StringBuffer buffer = new StringBuffer();
buffer.append("Hello").append(" ").append("World");
System.out.println(builder.toString()); // Hello World
System.out.println(buffer.toString()); // Hello World
}
}
💡 Правило большого пальца: всегда используйте StringBuilder. StringBuffer нужен только если несколько потоков одновременно пишут в один и тот же буфер (что на практике встречается крайне редко). В 99.9% случаев StringBuilder — правильный выбор.
🏋️ Практические примеры
Теперь соединим всё вместе на реальных задачах.
📌 Пример 1: Форматирование телефонного номера
public class PhoneFormatter {
public static String formatPhone(String raw) {
// Оставляем только цифры
String digits = raw.replaceAll("\\D", "");
if (digits.length() != 11) {
throw new IllegalArgumentException("Неверная длина номера: " + digits.length());
}
// +7 (999) 123-45-67
return new StringBuilder()
.append("+")
.append(digits.charAt(0)) // 7
.append(" (")
.append(digits.substring(1, 4)) // 999
.append(") ")
.append(digits.substring(4, 7)) // 123
.append("-")
.append(digits.substring(7, 9)) // 45
.append("-")
.append(digits.substring(9, 11)) // 67
.toString();
}
public static void main(String[] args) {
System.out.println(formatPhone("+7-999-123-45-67"));
// +7 (999) 123-45-67
System.out.println(formatPhone("79991234567"));
// +7 (999) 123-45-67
}
}
📌 Пример 2: Генератор SQL-запросов
public class QueryBuilder {
public static String buildInsertQuery(String table, String[] columns, String[] values) {
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO ").append(table).append(" (");
sb.append(String.join(", ", columns));
sb.append(") VALUES (");
for (int i = 0; i < values.length; i++) {
if (i > 0) sb.append(", ");
sb.append("'").append(values[i].replace("'", "''")).append("'");
}
sb.append(")");
return sb.toString();
}
public static void main(String[] args) {
String query = buildInsertQuery(
"users",
new String[]{"name", "email", "age"},
new String[]{"Иван", "ivan@mail.com", "30"}
);
System.out.println(query);
// INSERT INTO users (name, email, age) VALUES ('Иван', 'ivan@mail.com', '30')
}
}
📌 Пример 3: Анализ текста
public class TextAnalyzer {
public static void analyze(String text) {
if (text == null || text.isBlank()) {
System.out.println("Текст пуст");
return;
}
String[] words = text.trim().split("\\s+");
int letters = 0;
int digits = 0;
int spaces = 0;
int punct = 0;
for (char c : text.toCharArray()) {
if (Character.isLetter(c)) letters++;
else if (Character.isDigit(c)) digits++;
else if (Character.isWhitespace(c)) spaces++;
else punct++;
}
StringBuilder report = new StringBuilder();
report.append("=== Анализ текста ===\n");
report.append("Символов: ").append(text.length()).append("\n");
report.append("Слов: ").append(words.length).append("\n");
report.append("Букв: ").append(letters).append("\n");
report.append("Цифр: ").append(digits).append("\n");
report.append("Пробелов: ").append(spaces).append("\n");
report.append("Знаков препинания: ").append(punct).append("\n");
System.out.println(report.toString());
}
public static void main(String[] args) {
analyze("Java 21 — это круто! А ты пробовал?");
}
}
📌 Пример 4: Обработка CSV
public class CsvProcessor {
public static void main(String[] args) {
String csvData = """
name,age,city
Иван,30,Москва
Мария,25,Санкт-Петербург
Алексей,35,Казань
""";
String[] lines = csvData.trim().split("\n");
// Заголовок
String[] headers = lines[0].split(",");
System.out.println("Колонки: " + Arrays.toString(headers));
// Данные
for (int i = 1; i < lines.length; i++) {
String[] values = lines[i].split(",");
StringBuilder row = new StringBuilder();
for (int j = 0; j < headers.length; j++) {
if (j > 0) row.append(" | ");
row.append(headers[j].trim()).append(": ").append(values[j].trim());
}
System.out.println(row.toString());
}
}
}
🔑 Вывод: String и StringBuilder — это не конкуренты, а инструменты для разных задач. String — для неизменяемых текстовых данных, которые вы не собираетесь менять. StringBuilder — когда вы строите или модифицируете строку. Используйте правильный инструмент для правильной задачи.
🗄️ Пул строк и метод intern()
Раз уж мы говорим о строках, нужно упомянуть про пул строк (String Pool) и метод intern(). Когда Java встречает строковый литерал, она сначала проверяет, есть ли такая строка в пуле. Если есть — использует существующую, если нет — добавляет новую.
public class InternDemo {
public static void main(String[] args) {
// Литералы автоматически интернируются
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true — один объект из пула
// new String() НЕ интернируется автоматически
String s3 = new String("Java");
System.out.println(s1 == s3); // false — это новый объект
// intern() — принудительно добавляет в пул и возвращает канонический объект
String s4 = s3.intern();
System.out.println(s1 == s4); // true — теперь из пула
}
}
⚠️ Важно: Не вызывайте intern() без необходимости. Это может привести к тому, что пул строк разрастётся до огромных размеров, а intern() сам по себе — дорогая операция. На практике метод intern() используется редко, обычно в специфических оптимизациях.
📊 Производительность: String vs StringBuilder
Давайте посмотрим на цифры. Вот простой бенчмарк, который сравнивает конкатенацию строк через + и через StringBuilder:
public class PerformanceBenchmark {
public static void main(String[] args) {
int iterations = 100_000;
// StringBuilder
long start = System.nanoTime();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sb.append("число: ").append(i).append(", ");
}
long end = System.nanoTime();
System.out.println("StringBuilder: " + (end - start) / 1_000_000 + " ms");
// String concatenation (ОЧЕНЬ МЕДЛЕННО)
start = System.nanoTime();
String s = "";
for (int i = 0; i < iterations; i++) {
s += "число: " + i + ", "; // каждая итерация = новый объект!
}
end = System.nanoTime();
System.out.println("String + : " + (end - start) / 1_000_000 + " ms");
}
}
Результат для 100 000 итераций впечатляет: StringBuilder — около 10-20 миллисекунд. Конкатенация через + — несколько секунд (в 100-500 раз медленнее). Теперь вы понимаете, почему в циклах нужно использовать StringBuilder?
🧩 StringJoiner и Collectors.joining
Java 8 добавила ещё один удобный класс — StringJoiner — и статический метод Collectors.joining() для Stream API. Они упрощают соединение строк с разделителями, особенно с префиксом и суффиксом.
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.List;
public class JoinerDemo {
public static void main(String[] args) {
// StringJoiner с разделителем
StringJoiner joiner = new StringJoiner(", ");
joiner.add("яблоко");
joiner.add("груша");
joiner.add("банан");
System.out.println(joiner.toString()); // яблоко, груша, банан
// StringJoiner с префиксом и суффиксом
StringJoiner json = new StringJoiner("\", \"", "[\"", "\"]");
json.add("один").add("два").add("три");
System.out.println(json.toString()); // ["один", "два", "три"]
// Collectors.joining для Stream
List<String> words = List.of("Java", "Python", "Go");
String result = words.stream()
.collect(Collectors.joining(" | ", "[[ ", " ]]"));
System.out.println(result); // [[ Java | Python | Go ]]
// Простой join через String.join
String simple = String.join(" + ", "1", "2", "3");
System.out.println(simple); // 1 + 2 + 3
}
}
✅ Итоги урока
- String — неизменяемый (immutable) класс для работы с текстом. Любая «модификация» создаёт новый объект
- Основные методы String: charAt, length, indexOf, substring, equals, split, join, replace, trim, toLowerCase, toUpperCase, format, valueOf, contains, startsWith, endsWith
- Строки нельзя сравнивать через
==— только черезequals()илиequalsIgnoreCase() - StringBuilder — изменяемая последовательность символов. Методы: append, insert, delete, deleteCharAt, reverse, replace, setCharAt, substring
- StringBuilder быстрее String при многократных модификациях, особенно в циклах. Всегда используйте StringBuilder для построения строк из многих частей
- StringBuffer — потокобезопасная версия StringBuilder (синхронизирована). В однопоточном коде используйте StringBuilder
- String.split() принимает регулярное выражение — не забывайте экранировать специальные символы (
\\.для точки) - String Pool — пул строковых литералов. Строки из пула могут сравниваться через ==, но это не повод так делать
- StringJoiner и Collectors.joining — удобные утилиты для соединения строк с разделителями
В следующем уроке разберём работу с датой и временем — java.time, LocalDate, LocalTime, LocalDateTime, DateTimeFormatter и многое другое. 🚀
Тест: Работа со строками (String, StringBuilder)
10 вопросов