Блок текста в Java: как работают текстовые блоки «»»

Блок текста в Java: как работают текстовые блоки """ Полезное

Блок текста (text block) в Java — это многострочный строковый литерал в тройных кавычках """. Компилятор превращает его в обычный объект String: приводит переводы строк к \n, убирает общий отступ, который нужен только для красоты кода, и затем обрабатывает escape-последовательности. Текстовые блоки стали стандартной частью языка в Java 15 (JEP 378), в Java 13 и 14 они были preview-функцией.

Словом «блок текста» называют и другие вещи: в HTML это блочные элементы вроде <p> и <div>, в интернет-рекламе — текстовое объявление без картинок. Здесь речь только о Java: синтаксис, правила отступов, экранирование, formatted() и чем блок отличается от обычной строки. Все примеры проверены на OpenJDK 25.0.4 (Java 25 LTS).

Три термина, которые легко спутать

  • Строковый литерал — запись значения в исходном коде: "abc" или блок в """.
  • Строка — объект String во время работы программы. И обычный литерал, и текстовый блок дают один и тот же тип, разница есть только в исходнике.
  • Случайный и существенный отступ. Случайный (incidental) — пробелы, которые появились из-за вложенности кода, компилятор их убирает. Существенный (essential) — пробелы, которые вы хотите видеть в строке, они остаются.

Минимальный пример

public class Main {
    public static void main(String[] args) {
        String json = """
            {
              "name": "Otus",
              "year": 2026
            }
            """;
        System.out.print(json);
        System.out.println("строк: " + json.lines().count());
        System.out.println("в конце \\n: " + json.endsWith("\n"));
    }
}

Запуск без отдельной компиляции: java Main.java. Вывод:

{
  "name": "Otus",
  "year": 2026
}
строк: 4
в конце \n: true

Что здесь видно. Кавычки внутри JSON не нужно экранировать, как в "{\"name\": \"Otus\"}". Двенадцать пробелов слева, которые стоят в коде, в строку не попали, а два пробела перед "name" сохранились — это существенный отступ относительно самой левой строки. Строка заканчивается переводом строки, потому что закрывающие """ стоят на отдельной строке.

Правила синтаксиса

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

public class Bad {
    public static void main(String[] args) {
        String s = """Привет""";
        System.out.println(s);
    }
}

Результат javac Bad.java:

Bad.java:3: error: illegal text block open delimiter sequence, missing line terminator
        String s = """Привет""";
                      ^
Bad.java:3: error: illegal text block open delimiter sequence, missing line terminator
        String s = """Привет""";
                               ^
2 errors

Ошибка выдается дважды: вторые """ компилятор тоже читает как открывающие.

Исправление — перенести содержимое на следующую строку. Для короткого однострочного текста блок не нужен, обычный литерал "Привет" проще.

Закрывающие """ можно ставить где угодно: на отдельной строке или сразу после последнего символа. От этого зависят две вещи — будет ли в конце \n и какой отступ сохранится.

Как компилятор обрабатывает блок

Порядок фиксирован, и он объясняет почти все сюрпризы:

  1. Переводы строк. Окончания \r\n (Windows) и \r приводятся к \n. Строка получается одинаковой независимо от того, в какой ОС сохранен исходник.
  2. Случайный отступ. Компилятор находит минимальный отступ среди непустых строк содержимого, а также строки с закрывающими """, если они стоят на отдельной строке. Этот отступ срезается у всех строк. Пробелы в конце каждой строки тоже удаляются.
  3. Escape-последовательности. Только после этого обрабатываются \n, \t, \", \s и остальные. Поэтому \s в конце строки переживает шаг 2: на момент удаления хвостовых пробелов это еще не пробел, а два символа.

Где ставить закрывающие кавычки

Позиция закрывающих """ — главный рычаг управления отступом. Три варианта в одной программе:

public class Indent {
    static void show(String title, String s) {
        System.out.println(title);
        s.lines().forEach(line -> System.out.println("|" + line + "|"));
    }

