$ sudo teach IT
МОДУЛЬ 12 · УРОК 3

Text Blocks и полезные мелочи

Text Blocks для многострочных строк, новые методы класса String, компактное форматирование чисел, Records, Helpful NullPointerExceptions и другие полезные нововведения Java 11–21, которые сделают ваш код чище и удобнее.

~30 минут Для продвинутых Java 21+

Зачем нужны Text Blocks

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

Начиная с Java 15 (preview) и стабильно с Java 17, появилась возможность создавать многострочные строковые литералы с помощью тройных кавычек """. Это называется Text Block — текстовый блок. Теперь вы можете писать многострочный текст ровно так, как он должен выглядеть, без лишних хитростей.

Сравните старый способ создания многострочной строки с новым. Раньше это выглядело так:

String jsonOld = "{\n" +
                 "    \"name\": \"Иван\",\n" +
                 "    \"age\": 30,\n" +
                 "    \"city\": \"Москва\"\n" +
                 "}";

А с Text Blocks — так:

String jsonNew = """
                 {
                     "name": "Иван",
                     "age": 30,
                     "city": "Москва"
                 }
                 """;

Важно: Text Blocks — это не новый тип данных. Это просто другой способ записать строковый литерал. Результат — обычный объект String. Вы можете применять к нему любые методы String, присваивать переменным типа String и передавать как аргумент.

Text Blocks особенно полезны в следующих сценариях:

  • JSON и XML шаблоны, которые выглядят нечитаемо при конкатенации
  • SQL-запросы, особенно сложные SELECT с JOIN
  • HTML-шаблоны для генерации страниц
  • Многострочные сообщения и шаблоны писем
  • Любой текст, который естественно записывается в несколько строк

Синтаксис Text Blocks: тройные кавычки

Text Block начинается с тройных двойных кавычек """, за которыми должен следовать пробел, табуляция или перенос строки (но не другой символ). Текст блока идёт после начала и закрывается тройными кавычками, которые должны стоять на自己的 строке.

Вот базовый синтаксис:

String text = """
              Это текстовый блок.
              Он может занимать несколько строк.
              Каждая строка включается в результат.
              """;

Обратите внимание на отступ закрывающих кавычек """. Их позиция определяет, сколько пробелов будет обрезано из начала каждой строки. Это называется «incidental white space» (побочный пробел).

Давайте разберём это подробнее. Представьте следующий код:

String html = """
              <html>
                  <body>
                      <p>Привет</p>
                  </body>
              </html>
              """;

Закрывающие """ стоят на позиции с 14 пробелами от начала строки. Компилятор возьмёт минимальный общий отступ (14 пробелов) и вычтет его из каждой строки. Результат будет:

<html>
    <body>
        <p>Привет</p>
    </body>
</html>

Первая строка <html> имеет 14 пробелов — все обрезаются. Строка <body> имеет 18 пробелов — обрезаются 14, остаётся 4. Это создаёт идеально отформатированный вывод без ручного подсчёта отступов.

Совет: Располагайте закрывающие """ ровно под началом первого символа текста блока. Это создаёт самый чистый результат без лишних отступов. Если хотите оставить отступы как есть — расположите закрывающие кавычки в самой левой колонке.

Управление отступами и пробелами

Text Blocks обрабатывают отступы автоматически и очень элегантно. Давайте разберём правила обработки:

Правило 1: Минимальный общий отступ всех строк (кроме первой и последней) определяется как количество пробелов в начале строки с закрывающими """.

Правило 2: Все строки получают вычтенный отступ. Если строка имеет меньше пробелов, чем минимальный — это ошибка компиляции.

Правило 3: Последний символ строки (перевод строки) перед закрывающими """ не включается в результат, если закрывающие кавычки стоят на自己的 строке.

String example = """
                 Первый
                   Второй
                     Третий
                 """;

В этом примере минимальный отступ — 17 пробелов (позиция закрывающих """). Результат:

Первый
  Второй
    Третий

Закрывающие """ могут стоять на own строке или сразу после текста. Если они стоят сразу после текста без переноса строки, побочный пробел обрезается по-другому:

String inline = """Привет""";

Здесь закрывающие кавычки идут сразу после текста — отступов для обрезки нет. Результат — просто Привет.

Метод String.stripIndent() позволяет применить ту же логику обрезки к обычной строке:

String raw = "    Привет\\n        Мир\\n           !";
String stripped = raw.stripIndent();
System.out.println(stripped);

Аналогия: Подумайте о закрывающих """ как о «линейке». Вы прикладываете линейку к закрывающим кавычкам, а потом вычитаете отмеренное расстояние из каждой строки. Если какая-то строка короче минимального отступа — линейка не совпадает, и компилятор сообщит об ошибке.

