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 = 18es un cajón donde has metido físicamente 18 monedas. La variable contiene el valor. En cambio, una variableString 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.
Cuidado. Esta es la fuente número 1 de confusión con
String. Los métodos comotoUpperCase(),trim(),replace()osubstring()no modifican elStringoriginal: devuelven uno nuevo. Si no asignas el resultado a una variable, el cambio se pierde. El patrón correcto siempre esnombre = nombre.metodo();.
Por qué String es inmutable
La inmutabilidad no es un capricho: tiene tres ventajas técnicas importantes:
- 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. - Rendimiento (pool): al ser inmutable, la JVM puede reutilizar el mismo objeto
Stringpara literales idénticos (el pool de strings, que veremos en 5.2). - Cache de
hashCode: elhashCodede unStringse calcula una vez y se cachea; si el texto pudiera cambiar, el cache sería inválido.
Declaración y literales
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.
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 == bestrue: 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:
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():
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.
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:
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:
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, usaStringBuilder.
Concatenar con +=
El operador += también funciona con String, con la misma salvedad de rendimiento:
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:
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:
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
printfyformatusan 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 siempre3.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:
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 """:
Reglas del text block:
- Empieza con
"""seguido de un salto de línea (no puede haber texto en la misma línea que el"""inicial). - Termina con
"""que puede ir en su propia línea o al final de la última línea de contenido. - 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. - 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:
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:
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.
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
Stringvienen de literales, el compilador los mete en el pool y==datrue. - 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==dafalseaunque el contenido sea idéntico.
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énequals, pero lo verás en UD4.)
Casos límite
Comparar ignorando mayúsculas
Comparar con null
Para evitar el NPE, llama siempre a .equals() sobre la variable que sabes que no es null:
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 |
Pista. Para distinguir
trim()ystrip():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, usastrip().
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
Buenas prácticas resumidas
- Para comparar textos, siempre
.equals()(oequalsIgnoreCasesi no importan las mayúsculas). Reserva==para casos muy concretos donde quieres comprobar identidad real (casi nunca). - Usa
Objects.equals(a, b)si alguna de las dos puede sernull: es null-safe y te ahorra el NPE. - Recuerda que los métodos devuelven un
Stringnuevo: asigna siempre el resultado (nombre = nombre.strip();). - Para cadenas multilínea, usa text blocks
"""...""": más legibles que escapes y concatenaciones. - Para plantillas con variables, usa
formatted(Java 15+): más legible que+cuando hay más de 2 variables. - Para concatenar en bucles, usa
StringBuilder: evita crear miles de objetos intermedios. - Verifica
nullantes de llamar a métodos si la variable viene de una fuente externa (entrada de usuario, fichero, base de datos). - Distingue
""(vacío) denull(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:
Stringes un tipo de referencia, no primitivo: la variable contiene una dirección al objeto, no el texto directamente. Es inmutable: los métodos comotoUpperCase()devuelven unStringnuevo, no modifican el original.- La JVM mantiene un pool de strings para reutilizar literales idénticos. Por eso dos literales idénticos apuntan al mismo objeto (
==datrue), pero unnew String("hola")crea un objeto aparte (==dafalse). - La concatenación con
+es correcta para casos puntuales; en bucles, usaStringBuilderpara evitar crear miles de objetos intermedios. String.formaty el métodoformatted(Java 15+) permiten construir textos con plantillas y especificadores (%s,%d,%.2f,%n), más legibles que la concatenación cuando hay muchas variables.- Los text blocks
"""..."""(Java 15+) permiten escribir cadenas multilínea sin escapes ni concatenaciones. La indentación común se elimina automáticamente. - La trampa clásica
==vs.equals():==compara identidad (mismo objeto),.equals()compara contenido. Para comparar textos, usa siempre.equals()oObjects.equals()(null-safe). Stringtiene 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.