Text blocks e as novas APIs String (Java 15+)

1. Visão geral

Antes do Java 15, escrever um trecho de JSON, SQL ou HTML multi-linhas em código Java era bem trabalhoso. Você tinha que concatenar strings com +, adicionar \n e fazer escape de cada aspas dupla ". Text blocks (oficial a partir do Java 15, JEP 378) resolvem isso. Acompanham alguns métodos novos na classe String.

Depois de ler este artigo, você saberá escrever text blocks com indentação correta e usar os escapes \ e \s. Você também aprenderá quando usar formatted(), stripIndent(), translateEscapes() junto com algumas APIs adicionais até Java 21.

2. Preparação

Todos os exemplos executam no Java 21. Os testes usam JUnit 5 e AssertJ:

<properties>
    <maven.compiler.release>21</maven.compiler.release>
</properties>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.25.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Se você usar Spring Boot 3.x, o spring-boot-starter-test já inclui ambas as bibliotecas.

3. Text block básico

3.1. Sintaxe

Um text block abre com três aspas duplas """ seguido de uma quebra de linha imediata. Ele termina com """:

public class TextBlockDemo {
    public static void main(String[] args) {
        String json = """
                {
                  "id": 42,
                  "customer": "Lan",
                  "status": "PAID"
                }
                """;
        System.out.print(json);
    }
}

Resultado:

{
  "id": 42,
  "customer": "Lan",
  "status": "PAID"
}

Este trecho de código tem alguns pontos dignos de nota:

  • As aspas " dentro não precisam ser escapadas.
  • O espaçamento comum no início de cada linha é removido. Isso é chamado de incidental whitespace. A indentação relativa, como os 2 espaços antes de "id", é preservada.
  • Como o """ de fechamento está em sua própria linha, a string termina com \n.

Um text block é uma String normal. É processado completamente em tempo de compilação, portanto não há custo adicional em tempo de execução.

Nota: Você não pode colocar conteúdo imediatamente após """ de abertura. A linha String s = """Hello"""; resultará em erro de compilação: illegal text block open delimiter sequence.

3.2. A posição do """ de fechamento determina a indentação

O compilador encontra a linha com o menor espaçamento inicial, incluindo a linha que contém o """ de fechamento. Depois, ele remove essa quantidade exata de espaçamento de todas as linhas. Portanto, você pode adicionar intencionalmente indentação puxando o """ de fechamento para a esquerda:

String html = """
            <ul>
              <li>Order #1</li>
            </ul>
        """;
System.out.print(html);
    <ul>
      <li>Order #1</li>
    </ul>

O """ de fechamento tem 4 colunas de menos indentação que o conteúdo, portanto cada linha retém 4 espaços no início.

Por outro lado, se você não quer o caractere \n no final da string, coloque """ imediatamente após o conteúdo:

String greeting = """
        Olá Lan""";
System.out.println("[" + greeting + "]");
[Olá Lan]

3.3. Espaço no final das linhas e caracteres de nova linha

Existem duas outras regras que você precisa conhecer:

  1. Espaçamento no final de cada linha é sempre removido.
  2. Todos os tipos de terminações de linha (\r\n, \r) são normalizados para \n. Isso é verdadeiro mesmo se o arquivo-fonte for salvo com terminações de linha do Windows.

A regra 2 garante resultados consistentes entre sistemas operacionais. Mas se o seu protocolo exigir \r\n, como cabeçalhos HTTP ou arquivos CSV conforme RFC 4180, você deve substituir manualmente o caractere de nova linha.

4. Duas novas sequências de escape: \<quebra de linha> e \s

4.1. Unir linhas com \

Uma instrução SQL longa geralmente é escrita em várias linhas para legibilidade. Mas às vezes você precisa que ela esteja em uma única linha, como ao fazer log. Um \ no final da linha suprime o caractere de nova linha:

String sql = """
        SELECT id, total \
        FROM orders \
        WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'

Os espaços antes do \ são preservados. Isso ocorre porque não estão mais no final da linha: o caractere \ vem depois deles.

4.2. Preservar espaço em branco com \s

\s equivale a um espaço (U+0020). Use-o para preservar espaços no final de linhas que normalmente seriam removidos, como ao criar dados de largura fixa:

String report = """
        Lan    \s
        Minh   \s
        """;
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan     ]
[Minh    ]

Cada linha tem exatamente 8 caracteres. Os quatro espaços antes de \s são preservados porque não estão mais no final da linha.

4.3. Quando o conteúdo contém """

Duas aspas " consecutivas estão bem. Três aspas consecutivas serão interpretadas como o delimitador de fechamento do text block, portanto você precisa fazer escape de pelo menos uma:

String doc = """
        Python docstring usa \""" para abrir e fechar.
        """;

5. Novas APIs String que acompanham

Java 15 adicionou três métodos a String para suportar text blocks. Estes também são os passos que o compilador executa ao processar text blocks.

5.1. formatted()

formatted(Object... args) funciona como String.format(this, args), mas você pode chamá-lo diretamente na string. Este estilo é particularmente apropriado para text blocks:

record Order(long id, String customer, double total) {}

Order order = new Order(42, "Lan", 199_000);

String message = """
        Pedido #%d
        Cliente: %s
        Total: %,.0f VND
        """.formatted(order.id(), order.customer(), order.total());

System.out.print(message);
Pedido #42
Cliente: Lan
Total: 199.000 VND

O separador de milhar depende do Locale padrão da JVM. Se você precisar de um resultado fixo, use String.format(Locale.US, ...), pois formatted() não aceita um parâmetro Locale.

5.2. stripIndent()

Este método aplica o mesmo algoritmo de remoção de incidental whitespace dos text blocks em qualquer string. É útil quando strings multi-linhas vêm de um arquivo ou banco de dados em vez de estarem no código-fonte:

String raw = "    SELECT *\n      FROM customers\n    ";
System.out.print(raw.stripIndent());
SELECT *
  FROM customers

A última linha contém apenas espaçamento, portanto é considerada ao determinar a indentação e se torna uma string vazia. Este é exatamente o papel do """ de fechamento em um text block.

5.3. translateEscapes()

Este método converte sequências de escape em sua forma literal para os caracteres reais. Você precisará dele ao ler templates de um arquivo de configuração que contém strings \n ou \t:

String fromConfig = "Olá\\tLan\\nObrigado por encomendar";
System.out.println(fromConfig.translateEscapes());
Olá	Lan
Obrigado por encomendar

Escapes com sintaxe incorreta, como \q, farão o método lançar IllegalArgumentException. Não o chame em dados inseridos pelo usuário sem capturar exceções. Nota adicional: este método não processa \u porque escapes Unicode já foram traduzidos pelo compilador.

6. Outros métodos úteis (Java 11 – 21)

Muitos pequenos métodos foram adicionados ao longo das versões. Eles são frequentemente usados junto com text blocks:

String text = """
        Lan
           \s
        Minh
        """;

// Java 11
System.out.println("  ".isBlank());                      // true
System.out.println(text.lines().filter(l -> !l.isBlank()).count()); // 2
System.out.println("\u2003Lan\u2003".strip());           // "Lan" (strip entende Unicode, trim não)
System.out.println("-".repeat(10));                      // ----------

// Java 12
System.out.print("Lan\nMinh".indent(2));                 // adiciona 2 espaços em cada linha, termina com \n
int length = "Lan".transform(String::length);            // 3

// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8, procura apenas em [6, 13)
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]

Alguns detalhes precisam ser esclarecidos:

  • indent(n) adiciona n espaços no início de cada linha se n > 0, ou remove até n espaços se n < 0. Sempre normaliza terminações de linha e adiciona \n no final.
  • transform(Function) permite encadear etapas de transformação de string de forma natural, por exemplo template.transform(this::render).transform(String::strip).
  • A partir do Java 21, StringBuilder também tem repeat(...): new StringBuilder().repeat("=", 5) produz =====.

7. Usar text blocks no Spring Boot

Em aplicações Spring Boot 3.x, text blocks se adequam perfeitamente a @Query do Spring Data JPA. Você mantém a consulta multi-linhas sem precisar concatenar strings:

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("""
            SELECT o FROM Order o
            WHERE o.customer.email = :email
              AND o.status = 'PAID'
            ORDER BY o.createdAt DESC
            """)
    List<Order> findPaidOrders(@Param("email") String email);
}

Text blocks são constantes de tempo de compilação, portanto podem ser usados como valores de anotação. O parâmetro ainda é vinculado via :email, seguro contra injeção SQL.

8. Escrever testes para o comportamento do text block

As regras sobre indentação e caracteres de nova linha podem ser confusas. Portanto, você deve escrever testes para fixar o comportamento esperado:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class TextBlockTest {

    @Test
    void givenClosingDelimiterOnOwnLine_thenEndsWithNewline() {
        String json = """
                {
                  "id": 42
                }
                """;

        assertThat(json).isEqualTo("{\n  \"id\": 42\n}\n");
    }

    @Test
    void givenBackslashAtLineEnd_thenLinesAreJoined() {
        String sql = """
                SELECT id \
                FROM orders""";

        assertThat(sql).isEqualTo("SELECT id FROM orders");
    }

    @Test
    void givenSpaceEscape_thenTrailingSpacesArePreserved() {
        String padded = """
                Lan  \s""";

        assertThat(padded).hasSize(6).endsWith("   ");
    }

    @Test
    void givenTemplate_whenFormatted_thenPlaceholdersAreReplaced() {
        String message = """
                Olá %s, pedido #%d foi pago.""".formatted("Lan", 42);

        assertThat(message).isEqualTo("Olá Lan, pedido #42 foi pago.");
    }
}