Управляющие последовательности в Text Blocks

Text Blocks поддерживают все обычные управляющие последовательности строк: \n, \t, \", \\ и другие. Но есть и новые последовательности, предназначенные специально для Text Blocks.

Перенос строки \ (обратный слэш)

Обратный слэш в конце строки подавляет автоматический перенос строки. Это позволяет записать одну «логическую» строку в несколько строк исходного кода:

String singleLine = """
                    Первое слово \
                    второе слово \
                    третье слово
                    """;

Результат — одна строка: Первое слово второе слово третье слово. Обратные слэши и пробелы после них удаляются.

Пробел \s (space escape)

Последовательность \s добавляет один пробел и предотвращает обрезку завершающих пробелов в строке. Это полезно, когда вы хотите сохранить пробелы в конце строки:

String withSpaces = """
                   Привет \s
                   Мир \s
                   Java \s
                   """;

Без \s завершающие пробелы были бы обрезаны. С \s каждый пробел сохраняется. Метод translateEscapes() заменяет \s на обычный пробел:

String raw = "Привет \\s\\nМир";
String translated = raw.translateEscapes();
System.out.println(translated);
System.out.println(translated.length());

Экранирование двойных кавычек

Если вам нужно включить тройные кавычки """ внутрь текстового блока, экранируйте их обратным слэшем. Или используйте обычные двойные кавычки \", которые в Text Blocks не требуют экранирования:

String quotes = """
               Java поддерживает "одинарные" кавычки.
               И также \"двойные\" кавычки.
               А вот тройные кавычки: \"\"\" не будут работать без экранирования.
               """;
String tripleQuote = """
                    Используем тройные кавычки: \\"\\"
                    Это экранированная версия.
                    """;

Запомните: Одинарные кавычки \" в Text Blocks можно не экранировать — Java различает отдельные " и тройные """. Это отличается от обычных строк, где любую кавычку нужно экранировать.

Text Blocks в реальных проектах: SQL, HTML, JSON

Давайте посмотрим, как Text Blocks делают код чище в реальных ситуациях.

SQL-запросы

SQL-запросы часто бывают длинными и сложными. С Text Blocks они становятся читаемыми и поддерживаемыми:

String sql = """
             SELECT
                 u.name,
                 u.email,
                 o.order_id,
                 o.total_amount,
                 o.order_date
             FROM
                 users u
             INNER JOIN
                 orders o ON u.user_id = o.user_id
             WHERE
                 o.order_date >= '2024-01-01'
                 AND o.total_amount > 1000
             ORDER BY
                 o.order_date DESC;
             """;

Сравните это с конкатенацией — код с Text Blocks читается как обычный SQL, и вы сразу видите структуру запроса.

HTML-шаблоны

Генерация HTML — ещё один отличный кандидат для Text Blocks:

String page = """
             <!DOCTYPE html>
             <html lang="ru">
             <head>
                 <meta charset="UTF-8">
                 <title>Привет, мир!</title>
                 <style>
                     body { font-family: Arial, sans-serif; }
                     h1 { color: #333; }
                 </style>
             </head>
             <body>
                 <h1>Привет, мир!</h1>
                 <p>Это страница, созданная с помощью Text Blocks.</p>
             </body>
             </html>
             """;

JSON-шаблоны

JSON — ещё один формат, который часто встречается в API и конфигурационных файлах:

String config = """
               {
                   "database": {
                       "host": "localhost",
                       "port": 5432,
                       "name": "mydb"
                   },
                   "cache": {
                       "enabled": true,
                       "ttl": 3600
                   },
                   "logging": {
                       "level": "INFO",
                       "file": "/var/log/app.log"
                   }
               }
               """;

Совет: Для серьёзных JSON-операций лучше использовать библиотеки (Jackson, Gson), а Text Blocks — для шаблонов, тестовых данных и простых строковых констант.

Новые методы класса String

Начиная с Java 11, класс String получил несколько новых методов, которые значительно упрощают повседневную работу со строками. Давайте разберём каждый из них.

isBlank() — проверка на пустоту