    public static void main(String[] args) {
        String a = """
            SELECT id, name
            FROM users
            """;
        String b = """
            SELECT id, name
            FROM users
        """;
        String c = """
            SELECT id, name
            FROM users""";
        show("A: закрывающие кавычки под текстом", a);
        show("B: закрывающие кавычки левее на 4", b);
        show("C: кавычки в конце последней строки", c);
        System.out.println("C заканчивается переводом строки: " + c.endsWith("\n"));
    }
}
A: закрывающие кавычки под текстом
|SELECT id, name|
|FROM users|
B: закрывающие кавычки левее на 4
|    SELECT id, name|
|    FROM users|
C: кавычки в конце последней строки
|SELECT id, name|
|FROM users|
C заканчивается переводом строки: false
Где стоят """ Отступ в строке \n в конце
На своей строке, ровно под текстом Нет Есть
На своей строке, левее текста Сохраняется разница в пробелах Есть
Сразу после последнего символа Нет Нет

Табы и пробелы

Табуляция и пробел для компилятора — по одному символу отступа, ширину таба в редакторе он не учитывает. Если одна строка отбита табом, а другая пробелами, результат будет неожиданным. Флаг javac -Xlint:text-blocks предупреждает об этом:

warning: [text-blocks] inconsistent white space indentation

Практическое правило: в текстовых блоках использовать только пробелы и включить в IDE показ невидимых символов.

Escape-последовательности в текстовом блоке

Блок текста — не «сырая» строка: обратный слэш в нем работает так же, как в обычном литерале. Кроме привычных \n, \t, \\, \" есть две последовательности, которые появились вместе с текстовыми блоками (Java 14 preview, стандарт с Java 15):

Последовательность Что делает Где работает
\<перевод строки> Склеивает строку исходника со следующей, \n не добавляется Только в текстовом блоке
\s Один пробел (U+0020), не удаляется как хвостовой В блоке и в обычной строке
\""" Тройные кавычки внутри блока В блоке

Пример со всеми тремя:

public class Escapes {
    public static void main(String[] args) {
        String quote = """
            Тройные кавычки внутри: \"""
            Одиночные можно без экранирования: "text"
            """;
        String oneLine = """
            Очень длинная строка, \
            которую удобно переносить в коде
            """;
        String padded = """
            red  \s
            green\s
            """;
        System.out.print(quote);
        System.out.print(oneLine);
        padded.lines().forEach(l -> System.out.println("[" + l + "] длина " + l.length()));
    }
}
Тройные кавычки внутри: """
Одиночные можно без экранирования: "text"
Очень длинная строка, которую удобно переносить в коде
[red   ] длина 6
[green ] длина 6

Пробелы перед \s в строке red \s сохранились, потому что после них стоит не пробел, а последовательность. Так выравнивают колонки фиксированной ширины.

Ошибка: тройные кавычки без экранирования

public class Bad2 {
    public static void main(String[] args) {
        String s = """
            Он сказал: """Привет"""
            """;
        System.out.println(s);
    }
}
Bad2.java:4: error: ';' expected
            Он сказал: """Привет"""
                          ^
Bad2.java:4: error: ';' expected
            Он сказал: """Привет"""
                                ^
2 errors

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

Регулярные выражения

Раз слэш не «сырой», в регулярном выражении его все так же надо удваивать:

public class Regex {
    public static void main(String[] args) {
        String pattern = """
            \\d{3}-\\d{2}""";
        System.out.println(pattern);
        System.out.println("123-45".matches(pattern));
    }
}
\d{3}-\d{2}
true

Выигрыш от блока здесь только в том, что не нужно экранировать кавычки. Для коротких регулярок обычная строка не хуже.

Подстановка значений: formatted() и склейка

Интерполяции вида ${name} в Java нет. Строковые шаблоны (String Templates) были preview в Java 21 и 22 и затем убраны из языка, в Java 25 их нет. Значения подставляют методом formatted() — это String.format, вызванный у самой строки:

public class Fmt {
    public static void main(String[] args) {
        String template = """
            Курс: %s
            Модулей: %d
            """;
        System.out.print(template.formatted("Java", 5));

        String raw = "    a\n      b\n    c";
        System.out.println(raw.stripIndent());

        String escaped = "Колонка1\\tКолонка2";
        System.out.println(escaped);
        System.out.println(escaped.translateEscapes());
    }
}
Курс: Java
Модулей: 5
a
  b
c
Колонка1\tКолонка2
Колонка1    Колонка2

Вместе с текстовыми блоками в Java 15 появились три метода String. formatted(Object... args) — форматирование по шаблону. stripIndent() — то же удаление случайного отступа, что делает компилятор, но для строки, полученной во время работы (например, прочитанной из файла). translateEscapes() — превращает текст \t из двух символов в настоящую табуляцию.

Ловушка при склейке

Хвостовые пробелы удаляются и у последней строки, если закрывающие """ стоят сразу за ней. Код ниже выглядит правильно, но пробел после запятой теряется:

String name = "Otus";
String mixed = """
    Привет, """ + name + "!";
System.out.println(mixed); // Привет,Otus!

Исправление — \s перед кавычками или шаблон с formatted():

String fixed1 = """
    Привет,\s""" + name + "!";
String fixed2 = """
    Привет, %s!""".formatted(name);
System.out.println(fixed1); // Привет, Otus!
System.out.println(fixed2); // Привет, Otus!

SQL в текстовом блоке и инъекции

Текстовые блоки часто берут для SQL, и formatted() соблазняет подставить значение прямо в запрос. Так делать нельзя: при вводе x' OR '1'='1 получится WHERE login = 'x' OR '1'='1', то есть условие, истинное для всех строк. Текстовый блок хранит только текст запроса, а значения передаются через параметры ? и PreparedStatement.setString():

String sql = """
    SELECT id, name FROM users
    WHERE login = ?
    """;

То же касается HTML: блок с разметкой удобен для шаблона, но данные пользователя в него вставляют только после экранирования.

Чем блок текста отличается от обычной строки

Свойство Обычный литерал "..." Текстовый блок """..."""
Тип результата String String
Несколько строк Через \n и + Как написано в коде
Кавычки " внутри Нужно \" Можно без экранирования
Отступ кода Попадает в строку, если набран Общий отступ срезается
Хвостовые пробелы Сохраняются Удаляются (кроме \s)
Интернирование Да, как константа Да, как константа
Минимальная версия Любая Java 15 (13-14 — preview)

Проверка на практике:

public class Same {
    public static void main(String[] args) {
        String block = """
            Hello
            World""";
        String classic = "Hello\nWorld";
        System.out.println(block.equals(classic));
        System.out.println(block == classic);
        System.out.println(block.getClass().getName());
    }
}
true
true
java.lang.String

== вернул true только потому, что обе строки — одинаковые константы времени компиляции, и JVM хранит их в одном экземпляре. Для строк, собранных во время работы, это не так, поэтому сравнивают всегда через equals().

Если не получилось

  • illegal text block open delimiter sequence — после открывающих """ есть текст. Перенести его на следующую строку.
  • ';' expected внутри блока — в содержимом встретились """. Экранировать: \""".
  • Лишние пробелы слева — закрывающие """ стоят левее текста или в строках смешаны табы и пробелы. Выровнять кавычки и проверить -Xlint:text-blocks.
  • В конце лишний перевод строки — поставить """ сразу после последнего символа или добавить \ в конце последней строки.
  • Пропал пробел в конце строки — заменить его на \s.
  • Код не компилируется на старом JDK — текстовые блоки требуют Java 15+, проверить java -version и настройку release в Maven или Gradle.

Выводы

  • Блок текста в Java — многострочный литерал в """, результат — обычный String; стандарт с Java 15.
  • Компилятор выполняет три шага по порядку: нормализует переводы строк, срезает общий отступ и хвостовые пробелы, затем обрабатывает escape-последовательности.
  • Отступом и финальным \n управляет позиция закрывающих """.
  • \<перевод строки> склеивает строки, \s сохраняет пробел, \""" вставляет тройные кавычки; обратный слэш не «сырой».
  • Значения подставляются через formatted(), но в SQL — только параметрами PreparedStatement.

Где применяется / связь с практикой

Освойте тему на практике

Текстовые блоки встречаются там, где в Java-коде живут фрагменты другого языка: SQL-запросы в репозиториях, JSON в тестах и моках, HTML-шаблоны писем, конфигурации. Чтобы уверенно работать со строками, форматированием и JDBC, нужна база языка, которую системно дает курс «Java разработчик. Базовый уровень». Попробовать формат обучения можно на открытых уроках Otus.

FAQ

Можно ли использовать текстовый блок как аргумент метода или в switch?
Да. Это обычная константа типа String, ее можно передать в метод, объявить static final и использовать как метку case.

Работают ли текстовые блоки в Kotlin или Android-проекте?
В Kotlin есть свои «сырые» строки в """ с методом trimIndent(), это другой механизм. В Java-коде под Android текстовые блоки доступны, если сборка настроена на Java 15+ и инструменты сборки поддерживают эту версию языка.

Попадает ли отступ в байт-код?
Нет. Все три шага выполняет компилятор, в class-файл записывается уже готовая строка, поэтому затрат во время работы программы нет.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)