La clase java.lang.String es, probablemente, una de las más utilizadas en Java. Dominar los strings en Java es crucial, ya que su uso incorrecto genera múltiples problemas, principalmente de rendimiento.

En este artículo, te guiaré a través de la estructura de las cadenas en Java, las particularidades de su uso, las fuentes de problemas comunes y las buenas prácticas string Java para trabajar con ellos.

Estructura interna de los Strings en Java con Compact Strings
La optimización ‘Compact Strings’ de Java 9 redujo a la mitad el uso de memoria para la mayoría de las cadenas.

Los temas que se abordarán son:

  • Estructura de un string
  • Literales de string y el String Pool
  • Comparación de strings
  • Concatenación: el principal cuello de botella
  • Extracción de substrings
  • Métodos modernos para operar con strings

Comencemos con los fundamentos.

Estructura Interna de un String en Java

Antes de Java 9, la clase String almacenaba los datos en un array char[], la estructura base para una cadena de caracteres Java. Este enfoque era simple pero ineficiente: cada carácter ocupaba 2 bytes, incluso si se trataba de caracteres latinos básicos. En la mayoría de las aplicaciones, los strings contienen precisamente este tipo de caracteres, lo que resultaba en un desperdicio de la mitad de la memoria.

En Java 9, se introdujo una optimización denominada Compact Strings (según el JEP 254). Ahora, la estructura interna es la siguiente:

public final class String {
    private final byte[] value;
    private final byte coder;
    private int hash;
    // ... métodos
}

Ahora se utiliza un byte[]. El campo coder indica la codificación empleada:

  • LATIN1 (0) — si el string contiene únicamente caracteres Latin-1 (códigos 0-255), cada carácter ocupa 1 byte.
  • UTF16 (1) — si existen caracteres fuera del rango de Latin-1, se utiliza UTF-16, y cada carácter ocupa 2 bytes.

La JVM selecciona automáticamente la codificación óptima. Esta optimización es transparente para el programador.

Es relevante mencionar que, a partir de Java 5, se introdujo el soporte para versiones de Unicode superiores a la 2.0. Para representar dichos caracteres, como emojis o escrituras antiguas, no se utiliza un solo char, sino dos (un par sustituto o surrogate pair). Este es un tema complejo y crucial para manejar correctamente caracteres especiales. Si quieres dominarlo, he preparado una guía completa sobre Unicode en Java.

Literales y el String Pool en Java

Un literal de string es una cadena entre comillas dobles, como "abc".

Los text blocks fueron introducidos como preview en Java 13 (JEP 355) y Java 14 (JEP 368), y se convirtieron en una característica estándar a partir de Java 15 (JEP 378). Son literales multilínea muy convenientes para consultas SQL, HTML o JSON. Si antes era necesario escribir:

String json = "{\n" +
              "  \"name\": \"John\",\n" +
              "  \"age\": 30\n" +
              "}";

Ahora es posible hacerlo de esta forma:

String json = """
              {
                "name": "John",
                "age": 30
              }
              """;

La JVM mantiene un pool de strings (o String Pool Java) donde se almacenan todos los literales. Si se encuentran literales idénticos, se utiliza el mismo objeto para ahorrar memoria. Un string puede ser añadido explícitamente al pool mediante String.intern().

Buenas Prácticas: Evita la construcción new String("abc"). El literal "abc" ya es un objeto String en el pool. Crear un nuevo objeto es una copia innecesaria y un desperdicio de memoria. La forma correcta es String miString = "abc";.

Visualización de cómo funciona el String Pool en Java
El String Pool es un mecanismo clave de la JVM para ahorrar memoria reutilizando literales idénticos.

¿Cómo Comparar String en Java Correctamente?

Los strings deben compararse por su valor utilizando el método equals(). La comparación por referencia con == es un error común al comparar string Java.

Un punto que merece mención es la comparación con literales. A menudo se ven construcciones como str.equals("abc"), que pueden lanzar un NullPointerException si str es null.

Buenas Prácticas: Para evitar NullPointerException, invierte el orden de la comparación: "abc".equals(str). Si str es null, la expresión simplemente devolverá false.