Метод isBlank() (Java 11+) проверяет, содержит ли строка только пробельные символы. Это расширение isEmpty(), которое раньше считало пробелы непустыми:

public class BlankDemo {
    public static void main(String[] args) {
        String empty = "";
        String spaces = "   ";
        String tabs = "\t\n";
        String text = "Hello";

        System.out.println(empty.isEmpty());      // true
        System.out.println(empty.isBlank());      // true

        System.out.println(spaces.isEmpty());     // false — есть пробелы!
        System.out.println(spaces.isBlank());     // true — только пробелы

        System.out.println(tabs.isEmpty());       // false
        System.out.println(tabs.isBlank());       // true

        System.out.println(text.isEmpty());       // false
        System.out.println(text.isBlank());       // false
    }
}

Где использовать: При валидации пользовательского ввода. Если пользователь ввёл только пробелы — это по сути пустой ввод. Раньше приходилось писать text.trim().isEmpty(), теперь достаточно text.isBlank().

lines() — разбиение на строки

Метод lines() (Java 11+) возвращает Stream<String> строк, разбитых по разделителю переноса строки. Это гораздо удобнее, чем ручное разбиение через split("\\n"):

import java.util.stream.Collectors;

public class LinesDemo {
    public static void main(String[] args) {
        String text = "Первая строка\nВторая строка\nТретья строка";

        // Разбиение на строки через stream
        List<String> lines = text.lines().collect(Collectors.toList());
        System.out.println(lines);
        // [Первая строка, Вторая строка, Третья строка]

        // Подсчёт строк
        long count = text.lines().count();
        System.out.println("Строк: " + count);  // 3

        // Фильтрация и преобразование
        String result = text.lines()
            .filter(line -> line.length() > 10)
            .map(String::toUpperCase)
            .collect(Collectors.joining("\n"));
        System.out.println(result);
    }
}

Обратите внимание: lines() корректно обрабатывает переносы \n, \r и \r\n, что делает его переносимым между операционными системами.

repeat() — повторение строки

Метод repeat(int count) (Java 11+) создаёт строку, состоящую из исходной строки, повторённой указанное количество раз:

public class RepeatDemo {
    public static void main(String[] args) {
        String dash = "-".repeat(20);
        System.out.println(dash);
        // --------------------

        String stars = "★ ".repeat(5);
        System.out.println(stars);
        // ★ ★ ★ ★ ★ 

        String indent = "    ".repeat(3);
        System.out.println(indent + "Отступ 12 пробелов");

        StringBuilder sb = new StringBuilder();
        sb.append("=".repeat(40));
        sb.append("\n");
        sb.append("ЗАГОЛОВОК".repeat(1));
        sb.append("\n");
        sb.append("=".repeat(40));
        System.out.println(sb);
        // ========================================
        // ЗАГОЛОВОК
        // ========================================
    }
}

strip(), stripLeading(), stripTrailing() — удаление пробелов

Методы strip(), stripLeading() и stripTrailing() (Java 11+) — это более совершенные версии trim(). Они корректно работают с Unicode-пробелами, в отличие от trim(), который удаляет только символы с кодом ≤ 32:

public class StripDemo {
    public static void main(String[] args) {
        String text = "  Привет, мир!  ";

        System.out.println(text.strip());          // "Привет, мир!"
        System.out.println(text.stripLeading());    // "Привет, мир!  "
        System.out.println(text.stripTrailing());   // "  Привет, мир!"

        // Unicode пробелы (NBSP и другие)
        String unicode = "\u00A0Привет\u00A0";
        System.out.println(unicode.strip());        // "Привет"
        System.out.println(unicode.trim());         // "\u00A0Привет\u00A0" — trim не удаляет!

        // Пустая строка
        String empty = "   ";
        System.out.println(empty.strip().isEmpty()); // true
    }
}

Важно: trim() удалит только стандартные пробелы (код ≤ 32). Unicode-пробелы типа неразрывного пробела (NBSP, \u00A0) trim() не удалит. Используйте strip() — он понимает все виды пробельных символов Unicode.

compare() vs compareTo(): безопасное сравнение

Класс String имеет метод compareTo(), который сравн строки лексикографически. Но у него есть серьёзная проблема: он не может работать с null. Если вызвать null.compareTo("test"), получите NullPointerException.

Начиная с Java 21, статический метод String.compare() решает эту проблему:

public class CompareDemo {
    public static void main(String[] args) {
        String a = "Apple";
        String b = "Banana";
        String c = null;

        // compareTo — классический метод (NullPointerException при null)
        System.out.println(a.compareTo(b));   // отрицательное (Apple < Banana)
        System.out.println(b.compareTo(a));   // положительное (Banana > Apple)
        System.out.println(a.compareTo(a));   // 0 (равны)
        // System.out.println(c.compareTo(a)); // NullPointerException!

        // compare — статический метод (null-safe, Java 21+)
        System.out.println(String.compare(a, b));   // отрицательное
        System.out.println(String.compare(b, a));   // положительное
        System.out.println(String.compare(a, a));   // 0
        System.out.println(String.compare(c, a));   // -1 (null считается меньше)
        System.out.println(String.compare(a, c));   // 1 (не-null больше null)
        System.out.println(String.compare(c, c));   // 0 (оба null — равны)
    }
}

Правила сравнения с String.compare():

  • Если оба аргумента null — возвращает 0 (равны)
  • Если первый null, второй нет — возвращает отрицательное (null меньше любой строки)
  • Если первый не null, второй null — возвращает положительное
  • Если оба не null — ведёт себя как compareTo()

Рекомендация: Всегда используйте String.compare() вместо compareTo(), когда работаете с потенциально null-значениями. Это избавит вас от лишних проверок на null и сделает код чище.

String.indent() — форматирование отступов

Метод String.indent(int n) (Java 12+) добавляет или убирает отступы из каждой строки. Положительное значение n добавляет n пробелов в начало каждой строки, отрицательное — убирает до -n пробелов:

public class IndentDemo {
    public static void main(String[] args) {
        String text = "Первая строка\nВторая строка\nТретья строка";

        // Добавить отступ 4 пробела
        String indented = text.indent(4);
        System.out.println(indented);
        //     Первая строка
        //     Вторая строка
        //     Третья строка

        // Убрать отступ
        String raw = "    Привет\n        Мир";
        String unindented = raw.indent(-4);
        System.out.println(unindented);
        // Привет
        //     Мир

        // Отрицательный indent убирает пробелы из НАЧАЛА каждой строки
        // но не добавляет их
        String padded = "  A\n    B\n      C";
        String trimmed = padded.indent(-2);
        System.out.println(trimmed);
        // A
        //   B
        //     C
    }
}

Метод indent() также нормализует переносы строк: он заменяет все комбинации \r\n и \r на \n, и добавляет перенос строки в конце, если его нет.

Практический пример: форматирование вывода логов:

public class LogFormatter {
    public static void formatLog(String componentName, String message) {
        String formatted = message.indent(4);
        System.out.println("[" + componentName + "]");
        System.out.println(formatted);
        System.out.println("-".repeat(40));
    }

    public static void main(String[] args) {
        String log = "Пользователь вошёл в систему\n" +
                     "IP: 192.168.1.100\n" +
                     "Время: 2024-01-15 14:30:00";
        formatLog("AUTH", log);
    }
}

Сравнение: indent() не то же самое, что stripIndent(). stripIndent() убирает общий отступ (как в Text Blocks), а indent() добавляет/убирает фиксированное количество пробелов. Они дополняют друг друга.

String.transform() — универсальное преобразование

Метод String.transform(Function<String, T> f) (Java 12+) применяет функцию-преобразование к строке и возвращает результат. Это позволяет создавать цепочки операций, не прерывая поток вызовов:

import java.util.function.Function;
import java.util.Base64;

public class TransformDemo {
    public static void main(String[] args) {
        String text = "Привет, Мир!";

        // Обычный стиль — промежуточные переменные
        String step1 = text.toLowerCase();
        String step2 = step1.strip();
        String step3 = step2.replace("!", "");
        System.out.println(step3);

        // С transform — цепочка вызовов
        String result = text
            .transform(String::toLowerCase)
            .transform(s -> s.strip())
            .transform(s -> s.replace("!", ""));
        System.out.println(result);

        // Base64 кодирование
        String encoded = "Hello World"
            .transform(Base64.getEncoder()::encodeToString);
        System.out.println(encoded);  // SGVsbG8gV29ybGQ=

        // Декодирование обратно
        String decoded = encoded
            .transform(Base64.getDecoder()::decode)
            .transform(bytes -> new String(bytes));
        System.out.println(decoded);  // Hello World
    }
}

transform() особенно полезен, когда:

  • Вы хотите применить метод, который не возвращает String (например, Base64.getEncoder()::encodeToString)
  • Вы строите цепочку преобразований, где каждый шаг — отдельная функция
  • Вы хотите вынести преобразование в отдельную функцию и переиспользовать его