Todos os quatro testes passam. O primeiro teste é o mais importante: ele o lembra de que o """ de fechamento em sua própria linha criará \n no final da string.

9. Erros comuns e quando não usar

Misturar tabs e espaços. Ao calcular incidental whitespace, o compilador trata uma tabulação como um caractere de espaço, assim como um espaço. Se as linhas forem indentadas com dois tipos diferentes, o resultado será desalinhado, mesmo que pareça alinhado no IDE. Configure seu editor para usar apenas espaços.

Esquecer \n no final. Ao comparar strings ou concatenar text blocks com outras strings, o \n extra frequentemente causa erros do tipo “parece igual mas equals retorna false”. Se você não precisar, coloque """ imediatamente após o conteúdo ou chame strip().

Usar formatted() para concatenar SQL com dados do usuário. Text blocks apenas tornam o código mais legível, não o tornam mais seguro.

// ERRADO: vulnerabilidade de SQL injection
String sql = """
        SELECT * FROM customers WHERE email = '%s'
        """.formatted(email);

Use PreparedStatement, JdbcTemplate com parâmetros ?, ou @Query com named parameters.

Nota: Para JSON, text blocks são apropriados para dados de teste ou payloads fixos. Se o payload depender de dados dinâmicos, use Jackson para serializar o objeto. Concatenar strings manualmente não escapará caracteres especiais como " ou \ no nome do cliente.

Strings curtas de uma linha. "Olá" ainda é melhor do que um text block de três linhas. Use text blocks apenas quando o conteúdo tiver várias linhas ou contiver muitas aspas duplas.

Templates muito grandes. Um email HTML de várias centenas de linhas dentro de uma classe Java torna o código difícil de manter. Nesse caso, você deve movê-lo para um arquivo de template, por exemplo usando Thymeleaf ou recursos em src/main/resources.

10. Conclusão

Text blocks ajudam você a escrever JSON, SQL, HTML em Java de forma concisa e legível como no seu formato original. Você apenas precisa lembrar algumas regras:

  • """ de abertura deve ser seguido por uma quebra de linha imediata.
  • A posição do """ de fechamento determina a indentação e se a string termina com \n.
  • Espaço no final das linhas é removido. As terminações de linha são sempre normalizadas para \n.
  • Use \ para juntar linhas e \s para preservar espaços.

Quanto à API, formatted() é perfeito para preencher valores em text blocks. stripIndent() e translateEscapes() permitem aplicar essa mesma lógica a strings lidas de fora. Métodos como lines(), strip(), indent(), transform() e splitWithDelimiters() do Java 21 ajudam você a processar strings multi-linhas sem bibliotecas adicionais. Por fim, escreva testes para fixar o comportamento desejado, e não use text blocks para concatenar SQL manualmente com dados do usuário.

javatext-blockstringjava-21