Saltar a contenido

UD1 — Sección 5. El tipo String y texto multilínea

UD1 — Fundamentos de Programación · Continuación de la Sección 4 IES Thiar — Pilar de la Horadada (Alicante) · Curso 2026-2027

Aspecto Valor
Resultado de aprendizaje RA1 — Reconoce la estructura de un programa informático, identificando y relacionando los elementos propios del lenguaje de programación utilizado.
Criterios de evaluación cubiertos CE 1.5 — Se ha modificado el código de un programa para crear y utilizar variables (en este caso, de tipo String).
Marco normativo RD 405/2023 · RD 450/2010 · RD 659/2023
Tecnología base Java 25 LTS — String, text blocks (Java 15+, estable), formatted (Java 15+).

Introducción de la sección

En la Sección 4 cubrimos los ocho tipos primitivos de Java: enteros, decimales, booleanos y caracteres. Pero hay un tipo de dato que no es primitivo y que, sin embargo, vas a usar en casi todos los programas: el String, que representa cadenas de texto. Un nombre, una dirección, el cuerpo de un email, el contenido de un fichero de configuración... casi cualquier información humana que un programa necesita manejar es texto. Por eso Java le da un trato especial: tiene su propia sintaxis para literales (las comillas dobles "hola"), su propio operador de concatenación (+) y, desde Java 15, su propia forma de escribir texto multilínea (los text blocks """...""").

El objetivo de esta sección es que entiendas por qué String es un tipo de referencia y no un primitivo, qué es eso del pool de strings y por qué hace que a veces == funcione y a veces no (la fuente número 1 de bugs en principiantes), cómo concatenar textos de forma eficiente, cómo usar String.format y formatted para construir cadenas con variables, y cómo escribir textos multilínea limpios sin escapar cada salto de línea. Cuando acabes, manipularás texto como un profesional y reconocerás al instante la trampa clásica == vs .equals() que aparece en todos los exámenes.


5.1 String: el tipo de referencia que parece primitivo

La sorpresa del principierto

Si te preguntas "¿es String un tipo primitivo?", la respuesta corta es no. Aunque lo uses como si lo fuera (String nombre = "Ana"; se parece mucho a int edad = 18;), internamente String es una clase y sus valores son objetos. Es decir, una variable String no contiene el texto directamente, sino una referencia a un objeto que vive en el montículo (heap) de la JVM y que contiene el texto.

Analogía. Imagina que una variable int edad = 18 es un cajón donde has metido físicamente 18 monedas. La variable contiene el valor. En cambio, una variable String nombre = "Ana" es un cajón donde has metido una dirección ("el texto está en la estantería 3, fila 2, posición 5"). El texto en sí está en otro sitio; en el cajón solo hay un papel con la dirección. Esa es la diferencia entre tipo primitivo (contiene el valor) y tipo de referencia (contiene una dirección a donde está el valor).

Esta diferencia explica muchos comportamientos que de otro modo parecerían mágicos. Por ejemplo, que un String pueda ser null (la referencia no apunta a ningún sitio) mientras que un int no puede ser null (siempre es un número).

Inmutabilidad: el rasgo definitorio

La característica más importante de String es que es inmutable: una vez creado, su valor no puede cambiar. Cuando crees que estás "modificando" un String, en realidad estás creando uno nuevo y la variable pasa a apuntar al nuevo objeto.

1
2
3
4
5
6
7
8
9
String nombre = "Ana";
nombre = nombre + " Pérez";   // NO modifica "Ana"; crea un nuevo String "Ana Pérez"
// y la variable nombre ahora apunta a ese nuevo objeto. "Ana" sigue existiendo
// en memoria (al menos temporalmente) sin nadie que lo referencie.

String saludo = "Hola";
saludo.toUpperCase();         // ¡NO CAMBIA saludo! Devuelve un nuevo String "HOLA"
System.out.println(saludo);   // imprime "Hola", no "HOLA"
saludo = saludo.toUpperCase(); // ahora sí: saludo apunta al nuevo String "HOLA"

Cuidado. Esta es la fuente número 1 de confusión con String. Los métodos como toUpperCase(), trim(), replace() o substring() no modifican el String original: devuelven uno nuevo. Si no asignas el resultado a una variable, el cambio se pierde. El patrón correcto siempre es nombre = nombre.metodo();.

Por qué String es inmutable