Замечание: Метод transform() появился как preview в Java 12 и стал стабильным в Java 17. Он не ограничен типом String — работает с любым объектом (через обобщённый тип T transform(Function<T, T>)).

Stream.toList() — неизменяемый список

Начиная с Java 16, интерфейс Stream получил метод toList(), который создаёт неизменяемый список из элементов потока. Это более короткая и безопасная альтернатива collect(Collectors.toList()):

import java.util.stream.Collectors;
import java.util.List;

public class StreamToListDemo {
    public static void main(String[] args) {
        List<String> names = List.of("Иван", "Мария", "Алексей", "Ольга");

        // Старый способ (Java 8+) — создаст изменяемый список
        List<String> oldWay = names.stream()
            .filter(n -> n.length() > 4)
            .collect(Collectors.toList());

        // Новый способ (Java 16+) — создаст неизменяемый список
        List<String> newWay = names.stream()
            .filter(n -> n.length() > 4)
            .toList();

        System.out.println(oldWay);  // [Иван, Мария, Алексей, Ольга]
        System.out.println(newWay);  // [Иван, Мария, Алексей, Ольга]

        // Попытка изменить неизменяемый список — выбросит UnsupportedOperationException
        try {
            newWay.add("Новый");
        } catch (UnsupportedOperationException e) {
            System.out.println("Нельзя изменить неизменяемый список!");
        }

        // Преобразование чисел
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        List<String> descriptions = numbers.stream()
            .map(n -> "Число: " + n)
            .toList();
        System.out.println(descriptions);
        // [Число: 1, Число: 2, Число: 3, Число: 4, Число: 5]
    }
}
Характеристика collect(toList()) toList()
Версия Java Java 8+ Java 16+
Изменяемость Изменяемый список Неизменяемый список
Null в списке ✅ Допускает ✅ Допускает
Рекомендуется Когда нужен изменяемый список ✅ По умолчанию

Рекомендация: Используйте toList() по умолчанию. Неизменяемые коллекции безопаснее в многопоточной среде и предотвращают случайные модификации. Только если вам действительно нужен изменяемый список — используйте collect(Collectors.toList()).

Helpful NullPointerExceptions — понятные сообщения об ошибках

Начиная с Java 14 (и стабильно с Java 15), сообщения NullPointerException стали значительно информативнее. Раньше вы видели только «Cannot invoke method on null», а теперь Java показывает точное имя переменной и операцию, которая вызвала ошибку.

public class HelpfulNpeDemo {
    public static void main(String[] args) {
        String name = null;
        int length = name.length();

        // Раньше (Java 13 и ниже):
        // Exception in thread "main" java.lang.NullPointerException

        // Теперь (Java 14+):
        // Exception in thread "main" java.lang.NullPointerException:
        //   Cannot invoke "String.length()" because "name" is null
    }
}

Ещё пример — цепочки вызовов:

public class HelpfulNpeChain {
    public static void main(String[] args) {
        String input = null;

        // Цепочка вызовов
        String result = input.toLowerCase().strip().repeat(3);

        // Java 14+ покажет:
        // Cannot invoke "String.toLowerCase()" because the return value
        // of "String.strip()" is null
        //
        // Java покажет ПОСЛЕДНЮЮ операцию, которая вернула null
    }
}

Сложный пример с массивами и коллекциями:

import java.util.List;

public class HelpfulNpeComplex {
    public static void main(String[] args) {
        String[] array = null;
        System.out.println(array[0]);

        // Cannot load from int array because "array" is null

        List<String> list = null;
        String first = list.get(0);

        // Cannot invoke "java.util.List.get(int)" because "list" is null

        Object obj = null;
        int hash = obj.hashCode();

        // Cannot invoke "Object.hashCode()" because "obj" is null
    }
}

Это одна из тех функций, которая не добавляет возможностей, но делает жизнь разработчика значительно проще. Когда вы видите NPE в логах — теперь сразу понимаете, что именно было null и какая операция вызвала проблему.

Совет: Helpful NullPointerExceptions включены по умолчанию в Java 15+. Если вы используете Java 14, включите флаг -XX:+ShowCodeDetailsInExceptionMessages.

Records — альтернатива простым классам

Records (Java 14+, стабильно с Java 16) — это специальный тип класса, предназначенный для хранения неизменяемых данных. Records автоматически генерируют конструктор, геттеры, equals(), hashCode() и toString():

