Text blocks y nueva API String (Java 15+)
1. Descripción general
Antes de Java 15, escribir un fragmento de JSON, SQL o HTML multilínea en código Java era bastante tedioso. Tenías que concatenar strings con +, agregar \n y escapear cada comilla ". Text blocks (oficial desde Java 15, JEP 378) resuelve esto. Viene acompañado de varios métodos nuevos en String.
Después de leer este artículo, podrás escribir text blocks con la indentación correcta y usar los escapes \ y \s. También sabrás cuándo usar formatted(), stripIndent(), translateEscapes() y algunas APIs adicionales hasta Java 21.
2. Preparación
Todos los ejemplos se ejecutan en Java 21. La parte de pruebas utiliza JUnit 5 y 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>
Si usas Spring Boot 3.x, spring-boot-starter-test ya incluye ambas librerías.
3. Text blocks básicos
3.1. Sintaxis
Un text block se abre con tres comillas dobles """ seguido de una nueva línea. Se cierra con """:
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 código tiene algunos puntos a destacar:
- Las comillas
"dentro no necesitan escape. - El espaciado común al inicio de cada línea se elimina. Esto se llama incidental whitespace. El espaciado relativo, como los 2 espacios antes de
"id", se preserva. - Debido a que las comillas
"""de cierre están en una línea propia, la cadena termina con\n.
Un text block es un String normal. Se procesa completamente en tiempo de compilación, por lo que no tiene costo adicional en tiempo de ejecución.
Nota: No puedes escribir contenido inmediatamente después del
"""de apertura. La líneaString s = """Hello""";generará un error de compilación: illegal text block open delimiter sequence.
3.2. La posición de las comillas """ de cierre determina la indentación
El compilador encuentra la línea con el menor espaciado inicial, incluyendo la línea con """ de cierre. Luego elimina exactamente ese número de espacios de todas las líneas. Por lo tanto, puedes añadir indentación deliberadamente moviendo las comillas """ de cierre hacia la izquierda:
String html = """
<ul>
<li>Order #1</li>
</ul>
""";
System.out.print(html);
<ul>
<li>Order #1</li>
</ul>
Las comillas """ de cierre tienen 4 columnas menos de indentación que el contenido, por lo que cada línea retiene 4 espacios al inicio.
Si no quieres el carácter \n al final de la cadena, coloca """ inmediatamente después del contenido:
String greeting = """
Xin chào Lan""";
System.out.println("[" + greeting + "]");
[Xin chào Lan]
3.3. Espaciado al final de línea y caracteres de salto de línea
Hay dos reglas más que debes conocer:
- El espaciado al final de cada línea siempre se elimina.
- Todos los tipos de salto de línea (
\r\n,\r) se normalizan a\n. Esto es cierto incluso si el archivo fuente se guardó con terminaciones de línea de Windows.
La regla 2 ayuda a que los resultados sean consistentes entre sistemas operativos. Pero si tu protocolo requiere \r\n, como encabezados HTTP o archivos CSV según RFC 4180, debes reemplazar el salto de línea tú mismo.
4. Dos nuevos escape sequences: \<salto de línea> y \s
4.1. Unir líneas con \
Una sentencia SQL larga suele escribirse en múltiples líneas para legibilidad. Pero a veces necesitas que esté en una sola línea, por ejemplo al registrar. Una barra invertida \ al final de la línea suprime el salto de línea:
String sql = """
SELECT id, total \
FROM orders \
WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'
El espacio antes de \ se preserva. La razón es que ya no está al final de la línea: el carácter \ viene después.
4.2. Preservar espaciado con \s
\s equivale a un espacio (U+0020). Lo usas para preservar espacios al final de la línea que normalmente se eliminarían, por ejemplo al crear datos de ancho fijo:
String report = """
Lan \s
Minh \s
""";
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan ]
[Minh ]
Cada línea mide exactamente 8 caracteres. Los cuatro espacios antes de \s se preservan porque ya no están al final de la línea.
4.3. Cuando el contenido tiene """
Dos comillas " seguidas está bien. Tres seguidas se interpretarán como el delimitador de cierre del text block, así que necesitas escapar al menos una:
String doc = """
Python docstring dùng \""" để mở và đóng.
""";
5. Nueva API String que acompaña los text blocks
Java 15 agregó tres métodos a String para soportar text blocks. Estos son también los pasos que el compilador realiza al procesar text blocks.
5.1. formatted()
formatted(Object... args) funciona como String.format(this, args), pero puedes llamarlo directamente en la cadena. Esta sintaxis es especialmente adecuada para text blocks:
record Order(long id, String customer, double total) {}
Order order = new Order(42, "Lan", 199_000);
String message = """
Đơn hàng #%d
Khách hàng: %s
Tổng tiền: %,.0f VND
""".formatted(order.id(), order.customer(), order.total());
System.out.print(message);
Đơn hàng #42
Khách hàng: Lan
Tổng tiền: 199,000 VND
El separador de miles depende de la Locale predeterminada de la JVM. Si necesitas un resultado fijo, usa String.format(Locale.US, ...), porque formatted() no acepta un parámetro Locale.
5.2. stripIndent()
Este método aplica exactamente el algoritmo de eliminación de incidental whitespace de los text blocks a cualquier cadena. Es útil cuando una cadena multilínea proviene de un archivo o base de datos en lugar de estar en el código fuente:
String raw = " SELECT *\n FROM customers\n ";
System.out.print(raw.stripIndent());
SELECT *
FROM customers
La última línea solo contiene espaciado, por lo que se cuenta en la determinación de indentación y luego se convierte en una cadena vacía. Este es exactamente el papel de las comillas """ de cierre en un text block.
5.3. translateEscapes()
Este método convierte escape sequences en forma de texto en caracteres reales. Lo necesitarás cuando leas plantillas de archivos de configuración que contengan cadenas como \n o \t:
String fromConfig = "Xin chào\\tLan\\nCảm ơn bạn đã đặt hàng";
System.out.println(fromConfig.translateEscapes());
Xin chào Lan
Cảm ơn bạn đã đặt hàng
Los escapes mal formados, como \q, harán que el método lance IllegalArgumentException. No llames a este método en datos ingresados por el usuario sin capturar la excepción. Nota adicional: este método no maneja \u porque los escapes unicode ya fueron traducidos por el compilador.
6. Otros métodos útiles (Java 11 – 21)
Se han agregado muchos métodos pequeños a lo largo de las versiones. Suelen usarse junto con 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 entiende Unicode, trim no)
System.out.println("-".repeat(10)); // ----------
// Java 12
System.out.print("Lan\nMinh".indent(2)); // agrega 2 espacios cada línea, termina con \n
int length = "Lan".transform(String::length); // 3
// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8, busca solo en [6, 13)
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]
Algunos detalles a aclarar:
indent(n)agrega n espacios al inicio de cada línea sin > 0, o elimina hasta n espacios en blanco sin < 0. Siempre normaliza saltos de línea y agrega\nal final.transform(Function)te permite encadenar pasos de transformación de cadenas de forma natural, por ejemplotemplate.transform(this::render).transform(String::strip).- A partir de Java 21,
StringBuildertambién tienerepeat(...):new StringBuilder().repeat("=", 5)produce=====.
7. Usar text blocks en Spring Boot
En aplicaciones Spring Boot 3.x, los text blocks encajan perfectamente con @Query de Spring Data JPA. Puedes mantener consultas multilínea sin necesidad de concatenar cadenas:
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);
}
Un text block es una constante en tiempo de compilación, por lo que puede usarse como valor de una anotación. Los parámetros aún se vinculan a través de :email, lo que es seguro contra la inyección SQL.
8. Escribir pruebas para el comportamiento de text blocks
Las reglas sobre indentación y caracteres de salto de línea son fáciles de confundir. Por lo tanto, debes escribir pruebas para confirmar el comportamiento que esperas:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TextBlockTest {
@Test
void dadoDelimitadorCierreEnLineaSeparada_entoncesTerminaConNewline() {
String json = """
{
"id": 42
}
""";
assertThat(json).isEqualTo("{\n \"id\": 42\n}\n");
}
@Test
void dadoBarraReverseAlFinDeLinea_entoncesLasLineasSeunen() {
String sql = """
SELECT id \
FROM orders""";
assertThat(sql).isEqualTo("SELECT id FROM orders");
}
@Test
void dadoEscapeDeEspacio_entoncesEspaciosFinalesSePreservan() {
String padded = """
Lan \s""";
assertThat(padded).hasSize(6).endsWith(" ");
}
@Test
void dadoTemplate_cuandoSeFormatean_entoncesPlaceholdersSeReemplazan() {
String message = """
Xin chào %s, đơn #%d đã thanh toán.""".formatted("Lan", 42);
assertThat(message).isEqualTo("Xin chào Lan, đơn #42 đã thanh toán.");
}
}
Las cuatro pruebas pasan. La primera es la más importante de recordar: te recuerda que las comillas """ de cierre en una línea propia crearán un \n al final de la cadena.
9. Errores comunes y cuándo no usar text blocks
Mezclar tabulaciones y espacios. Al calcular incidental whitespace, el compilador trata una tabulación como un carácter de espaciado, igual que un espacio. Si las líneas tienen indentación con dos tipos diferentes, el resultado será incorrecto aunque en el IDE se vea alineado. Configura tu editor para usar solo espacios.
Olvidar \n al final. Al comparar cadenas o concatenar un text block con otra cadena, el \n extra suele causar errores del tipo “se ve igual pero equals devuelve false”. Si no lo necesitas, coloca """ inmediatamente después del contenido o llama a strip().
Usar formatted() para concatenar SQL con datos de usuario. Los text blocks solo hacen que el código sea más legible, no más seguro.
// INCORRECTO: vulnerabilidad de SQL injection
String sql = """
SELECT * FROM customers WHERE email = '%s'
""".formatted(email);
Usa PreparedStatement, JdbcTemplate con parámetros ?, o @Query con parámetros nombrados.
Nota: Con JSON, los text blocks son adecuados para datos de prueba o payloads fijos. Si el payload depende de datos dinámicos, usa Jackson para serializar el objeto. Concatenar manualmente no escapará caracteres especiales como
\"o\\en nombres de clientes.
Cadenas cortas de una línea. "Lan" sigue siendo mejor que un text block de tres líneas. Solo usa text blocks cuando el contenido realmente tiene múltiples líneas o contiene muchas comillas ".
Plantillas demasiado grandes. Un email HTML de cientos de líneas en una clase Java hace que el código sea difícil de mantener. En ese caso, debes trasladarlo a un archivo de plantilla, por ejemplo usando Thymeleaf o un recurso en src/main/resources.
10. Conclusión
Los text blocks te ayudan a escribir JSON, SQL, HTML en Java de forma concisa y legible como en su forma original. Solo necesitas recordar algunas reglas:
- Las comillas
"""de apertura deben ser seguidas inmediatamente por un salto de línea. - La posición de las comillas
"""de cierre determina la indentación y si la cadena tiene\nal final. - El espaciado al final de la línea se elimina. Los saltos de línea siempre se normalizan a
\n. - Usa
\para unir líneas y\spara preservar espacios en blanco.
En cuanto a la API, formatted() es perfecta para rellenar valores en un text block. stripIndent() y translateEscapes() te permiten aplicar la misma lógica a cadenas leídas de fuentes externas. Métodos como lines(), strip(), indent(), transform() y splitWithDelimiters() de Java 21 te ayudan a procesar cadenas multilínea sin necesidad de librerías adicionales. Finalmente, escribe pruebas para confirmar el comportamiento deseado, y no uses text blocks para concatenar manualmente SQL con datos de usuario.