$ sudo teach IT
МОДУЛЬ 11 · УРОК 1

Работа со строками (String, StringBuilder)

Строки — это, пожалуй, самый используемый тип данных в любой Java-программе. Вы будете работать с ними постоянно: читать ввод пользователя, формировать ответы, обрабатывать тексты, парсить файлы. В этом уроке мы досконально разберём работу со строками: неизменяемость String, мощный StringBuilder для эффективной работы и все основные методы, которые вы будете использовать каждый день.

⏱ ~35 минут 🎓 Средний уровень ☕ Java

📖 Что такое 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 Pool

Чтобы пул строк работал, строки должны быть неизменяемыми. Если бы одну и ту же строку из пула мог изменить кто-то другой, это привело бы к хаосу.

🧵
Потокобезопасность

String автоматически потокобезопасен — его не нужно синхронизировать, потому что его никто не может изменить. Это упрощает многопоточное программирование.

🎯
hashCode кеширование

Раз строка не меняется, её хеш-код можно вычислить один раз и закешировать. 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 вопросов

StringBuilder

Premium