public class RecordDemo {
    record Point(int x, int y) {}
    record Person(String name, int age, String email) {}

    public static void main(String[] args) {
        Point p = new Point(10, 20);
        Person person = new Person("Иван", 30, "ivan@mail.com");

        // Автоматические геттеры (без префикса get)
        System.out.println(p.x());           // 10
        System.out.println(person.name());   // Иван

        // Автоматический toString
        System.out.println(p);               // Point[x=10, y=20]
        System.out.println(person);          // Person[name=Иван, age=30, email=ivan@mail.com]

        // Автоматический equals и hashCode
        Point p2 = new Point(10, 20);
        System.out.println(p.equals(p2));    // true

        // Records неизменяемы — нет сеттеров
        // p.x = 30; // Ошибка компиляции!
    }
}

Вы также можете добавлять кастомные методы, валидацию в конструкторе и дополнительные конструкторы:

public class RecordAdvanced {
    record Range(int min, int max) {
        Range {
            if (min > max) {
                throw new IllegalArgumentException(
                    "min (" + min + ") must be <= max (" + max + ")"
                );
            }
        }

        boolean contains(int value) {
            return value >= min && value <= max;
        }

        int size() {
            return max - min + 1;
        }

        static Range of(int min, int max) {
            return new Range(min, max);
        }
    }

    public static void main(String[] args) {
        Range range = Range.of(1, 10);
        System.out.println(range);             // Range[min=1, max=10]
        System.out.println(range.contains(5)); // true
        System.out.println(range.contains(15)); // false
        System.out.println(range.size());       // 10

        // Валидация в compact constructor
        try {
            Range invalid = new Range(10, 5);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            // min (10) must be <= max (5)
        }
    }
}

Когда использовать Records: Records идеально подходят для DTO (Data Transfer Objects), точек данных, результатов вычислений и любых неизменяемых структур данных. Если класс — это просто «контейнер для данных с equals/hashCode/toString» — используйте Record.

Компактное форматирование чисел

Начиная с Java 12, NumberFormat получил метод compact(), который создаёт формат для «компактного» отображения чисел. Например, вместо 1000000 будет показано «1M», а вместо 1500 — «1.5K»:

import java.text.NumberFormat;
import java.util.Locale;

public class CompactNumberDemo {
    public static void main(String[] args) {
        NumberFormat formatter = NumberFormat.getCompactNumberInstance(
            new Locale("ru", "RU"),
            NumberFormat.Style.SHORT
        );

        System.out.println(formatter.format(0));          // 0
        System.out.println(formatter.format(1));          // 1
        System.out.println(formatter.format(100));        // 100
        System.out.println(formatter.format(1000));       // 1 тыс.
        System.out.println(formatter.format(1500));       // 1,5 тыс.
        System.out.println(formatter.format(10000));      // 10 тыс.
        System.out.println(formatter.format(100000));     // 100 тыс.
        System.out.println(formatter.format(1000000));    // 1 млн
        System.out.println(formatter.format(1500000));    // 1,5 млн
        System.out.println(formatter.format(1000000000)); // 1 млрд

        // LONG стиль
        NumberFormat longFormatter = NumberFormat.getCompactNumberInstance(
            new Locale("ru", "RU"),
            NumberFormat.Style.LONG
        );
        System.out.println(longFormatter.format(1000));    // 1 тысяча
        System.out.println(longFormatter.format(1000000)); // 1 миллион

        // Английская локаль
        NumberFormat enFormatter = NumberFormat.getCompactNumberInstance(
            Locale.US,
            NumberFormat.Style.SHORT
        );
        System.out.println(enFormatter.format(1500));    // 1.5K
        System.out.println(enFormatter.format(1000000)); // 1M
    }
}

Это очень полезно для отображения статистики: количество пользователей, просмотров, загрузок и т.д. Вместо «1523456 просмотров» вы покажете «1.5 млн просмотров».

Поддерживаемые локали: Компактное форматирование работает для множества локалей. Для русского языка используются суффиксы «тыс.», «млн», «млрд». Для английского — «K», «M», «B». Выбор локали влияет на форматирование и суффиксы.

Files.readString() и Files.writeString() — работа с файлами

Начиная с Java 11, класс java.nio.file.Files получил два новых метода, которые значительно упрощают чтение и запись текстовых файлов. Раньше для чтения всего файла приходилось писать несколько строк кода с BufferedReader, теперь — одна строка:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;