Concatenación de Strings: El Gran Cuello de Botella

La forma más eficiente de concatenar strings en Java, especialmente dentro de bucles, es usando la clase StringBuilder. A diferencia del operador +, que crea un nuevo objeto String en cada operación, StringBuilder utiliza un búfer interno mutable que modifica la cadena sin generar sobrecarga de memoria, mejorando drásticamente el rendimiento.

Los strings son inmutables. Por ello, cada vez que se usa el operador + para concatenar, se crea un nuevo objeto String en memoria. Para un programador Java, entender esto es fundamental.

Considera este código que lee un archivo línea por línea:

// INEFICIENTE - NO USAR EN BUCLES
BufferedReader br = new BufferedReader(new FileReader("filename.txt"));
String result = "";
String line;
while ((line = br.readLine()) != null) {
    result += line;
}

En cada iteración, se crea un nuevo String, se copia todo el contenido anterior y se añade la nueva línea. Para un archivo de 1000 líneas de 80 caracteres (80 KB), esto puede generar un tráfico de memoria de casi 80 MB.

Advertencia: Nunca utilices la concatenación de strings con el operador + dentro de un bucle. El impacto en el rendimiento es exponencial y puede ralentizar una aplicación cientos de veces.

La solución correcta es usar StringBuilder:

// EFICIENTE Y CORRECTO
BufferedReader br = new BufferedReader(new FileReader("filename.txt"));
StringBuilder result = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
    result.append(line);
}
String finalResult = result.toString();

StringBuilder es un búfer mutable que modifica su contenido internamente sin crear nuevos objetos en cada paso. Para cualquier programador, dominar StringBuilder en Java es clave para optimizar strings Java.

CaracterísticaStringStringBuilderStringBuffer
MutabilidadInmutableMutableMutable
Seguridad en HilosThread-SafeNo Thread-SafeThread-Safe
RendimientoLento para modificacionesEl más rápidoMás lento (por sincronización)
Caso de Uso IdealDatos fijos, claves de mapasConcatenación en un solo hilo (99% de los casos)Concatenación en entorno multihilo (raro)

El compilador optimiza concatenaciones simples (String s = "a" + "b";) usando StringBuilder automáticamente, pero no puede hacerlo dentro de bucles.

Un Caso Real de Optimización con StringBuilder

Un caso práctico ilustra este problema. Un código que yo escribí leía archivos HTML de gran tamaño (200 KB) y funcionaba con una lentitud extrema. Se identificó el uso de concatenación de strings dentro de un bucle, lo que provocaba un tráfico de memoria de más de 40 MB. Al reescribir el código con StringBuilder, el procesamiento se completó en una fracción de segundo, con una mejora de velocidad de entre 300 y 800 veces.

Gráfico de rendimiento que muestra la superioridad de StringBuilder sobre el operador + para concatenar strings en Java
La diferencia de rendimiento es abrumadora: StringBuilder mantiene un rendimiento constante, mientras que ‘+’ se degrada exponencialmente

Funciones de Cadena en Java Moderno

En versiones modernas de Java, se han añadido numerosas y útiles funciones de cadena en Java:

  • String.join(): Une varios strings con un delimitador.
String resultado = String.join(", ", "Java", "Python", "C++");
// resultado = "Java, Python, C++"

List palabras = Arrays.asList("Hola", "mundo");
String frase = String.join(" ", palabras);
// frase = "Hola mundo"
  • String.format() (disponible desde Java 5): Formatea strings al estilo de printf. Es la respuesta nativa para cualquier necesidad de Java string format.
String nombre = "John";
int edad = 30;
String mensaje = String.format("Hola, %s! Tienes %d años.", nombre, edad);
// mensaje = "Hola, John! Tienes 30 años."

String precio = String.format("Precio: %.2f unidades.", 123.456);
// precio = "Precio: 123.46 unidades."

Especificadores de formato:

  • %s — cadena de texto
  • %d — número entero
  • %f — número de coma flotante
  • %n — salto de línea (dependiente del sistema operativo)
  • repeat() (Java 11): Repite un string un número de veces.