La inmutabilidad no es un capricho: tiene tres ventajas técnicas importantes:

  1. Seguridad: como el texto no puede cambiar, es seguro compartirlo entre hilos y usarlo como clave en colecciones (HashMap, HashSet) sin riesgo de corrupción.
  2. Rendimiento (pool): al ser inmutable, la JVM puede reutilizar el mismo objeto String para literales idénticos (el pool de strings, que veremos en 5.2).
  3. Cache de hashCode: el hashCode de un String se calcula una vez y se cachea; si el texto pudiera cambiar, el cache sería inválido.

Declaración y literales

1
2
3
4
5
6
String nombre = "Ana";                 // literal entre comillas dobles
String vacio = "";                     // cadena vacía (longitud 0)
String saludo = "Hola, " + nombre;     // concatenación
String conEscapes = "Línea1\nLínea2";  // con secuencia de escape \n
String conComillas = "Dijo \"hola\"";  // comillas escapadas
String conBarra = "Ruta: C:\\Users";   // barra invertida escapada

Recuerda las secuencias de escape más usadas (ya las viste con char en la Sección 4):

Secuencia Significado
\n Nueva línea
\t Tabulador
\\ Barra invertida literal
\" Comilla doble literal
\' Comilla simple literal
\uXXXX Carácter Unicode (4 hex)

5.2 El pool de strings: una optimización que da sorpresas

Qué es y cómo funciona

Para ahorrar memoria, la JVM mantiene una zona especial llamada string pool (o string intern pool) donde guarda una copia única de cada literal String que aparece en tu código. Si dos variables apuntan al mismo literal, en realidad apuntan al mismo objeto del pool.

1
2
3
String a = "hola";
String b = "hola";
System.out.println(a == b);   // true: apuntan al mismo objeto del pool

Analogía. El pool de strings es como una biblioteca pública de textos. Si en tu código pides dos veces "Don Quijote", el bibliotecario no te compra dos libros: te da dos carnets que apuntan al mismo ejemplar físico. Los dos carnets son distintos, pero el libro que referencian es el mismo. Por eso a == b es true: las dos variables son direcciones distintas, pero apuntan al mismo objeto del pool.

Sin embargo, si creas un String con new, te saltas el pool y creas un objeto nuevo en el heap:

1
2
3
4
String a = "hola";
String c = new String("hola");
System.out.println(a == c);   // false: c es un objeto nuevo, distinto del pool
System.out.println(a.equals(c)); // true: el contenido sí es igual
flowchart TD
    A["String a = \"hola\";"] --> POOL["Pool de strings<br/>objeto 1: \"hola\""]
    B["String b = \"hola\";"] --> POOL
    C["String c = new String(\"hola\");"] --> HEAP["Heap (fuera del pool)<br/>objeto 2: \"hola\""]

    A -.->|misma referencia| B
    A -.->|referencia distinta| C

    POOL --> R1["a == b: TRUE"]
    A --> R1
    B --> R1
    A --> R2["a == c: FALSE"]
    C --> R2

    style POOL fill:#e8f5e9,stroke:#2e7d32,color:#000
    style HEAP fill:#fff3e0,stroke:#e65100,color:#000
    style R1 fill:#e8f5e9,stroke:#2e7d32,color:#000
    style R2 fill:#ffebee,stroke:#c62828,color:#000
    style A fill:#e3f2fd,stroke:#1565c0,color:#000
    style B fill:#e3f2fd,stroke:#1565c0,color:#000
    style C fill:#f3e5f5,stroke:#6a1b9a,color:#000

Diagrama 5.1 — El pool de strings: literales compartidos vs. objetos creados con new.

El método intern()

Si quieres meter un String creado con new en el pool, puedes usar el método intern():

1
2
3
4
String c = new String("hola");
String d = c.intern();        // d apunta al objeto del pool
System.out.println(c == d);   // false: c sigue fuera del pool
System.out.println("hola" == d); // true: d está en el pool

En la práctica casi nunca llamas a intern() a mano: la JVM ya lo hace por ti con los literales, y el resto del tiempo usas .equals() para comparar contenido. Pero conviene saber que existe, porque lo verás en preguntas de examen y en código heredado.

Implicaciones para tu código