public class FilesReadWriteDemo {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("example.txt");

        // Запись строки в файл (перезаписывает файл)
        Files.writeString(path, "Привет, мир!\nЭто вторая строка.");

        // Чтение всего файла в строку
        String content = Files.readString(path);
        System.out.println(content);
        // Привет, мир!
        // Это вторая строка.

        // Дозапись в конец файла
        Files.writeString(path, "\nТретья строка.",
            StandardOpenOption.APPEND);

        // Чтение с指定анием кодировки
        String utf16 = Files.readString(path, StandardCharsets.UTF_16);

        // Чтение в байтах (если нужен не текст)
        byte[] bytes = Files.readAllBytes(path);

        // Запись из строки
        Path out = Path.of("output.txt");
        Files.writeString(out, "Новый файл", StandardCharsets.UTF_8);

        // Чтение всех строк как список
        var lines = Files.readAllLines(path);
        System.out.println(lines);
        // [Привет, мир!, Это вторая строка., Третья строка.]
    }
}

writeString() принимает опциональный параметр StandardOpenOption для контроля поведения записи:

  • StandardOpenOption.CREATE — создать файл, если не существует (по умолчанию)
  • StandardOpenOption.CREATE_NEW — создать файл, ошибка если существует
  • StandardOpenOption.APPEND — добавить в конец файла
  • StandardOpenOption.TRUNCATE_EXISTING — обрезать файл при открытии (по умолчанию)

Практический пример: простой логгер файлов:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;

public class SimpleLogger {
    private final Path logFile;

    public SimpleLogger(String filename) {
        this.logFile = Path.of(filename);
    }

    public void log(String message) throws Exception {
        String entry = LocalDateTime.now() + " | " + message + "\n";
        Files.writeString(logFile, entry, StandardOpenOption.APPEND);
    }

    public String readLogs() throws Exception {
        return Files.readString(logFile);
    }

    public static void main(String[] args) throws Exception {
        SimpleLogger logger = new SimpleLogger("app.log");
        logger.log("Приложение запущено");
        logger.log("Пользователь вошёл в систему");
        logger.log("Данные обновлены");

        System.out.println(logger.readLogs());
    }
}

Важно: readString() и writeString() загружают весь файл в память. Для очень больших файлов (гигабайты) используйте Files.lines() (чтение построчно) или BufferedReader/BufferedWriter (построчная обработка).

Полный пример: всё вместе

Давайте соберём всё вместе в один практический пример. Мы создадим утилиту для форматирования и анализа текста, используя все рассмотренные функции:

import java.text.NumberFormat;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.List;

public class TextToolkit {
    record WordStats(String word, int count, double frequency) {}

    static String formatJson(String name, int age, String city) {
        return """
               {
                   "name": "%s",
                   "age": %d,
                   "city": "%s"
               }
               """.formatted(name, age, city);
    }

    static String generateReport(String title, List<String> items) {
        String header = "=".repeat(40);
        String itemLines = items.stream()
            .map(item -> "  • " + item)
            .collect(Collectors.joining("\n"));

        return """
               %s
               %s
               %s

               Статистика:
               Всего элементов: %s

               %s
               """
               .formatted(header, title, header,
                   NumberFormat.getCompactNumberInstance(
                       new Locale("ru", "RU"),
                       NumberFormat.Style.SHORT
                   ).format(items.size()),
                   itemLines);
    }

    static List<WordStats> analyzeWords(String text) {
        return text.lines()
            .flatMap(line -> line.isBlank() ? java.util.stream.Stream.of()
                                             : java.util.stream.Stream.of(line.strip().split("\\s+")))
            .collect(Collectors.groupingBy(
                word -> word.toLowerCase().strip(),
                Collectors.counting()
            ))
            .entrySet().stream()
            .map(entry -> new WordStats(
                entry.getKey(),
                entry.getValue().intValue(),
                (double) entry.getValue() / text.lines()
                    .flatMap(l -> java.util.stream.Stream.of(l.strip().split("\\s+")))
                    .count() * 100
            ))
            .sorted((a, b) -> Integer.compare(b.count(), a.count()))
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        String userJson = formatJson("Иван", 30, "Москва");
        System.out.println(userJson);

        List<String> features = List.of(
            "Text Blocks для многострочных строк",
            "Новые методы String: isBlank, lines, repeat",
            "Records как альтернатива классов",
            "Helpful NullPointerExceptions",
            "Files.readString() и Files.writeString()"
        );

        String report = generateReport("Функции Java 21", features);
        System.out.println(report);

        String sampleText = """
                            Java это отлично Java это круто
                            Java это просто Java это быстро
                            Python это тоже хорошо
                            """;

        List<WordStats> stats = analyzeWords(sampleText);
        System.out.println("Частота слов:");
        stats.forEach(s ->
            System.out.printf("  %s: %d (%.1f%%)%n",
                s.word(), s.count(), s.frequency()));
    }
}