String linea = "-".repeat(50);
// linea = "--------------------------------------------------"

String sangria = "  ".repeat(3);
// sangria = "      " (6 espacios)
  • isBlank() (Java 11): Comprueba si un string está vacío o contiene solo espacios en blanco.
"".isBlank()        // true
"   ".isBlank()     // true
"  \n  ".isBlank()  // true
"abc".isBlank()     // false

El método isEmpty() solo verifica la longitud (length == 0), mientras que isBlank() también tiene en cuenta los caracteres de espacio en blanco.

  • strip(), stripLeading(), stripTrailing() (Java 11): Versiones mejoradas de trim() que manejan correctamente espacios en blanco Unicode.
String texto = "  Hello  ";
texto.trim()           // "Hello"
texto.strip()          // "Hello"
texto.stripLeading()   // "Hello  "
texto.stripTrailing()  // "  Hello"
  • lines() (Java 11): Divide un string en un Stream de líneas.
String multilinea = """
    Primera línea
    Segunda línea
    Tercera línea
    """;

multilinea.lines()
    .forEach(System.out::println);

Es muy conveniente en combinación con text blocks y el Stream API.

  • stripIndent(), translateEscapes() y formatted() (Java 15): Complementan los text blocks. stripIndent() elimina indentación incidental, translateEscapes() procesa secuencias de escape, y formatted() permite formatear directamente como método de instancia.
String json = """
              {
                "name": "%s",
                "age":  %d
              }
              """.stripIndent().translateEscapes().formatted("John", 30);

Extracción de Substrings: El Método substring()

El método substring() extrae una parte de un string.

String str = "abcdefghijklmnopqrstuvwxyz";
str = str.substring(5, 10); // "fghij"

Hasta Java 7 update 6 (julio de 2012), substring() podía causar retención innecesaria de memoria, ya que el nuevo string compartía el array de caracteres interno del string original. Esto impedía que el recolector de basura liberara memoria cuando un substring pequeño se obtenía de un string muy grande. Este comportamiento se corrigió en Java 7 update 6 (como se detalla en este bug report de OpenJDK): desde entonces, substring() crea una copia del array necesario, por lo que su uso es completamente seguro en cuanto a memoria.

Buenas Prácticas con Strings en Java: Resumen Clave

  1. Compact Strings (Java 9+): Ahorran memoria automáticamente.
  2. Concatenación en bucles: Usa siempre StringBuilder.
  3. Comparación: Usa "literal".equals(variable) para evitar NullPointerException.
  4. Métodos modernos: Aprovecha join(), format(), isBlank(), etc., para un código más limpio.
  5. Text Blocks (Java 15+): Simplifican el texto multilínea.
  6. substring(): Es seguro de usar; los problemas de memoria fueron corregidos.

Preguntas Frecuentes sobre Strings en Java

¿Cuándo debería usar StringBuffer en lugar de StringBuilder?

Usa StringBuffer únicamente cuando necesites que un búfer de caracteres sea modificado de forma segura por múltiples hilos simultáneamente. En la práctica, esto es muy raro. Para el 99% de los casos, StringBuilder es la elección correcta por su mayor rendimiento.

¿Es String.format() o String.join() lento? ¿Debería usar StringBuilder en su lugar?

Internamente, tanto format() como join() utilizan mecanismos optimizados similares a StringBuilder, por lo que son eficientes. No es necesario reemplazarlos manualmente para operaciones simples; son preferibles por su legibilidad.

¿Qué es exactamente el “String Pool” y debo preocuparme por él?

El String Pool es un área en la memoria donde Java almacena los literales de string ("abc"). Para el uso diario, no necesitas gestionarlo. Simplemente sigue la buena práctica de no usar new String("literal") y deja que la JVM se encargue de la optimización.

¿Cómo obtengo la longitud de un string en Java?

Para conocer la longitud de una cadena (el número de caracteres), se utiliza el método length(). Por ejemplo: String s = "hola"; int len = s.length(); daría como resultado 4. Esta es una de las operaciones más básicas, y su palabra clave asociada es Java string length. Es importante no confundir el método length() con la propiedad length de los arrays.

Categorizado en:

Java,