Situación Comportamiento Recomendación
Dos literales idénticos Comparten objeto en el pool Comparar con .equals() por seguridad
new String("hola") Crea objeto fuera del pool Evitar new String(...) salvo necesidad justificada
Concatenación de literales en compilación El compilador la resuelve y mete el resultado en el pool Bien, eficiente
Concatenación con variables en ejecución Crea objeto nuevo fuera del pool Usar StringBuilder si es en bucle

Pista. Java moderno (desde Java 9) usa compact strings por defecto: en lugar de guardar cada carácter como 2 bytes (UTF-16), detecta si la cadena solo tiene caracteres Latin-1 y la guarda como 1 byte por carácter. Esto reduce a la mitad el consumo de memoria para textos en español, inglés y otros idiomas occidentales, sin que tú tengas que hacer nada. Es una de las razones por las que Java 25 es más eficiente que versiones antiguas.


5.3 Concatenación: el operador + y sus trampas

La forma sencilla: el operador +

En Java, el operador + está sobrecargado para String: si uno de los operandos es String, el otro se convierte a String y se concatenan.

1
2
3
4
5
6
7
String nombre = "Ana";
int edad = 23;
double nota = 8.75;

String mensaje = "Alumna " + nombre + ", edad " + edad + ", nota " + nota;
System.out.println(mensaje);
// imprime: Alumna Ana, edad 23, nota 8.75

Esta forma es legible y perfecta para casos puntuales. El compilador, además, optimiza automáticamente la concatenación simple convirtiéndola internamente en llamadas a StringBuilder (desde Java 9, a invokedynamic con StringConcatFactory), así que no tienes que preocuparte por el rendimiento cuando concatenas una vez.

La trampa del rendimiento en bucles

El problema aparece cuando concatenas dentro de un bucle. Como String es inmutable, cada concatenación crea un objeto nuevo:

1
2
3
4
5
6
String resultado = "";
for (int i = 0; i < 1000; i++) {
    resultado = resultado + i + ",";   // crea un String nuevo en cada iteración
}
// resultado tiene 1000 comas, pero por el camino se han creado 1000 Strings
// intermedios que el garbage collector tiene que limpiar
flowchart LR
    A["resultado = '' (vacío)"] --> B["+ '0,'"]
    B --> C["resultado = '0,'<br/>(objeto nuevo)"]
    C --> D["+ '1,'"]
    D --> E["resultado = '0,1,'<br/>(otro objeto nuevo)"]
    E --> F["+ '2,'"]
    F --> G["resultado = '0,1,2,'<br/>(otro más)"]
    G --> H["... 1000 veces ..."]

    C -.->|basura| GC1["GC"]
    E -.->|basura| GC1
    G -.->|basura| GC1

    style A fill:#e3f2fd,stroke:#1565c0,color:#000
    style C fill:#fff3e0,stroke:#e65100,color:#000
    style E fill:#fff3e0,stroke:#e65100,color:#000
    style G fill:#fff3e0,stroke:#e65100,color:#000
    style GC1 fill:#ffebee,stroke:#c62828,color:#000

Diagrama 5.2 — Concatenar String en un bucle crea un objeto nuevo en cada iteración; los anteriores quedan como basura para el GC.

La solución es usar StringBuilder (lo verás en detalle en UD2), una clase mutable que permite construir cadenas eficientemente:

1
2
3
4
5
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(",");
}
String resultado = sb.toString();   // una sola conversión final

No te preocupes ahora por la sintaxis de StringBuilder; lo importante es que recuerdes la regla:

Regla. Para concatenaciones puntuales (una o dos veces), usa +. Para concatenaciones en bucles o muchas concatenaciones seguidas, usa StringBuilder.

Concatenar con +=

El operador += también funciona con String, con la misma salvedad de rendimiento:

1
2
3
4
5
String log = "";
log += "Inicio del programa\n";
log += "Procesando datos...\n";
log += "Fin.\n";
// equivalente a log = log + "..."; en cada línea

Bien para mensajes ocasionales; malo para logs masivos.


5.4 String.format y formatted: plantillas para textos

Por qué no siempre concatenar

La concatenación con + se vuelve ilegible cuando hay muchas variables:

1
2
3
4
5
6
7
8
9
// Difícil de leer
String mensaje = "Alumna " + nombre + ", edad " + edad + ", nota media " + nota
                + ", matriculada en " + curso + " con profesor " + profesor;

// Mucho más legible con plantilla
String mensaje = String.format(
    "Alumna %s, edad %d, nota media %.2f, matriculada en %s con profesor %s",
    nombre, edad, nota, curso, profesor
);