Этот пример демонстрирует:

  • Text Blocks с .formatted() для шаблонов
  • Метод lines() для разбиения текста
  • repeat() для создания разделителей
  • strip() для обработки пробелов
  • Records для структуры данных
  • Stream API и toList()
  • Компактное форматирование чисел

Сводная таблица новых возможностей

Возможность Версия Java Описание
String.isBlank() Java 11 Проверка на пустоту с учётом пробелов
String.lines() Java 11 Разбиение строки на Stream строк
String.repeat() Java 11 Повторение строки N раз
String.strip() Java 11 Удаление Unicode-пробелов
Files.readString() Java 11 Чтение всего файла в строку
String.indent() Java 12 Добавление/удаление отступов
String.transform() Java 12 Универсальное преобразование строки
CompactNumberFormat Java 12 Компактное форматирование чисел
Helpful NPE Java 14 Информативные сообщения NullPointerException
Records Java 16 Неизменяемые классы данных
Stream.toList() Java 16 Неизменяемый список из Stream
Text Blocks Java 17 Многострочные строковые литералы

Практические советы и паттерны

Вот несколько практических рекомендаций по использованию новых возможностей Java в реальных проектах:

1. Используйте Records для DTO и результатов. Если вам нужно передавать данные между методами — Record идеален. Он короче, чище и безопаснее обычного класса:

record Coordinates(double latitude, double longitude) {}
record SearchResult(String title, String url, double relevance) {}
record ApiResponse(int statusCode, String body, Map<String, String> headers) {}

2. Заменяйте конкатенацию на Text Blocks. Если строка содержит переносы строк и форматирование — используйте Text Blocks:

// Вместо:
String error = "Ошибка:\\n" +
               "  Файл: " + filename + "\\n" +
               "  Строка: " + lineNum;

// Используйте:
String error = """
               Ошибка:
                 Файл: %s
                 Строка: %d
               """.formatted(filename, lineNum);

3. Заменяйте trim() на strip(). strip() корректнее работает с Unicode и не имеет проблем с неразрывными пробелами:

// Вместо:
String cleaned = input.trim();

// Используйте:
String cleaned = input.strip();

4. Используйте toList() вместо collect(toList()). Неизменяемые списки безопаснее:

// Вместо:
List<String> result = stream.collect(Collectors.toList());

// Используйте:
List<String> result = stream.toList();

5. Используйте isBlank() вместо trim().isEmpty(). Короче и чище:

// Вместо:
if (input == null || input.trim().isEmpty()) { ... }

// Используйте:
if (input == null || input.isBlank()) { ... }

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

Итоги урока

  • Text Blocks (Java 15+) позволяют записывать многострочные строки с тройными кавычками """, автоматически обрабатывая отступы и переносы
  • Управляющие последовательности в Text Blocks: \ подавляет перенос строки, \s сохраняет пробелы
  • isBlank() проверяет строку на пустоту с учётом пробелов, lines() разбивает на Stream, repeat() повторяет строку
  • strip(), stripLeading(), stripTrailing() корректнее, чем trim() — работают с Unicode-пробелами
  • String.compare() (Java 21) — null-безопасное сравнение вместо compareTo()
  • indent() добавляет/убирает отступы, transform() применяет произвольную функцию к строке
  • Stream.toList() (Java 16+) создаёт неизменяемый список — короче и безопаснее collect(toList())
  • Helpful NullPointerExceptions (Java 14+) показывают имя переменной и операцию в сообщении об ошибке
  • Records (Java 16+) — компактная альтернатива неизменяемым классам с автоматическим equals/hashCode/toString
  • CompactNumberFormat (Java 12+) форматирует числа компактно: 1500 → «1.5 тыс.»
  • Files.readString() и Files.writeString() (Java 11+) упрощают работу с текстовыми файлами

Тест: Text Blocks и полезные мелочи

6 вопросов

Text Blocks

Premium