String.format usa especificadores de formato (los mismos que printf, que ya viste en la Sección 3). La ventaja sobre + es que separas la plantilla (el texto con marcadores) de los datos (las variables), lo cual es más legible y más fácil de traducir o mantener.

formatted: el método de instancia (Java 15+)

Desde Java 15, los String tienen un método formatted que es azúcar sintáctico para String.format:

1
2
3
4
5
// Forma clásica
String m1 = String.format("Hola, %s. Tienes %d años.", nombre, edad);

// Forma moderna (Java 15+)
String m2 = "Hola, %s. Tienes %d años.".formatted(nombre, edad);

Ambas hacen exactamente lo mismo; la segunda se lee más naturalmente cuando ya tienes la plantilla como String.

Especificadores más usados

Especificador Tipo Ejemplo Salida
%s String (cualquier tipo llama a toString()) "%s".formatted("hola") hola
%d Entero (byte, short, int, long) "%d".formatted(42) 42
%f Decimal (float, double) "%f".formatted(3.14) 3,140000
%.2f Decimal con 2 decimales "%.2f".formatted(3.14159) 3,14
%n Salto de línea portable "Línea%n".formatted() Línea + salto
%% Símbolo % literal "%d%%".formatted(50) 50%
%b Booleano "%b".formatted(true) true
%c Carácter "%c".formatted('A') A
%x Entero en hexadecimal "%x".formatted(255) ff
%e Notación científica "%e".formatted(123456.789) 1,234568e+05
%10s String en 10 caracteres (alineado derecha) "%10s".formatted("hi") hi
%-10s String en 10 caracteres (alineado izquierda) "%-10s".formatted("hi") hi

Cuidado con la coma decimal. Como vimos en la Sección 3, en sistemas con locale español printf y format usan la coma (8,75); con locale inglés, el punto (8.75). Si necesitas un formato consistente independientemente del país (por ejemplo, al generar JSON o CSV), fuerza el locale: "%.2f".formatted(java.util.Locale.US, 3.14) produce siempre 3.14.


5.5 Text blocks: cadenas multilínea (Java 15+)

El problema que resuelven

Antes de Java 15, escribir una cadena multilínea (un mensaje HTML, una consulta SQL, un JSON) era un calvario de escapes y concatenaciones:

1
2
3
4
5
6
7
8
9
// Forma antigua: horrible
String html = "<html>\n" +
              "  <head>\n" +
              "    <title>Mi página</title>\n" +
              "  </head>\n" +
              "  <body>\n" +
              "    <h1>Hola</h1>\n" +
              "  </body>\n" +
              "</html>";

Cada línea acaba con \n" +, hay que escapar comillas internas, y la indentación del código no se corresponde con la indentación del texto resultante. Un infierno para leer y mantener.

La solución: text blocks """..."""

Desde Java 15 (estable), puedes escribir cadenas multilínea con tres comillas dobles """:

String html = """
        <html>
          <head>
            <title>Mi página</title>
          </head>
          <body>
            <h1>Hola</h1>
          </body>
        </html>
        """;

Reglas del text block:

  1. Empieza con """ seguido de un salto de línea (no puede haber texto en la misma línea que el """ inicial).
  2. Termina con """ que puede ir en su propia línea o al final de la última línea de contenido.
  3. La indentación mínima común a todas las líneas se elimina automáticamente (es la indentación de cierre """). Así puedes indentar el bloque al nivel del código sin que esa indentación contamine el texto.
  4. No necesitas escapar comillas dobles simples ("), ni concatenar, ni añadir \n.

Reglas de indentación

La indentación que se conserva en el texto resultante es la que está por encima de la mínima. El compilador calcula la indentación de la línea de cierre """ y la resta a todas las líneas:

1
2
3
4
5
6
7
String texto = """
        Hola,
          esto tiene 2 espacios extra
        adiós
        """;
// Resultado:
// "Hola,\n  esto tiene 2 espacios extra\nadiós\n"

Escapes dentro de text blocks

Dentro de un text block puedes seguir usando secuencias de escape, y además tienes dos nuevas:

Escape Significado Ejemplo
\n, \t Nueva línea, tabulador (como siempre) Estándar
\" Comilla doble (aunque a menudo no hace falta) """Dijo \"hola\""""
\s Espacio al final de línea (preserva trailing spaces) "línea\s\n"
\\ Barra invertida Estándar

El escape \s es útil porque por defecto el compilador elimina los espacios al final de cada línea de un text block. Si necesitas preservarlos (por ejemplo, en un formato que los use como separadores), añade \s al final.

Text blocks con formatted

La combinación ganadora para construir textos complejos con variables es text block + formatted:

1
2
3
4
5
6
7
8
String informe = """
        --- Informe de alumno ---
        Nombre:    %s
        Edad:      %d
        Nota media: %.2f
        Estado:    %s
        -------------------------
        """.formatted(nombre, edad, nota, estado);

Mucho más legible que concatenar con + y más mantenible que escapar \n a mano.


5.6 La trampa clásica: == vs .equals()

El error más famoso de Java

Esta es, posiblemente, la fuente número 1 de bugs en programas Java escritos por principiantes. Aparece en todos los exámenes, en todas las entrevistas de trabajo y en miles de issues de GitHub. Vamos a desterrarlo de una vez por todas.

1
2
3
4
5
6
7
String a = "hola";
String b = "hola";
System.out.println(a == b);        // true (mismo objeto del pool)

String c = new String("hola");
System.out.println(a == c);        // false (c es otro objeto)
System.out.println(a.equals(c));   // true (contenido idéntico)

Qué hace cada uno

Operador/Método Compara Cuándo da true
== Identidad: si las dos variables apuntan al mismo objeto en memoria Solo cuando son el mismo objeto
.equals() Contenido: si los dos textos son iguales carácter a carácter Siempre que el contenido sea idéntico, sean o no el mismo objeto
flowchart TD
    Q["Quiero comparar dos Strings"] --> P{"¿Qué quiero comprobar?"}
    P -->|"Si son el mismo objeto"| EQ["a == b"]
    P -->|"Si tienen el mismo texto"| EQM["a.equals(b)"]

    EQ --> R1{"¿Cuándo TRUE?"}
    R1 --> R1A["Solo si a y b apuntan<br/>al mismo objeto del pool/heap"]
    R1 --> R1B["false aunque el texto sea idéntico<br/>si uno se creó con new"]

    EQM --> R2{"¿Cuándo TRUE?"}
    R2 --> R2A["Siempre que el texto sea idéntico<br/>carácter a carácter"]
    R2 --> R2B["Independientemente de si<br/>comparten o no objeto"]

    R1B --> CONCL["USAR == ES CASI SIEMPRE UN BUG"]
    R2A --> CONCL2["USAR .equals() ES LO CORRECTO"]

    style Q fill:#1565c0,color:#fff,stroke:#0d47a1
    style P fill:#fff8e1,stroke:#f57f17,color:#000
    style EQ fill:#ffebee,stroke:#c62828,color:#000
    style EQM fill:#e8f5e9,stroke:#2e7d32,color:#000
    style CONCL fill:#ffebee,stroke:#c62828,color:#000
    style CONCL2 fill:#e8f5e9,stroke:#2e7d32,color:#000

Diagrama 5.3 — Regla de oro: para comparar textos, usa siempre .equals(). == casi siempre es un bug.

Por qué == a veces "funciona"

Lo que confunde a los principiantes es que == a veces da true y a veces false, sin razón aparente. La razón es el pool de strings:

  • Si los dos String vienen de literales, el compilador los mete en el pool y == da true.
  • Si uno viene de new String(), de una concatenación en ejecución, de leer un fichero, de entrada de usuario o de un método, se sale del pool y == da false aunque el contenido sea idéntico.
String a = "hola";
String b = "hola";
String c = "ho" + "la";            // el compilador lo resuelve a "hola" → pool
String d = "ho";
String e = d + "la";               // concatenación en ejecución → fuera del pool
String f = new String("hola");     // explícitamente fuera del pool

System.out.println(a == b);  // true  (literales, mismo objeto del pool)
System.out.println(a == c);  // true  (el compilador resuelve c a "hola" en compilación)
System.out.println(a == e);  // false (e se calcula en ejecución, fuera del pool)
System.out.println(a == f);  // false (f es new)

Cuidado en exámenes y entrevistas. Esta trampa es tan frecuente que aparece literalmente en todos los exámenes de certificación Java y en el 90% de las entrevistas técnicas. La regla mnemotécnica: para comparar textos, equals; para comparar números, ==. (Para objetos en general, también equals, pero lo verás en UD4.)

Casos límite

Comparar ignorando mayúsculas

1
2
3
4
String a = "Hola";
String b = "hola";
System.out.println(a.equals(b));           // false (distingue mayúsculas)
System.out.println(a.equalsIgnoreCase(b)); // true (ignora mayúsculas)

Comparar con null

1
2
3
4
String a = "hola";
String b = null;
System.out.println(b.equals(a));   // CRASH: NullPointerException
System.out.println(a.equals(b));   // false (sin excepción)

Para evitar el NPE, llama siempre a .equals() sobre la variable que sabes que no es null:

1
2
3
4
5
// Patrón seguro
if (a != null && a.equals(b)) { ... }

// Mejor aún con Objects.equals (Java 7+)
if (java.util.Objects.equals(a, b)) { ... }

Objects.equals(a, b) es null-safe: devuelve true si ambos son null, false si solo uno lo es, y a.equals(b) en caso contrario. Es la forma recomendada cuando no estás seguro de si las referencias pueden ser null.


5.7 Métodos más útiles de String

String tiene más de 60 métodos. Estos son los que vas a usar a diario:

Método Qué hace Ejemplo Resultado
length() Devuelve la longitud (número de caracteres) "hola".length() 4
charAt(i) Carácter en la posición i (empieza en 0) "hola".charAt(1) 'o'
substring(i) Subcadena desde i hasta el final "hola".substring(2) "la"
substring(i, f) Subcadena desde i (inclusive) hasta f (exclusive) "hola".substring(1, 3) "ol"
toUpperCase() Mayúsculas "hola".toUpperCase() "HOLA"
toLowerCase() Minúsculas "HOLA".toLowerCase() "hola"
trim() Elimina espacios al inicio y al final " hola ".trim() "hola"
strip() Igual que trim pero Unicode-aware (Java 11+) "\u2000hola\u2000".strip() "hola"
stripLeading() / stripTrailing() Solo por un lado " hola ".stripLeading() "hola "
replace(old, new) Reemplaza todas las ocurrencias "hola".replace('o', '0') "h0la"
replaceAll(regex, new) Reemplaza con expresión regular "h1o2l3a".replaceAll("\\d", "") "hola"
split(regex) Divide en array por el separador "a,b,c".split(",") ["a","b","c"]
contains(s) ¿Contiene la subcadena? "hola".contains("ol") true
startsWith(s) / endsWith(s) ¿Empieza/acaba por? "hola".startsWith("ho") true
indexOf(s) Posición de la primera ocurrencia "hola".indexOf("l") 2
isEmpty() ¿Está vacío? (longitud 0) "".isEmpty() true
isBlank() ¿Está en blanco? (solo espacios, Java 11+) " ".isBlank() true
repeat(n) Repite la cadena n veces (Java 11+) "ab".repeat(3) "ababab"
equals(s) Compara contenido "hola".equals("hola") true
equalsIgnoreCase(s) Compara ignorando mayúsculas "Hola".equalsIgnoreCase("hola") true
compareTo(s) Comparación lexicográfica (<0, 0, >0) "ana".compareTo("ben") <0
// Ejemplos en contexto
String nombre = "  Ana Pérez  ";

String limpio = nombre.strip();           // "Ana Pérez"
int longitud = limpio.length();           // 9
char primera = limpio.charAt(0);          // 'A'
String mayus = limpio.toUpperCase();      // "ANA PÉREZ"
String[] partes = limpio.split(" ");      // ["Ana", "Pérez"]
boolean esAna = limpio.startsWith("Ana"); // true
String apellido = limpio.substring(4);    // "Pérez"

// repeat es útil para generar separadores
String separador = "-".repeat(30);        // "------------------------------"

Pista. Para distinguir trim() y strip(): trim() elimina caracteres ASCII con código ≤ 32 (espacio, tabulador, salto de línea...); strip() elimina todos los caracteres Unicode considerados "whitespace" (incluido el espacio inseparable, el espacio fino tipográfico, etc.). En código nuevo, usa strip().


5.8 Errores comunes y buenas prácticas

Los seis errores que verás

Error Síntoma Causa Solución
Comparar con == Comparación da false cuando el texto es idéntico == compara identidad, no contenido Usa .equals() (o Objects.equals())
Olvidar asignar el resultado El String "no cambia" tras toUpperCase() etc. Los métodos devuelven un String nuevo; no modifican el original nombre = nombre.toUpperCase();
Llamar a .equals() sobre null NullPointerException La variable es null Objects.equals(a, b) o comprobar null antes
Índice fuera de rango StringIndexOutOfBoundsException charAt(i) o substring(i, j) con índices inválidos Comprueba length() antes
Confundir "" y null Comportamiento distinto entre vacío y nulo "" es un String de longitud 0; null es "no hay String" Usa isEmpty() o isBlank() para vacío; == null para nulo
Concatenar en bucle Rendimiento pésimo en logs o construcción de mensajes Cada + crea un String nuevo Usa StringBuilder (UD2)

Diferencia entre "", " " y null

String vacio = "";         // String de longitud 0, no es null
String espacios = "   ";   // String de longitud 3, no es null
String nulo = null;        // no apunta a ningún String

vacio.isEmpty();           // true
vacio.isBlank();           // true (Java 11+)
espacios.isEmpty();        // false (longitud 3)
espacios.isBlank();        // true (solo espacios, Java 11+)
nulo.isEmpty();            // CRASH: NullPointerException

vacio == null;             // false
nulo == null;              // true

Buenas prácticas resumidas

  1. Para comparar textos, siempre .equals() (o equalsIgnoreCase si no importan las mayúsculas). Reserva == para casos muy concretos donde quieres comprobar identidad real (casi nunca).
  2. Usa Objects.equals(a, b) si alguna de las dos puede ser null: es null-safe y te ahorra el NPE.
  3. Recuerda que los métodos devuelven un String nuevo: asigna siempre el resultado (nombre = nombre.strip();).
  4. Para cadenas multilínea, usa text blocks """...""": más legibles que escapes y concatenaciones.
  5. Para plantillas con variables, usa formatted (Java 15+): más legible que + cuando hay más de 2 variables.
  6. Para concatenar en bucles, usa StringBuilder: evita crear miles de objetos intermedios.
  7. Verifica null antes de llamar a métodos si la variable viene de una fuente externa (entrada de usuario, fichero, base de datos).
  8. Distingue "" (vacío) de null (ausente) y comprueba ambos si procede: if (nombre == null || nombre.isBlank()) para validar "no tiene valor útil".

Resumen de la sección 5

Antes de pasar a la siguiente sección, asegúrate de que controlas estos puntos:

  1. String es un tipo de referencia, no primitivo: la variable contiene una dirección al objeto, no el texto directamente. Es inmutable: los métodos como toUpperCase() devuelven un String nuevo, no modifican el original.
  2. La JVM mantiene un pool de strings para reutilizar literales idénticos. Por eso dos literales idénticos apuntan al mismo objeto (== da true), pero un new String("hola") crea un objeto aparte (== da false).
  3. La concatenación con + es correcta para casos puntuales; en bucles, usa StringBuilder para evitar crear miles de objetos intermedios.
  4. String.format y el método formatted (Java 15+) permiten construir textos con plantillas y especificadores (%s, %d, %.2f, %n), más legibles que la concatenación cuando hay muchas variables.
  5. Los text blocks """...""" (Java 15+) permiten escribir cadenas multilínea sin escapes ni concatenaciones. La indentación común se elimina automáticamente.
  6. La trampa clásica == vs .equals(): == compara identidad (mismo objeto), .equals() compara contenido. Para comparar textos, usa siempre .equals() o Objects.equals() (null-safe).
  7. String tiene más de 60 métodos útiles: length, charAt, substring, toUpperCase, strip, replace, split, contains, startsWith, indexOf, isEmpty, isBlank, repeat, etc.

Próximos pasos y checkpoint

En la Sección 6 entraremos en el mundo de las constantes y los literales: por qué evitar los magic numbers, cómo declarar constantes con final (locales y de campo), las constantes de clase con static final, la convención UPPER_SNAKE_CASE, y una introducción a los enum (que se profundizarán en UD4). Empezarás a escribir código más mantenible y profesional.

¿Te encaja el nivel y el ritmo de esta Sección 5, o prefieres que ajuste algo (más ejemplos de cada método, ejercicios resueltos, más profundo en el pool, etc.)?

Cuando me confirmes, redactaré la Sección 6 — Constantes y literales, donde aprenderás a eliminar los números mágicos de tu código.