Saltar a contenido

5. El tipo String y texto multilínea

Ficha de la sección

Campo Valor
Unidad de Programación UD1 — Fundamentos de Programación
Resultado de Aprendizaje RA1 — Reconoce la estructura de un programa informático, identificando y relacionando los elementos propios del lenguaje de programación utilizado
Duración orientativa Aproximadamente 3 de las 28 horas totales de UD1
Criterios de Evaluación implicados CE 1.4 (Se han identificado los distintos tipos de variables y la utilidad específica de cada uno) y CE 1.5 (Se ha modificado el código de un programa para crear y utilizar variables), extendiendo lo visto en la Sección 4 más allá de los tipos primitivos
Encaja en el roadmap Cierra el bloque de "tipos de dato" de UD1. Esta sección solo abre la puerta al String; su inmutabilidad, el string interning en profundidad y el resto de sus métodos (substring, split, trim...) se trabajarán a fondo en UD2

Antes de empezar

En la Sección 4 aprendiste que los tipos primitivos son como recipientes de tamaño fijo: una vez eliges int o double, el recipiente ocupa siempre el mismo espacio y contiene el valor directamente dentro. Pero un texto no funciona así. Un nombre puede tener 3 letras o 300, una frase puede ser corta o un párrafo entero: no existe un "recipiente de tamaño fijo" razonable para eso.

Por eso String no es un tipo primitivo. Es una clase del API estándar de Java (concretamente de java.lang), y eso lo convierte en lo que se llama un tipo de referencia. Vamos a ver qué significa exactamente esa diferencia, porque de ella depende directamente la trampa más famosa de todo Java para quien empieza.


5.1. String como tipo de referencia especial: el pool de strings

Tipo primitivo vs. tipo de referencia

Piensa en una variable primitiva como una taquilla que contiene el objeto directamente dentro: abres la taquilla edad y ahí está el 25, físicamente. En cambio, piensa en una variable de tipo referencia como una taquilla que contiene una papeleta con una dirección, no el objeto en sí. Esa papeleta te dice "el contenido de verdad está en tal sitio de la memoria". Para llegar al texto real, primero lees la papeleta y luego vas a esa dirección.

flowchart LR
    subgraph PRIM["Tipo primitivo — int edad = 25;"]
        direction TB
        C1["Variable 'edad'"] -->|"contiene directamente"| V1["25"]
    end
    subgraph REF["Tipo de referencia — String nombre = \"Ana\";"]
        direction TB
        C2["Variable 'nombre'"] -->|"contiene una papeleta\ncon una dirección"| PTR["Dirección de memoria"]
        PTR -->|"apunta a"| OBJ["El objeto String real: \"Ana\""]
    end

    style C1 fill:#1565c0,color:#fff,stroke:#0d47a1
    style C2 fill:#6a1b9a,color:#fff,stroke:#4a148c
    style OBJ fill:#2e7d32,color:#fff,stroke:#1b5e20

Esta distinción, que hoy parece un detalle técnico menor, se va a convertir en la base de casi toda la programación orientada a objetos que verás a partir de UD4: cualquier objeto (no solo String) funciona con esta misma lógica de "papeleta con dirección".

El pool de strings: la biblioteca compartida

Java hace algo inteligente con los literales de texto (los String que escribes directamente entre comillas dobles): en vez de crear un ejemplar nuevo cada vez que escribes el mismo texto, los guarda en una zona especial de memoria llamada pool de strings, y reutiliza el mismo ejemplar cuantas veces haga falta.

Es exactamente como una biblioteca pública que solo mantiene un único ejemplar físico de un libro popular: si dos personas piden ese libro, a ambas se les entrega una papeleta que apunta al mismo ejemplar. Nadie necesita imprimir una copia nueva, porque nadie va a escribir ni a rayar el libro (los String son inmutables, algo que estudiaremos a fondo en UD2), así que compartir el mismo ejemplar es completamente seguro.

flowchart TB
    subgraph POOL["Pool de Strings (biblioteca compartida de literales)"]
        LIB["\"Hola\"  (un único ejemplar físico)"]
    end
    A["String a = \"Hola\";"] -->|"papeleta apunta a"| LIB
    B["String b = \"Hola\";"] -->|"papeleta apunta también a"| LIB
    C["String c = new String(\"Hola\");"] --> NEW["Un ejemplar NUEVO e independiente,\nfuera del pool, aunque diga lo mismo"]

    style LIB fill:#2e7d32,color:#fff,stroke:#1b5e20
    style NEW fill:#e65100,color:#fff,stroke:#bf360c

Fíjate en el tercer caso: cuando creas un String explícitamente con new String("Hola"), le estás pidiendo a Java "imprímeme una copia nueva, aunque ya exista una en la biblioteca". Esa copia nueva vive fuera del pool, es un objeto distinto en memoria, aunque su contenido sea idéntico. Este detalle, en apariencia insignificante, es la raíz exacta de la trampa que veremos en el punto 5.5.


5.2. Concatenación con + y String.format()

La forma más directa de construir un texto a partir de otros valores es el operador +:

1
2
3
4
5
String nombre = "Ana";
int edad = 22;

String mensaje = "Hola, " + nombre + ". Tienes " + edad + " años.";
System.out.println(mensaje);

Cuando necesitas un control más preciso del formato (por ejemplo, cuántos decimales mostrar, o cómo alinear columnas), String.format() suele ser más legible que encadenar muchos +:

String mensaje = String.format("Hola, %s. Tienes %d años.", nombre, edad);
System.out.println(mensaje);
Concatenación con + String.format()
Legibilidad con varias variables Se vuelve difícil de leer de un vistazo El texto mantiene su forma de frase, más fácil de leer
Control de formato (decimales, anchura de columna...) No permite formato preciso Sí, mediante especificadores (%d, %.2f...), que explotaremos a fondo en UD2 y UD5
Rendimiento en bucles largos Ineficiente, por la inmutabilidad de String (lo veremos en UD2 con StringBuilder) No aplica en este uso puntual
Cuándo usarlo Concatenaciones simples y cortas Cuando necesitas dar un formato exacto al resultado

5.3. Text blocks: strings multilínea sin escapes (Java 15+)

Antes de los text blocks, escribir un texto que ocupara varias líneas (por ejemplo, un fragmento de JSON o de HTML) obligaba a usar el carácter de salto de línea escapado \n y a concatenar línea a línea, con el consiguiente riesgo de errores:

1
2
3
4
5
// Antes de los text blocks: verboso y frágil
String json = "{\n" +
              "  \"nombre\": \"Ana\",\n" +
              "  \"edad\": 22\n" +
              "}";

Desde Java 15, los text blocks (bloques de texto) permiten escribir directamente texto multilínea, delimitado por triple comilla doble """, sin necesidad de escapar los saltos de línea ni las comillas internas:

1
2
3
4
5
6
7
// Con text blocks: tal cual se ve, así se guarda
String json = """
        {
          "nombre": "Ana",
          "edad": 22
        }
        """;
Regla del text block Detalle
Apertura """ seguido obligatoriamente de un salto de línea (no puede haber texto en la misma línea de apertura)
Cierre """ puede ir en su propia línea o al final del último contenido
Comillas dobles internas No hace falta escaparlas: "nombre" se escribe tal cual, sin \"
Indentación Java calcula automáticamente cuánta indentación es "común" a todas las líneas y la retira, dejando el texto limpio

Dato clave. Los text blocks no son solo una comodidad estética. Cuando en UD5 y UD6 trabajemos con JSON, XML o consultas SQL multilínea, vas a agradecer poder escribir ese contenido tal y como se vería en su formato original, en lugar de una maraña de comillas escapadas y símbolos + de concatenación.


5.4. El método formatted()

Desde Java 15, además de String.format(patron, argumentos), existe una forma equivalente pero más fluida: llamar directamente al método formatted() sobre el propio texto:

1
2
3
4
5
String nombre = "Ana";
int edad = 22;

String mensaje = "Hola, %s. Tienes %d años.".formatted(nombre, edad);
System.out.println(mensaje);

Funcionalmente, "patrón".formatted(args) y String.format("patrón", args) hacen exactamente lo mismo: la diferencia es puramente de estilo. formatted() se lee de forma más natural cuando el propio texto con los especificadores ya está delante de tus ojos, y encaja mejor con el estilo de encadenar llamadas a métodos que iremos viendo a lo largo del curso.


5.5. ¡Trampa clásica! == frente a .equals()

Este es, sin exagerar, el error de principiante más repetido en toda la historia de Java, y ahora ya tienes todas las piezas para entender exactamente por qué ocurre.

  • ==, aplicado a tipos de referencia como String, no compara el contenido del texto. Compara si las dos papeletas apuntan a la misma dirección de memoria, es decir, si son literalmente el mismo objeto físico.
  • .equals() sí compara el contenido: recorre carácter a carácter y te dice si el texto es igual, sin importar si son el mismo objeto en memoria o dos copias distintas.
1
2
3
4
5
6
7
String a = "Hola";
String b = "Hola";
String c = new String("Hola");

System.out.println(a == b);          // true  → misma referencia: ambas vienen del pool
System.out.println(a == c);          // false → objetos distintos en memoria
System.out.println(a.equals(c));     // true  → mismo contenido, aunque sean objetos distintos
flowchart TD
    Q["¿Qué quieres comprobar?"] -->|"¿Tienen el mismo CONTENIDO\n(casi siempre esto es lo que buscas)?"| EQ[".equals() → compara el texto"]
    Q -->|"¿Son literalmente\nel mismo objeto en memoria\n(caso muy poco frecuente)?"| DE["== → compara la referencia"]

    style EQ fill:#2e7d32,color:#fff,stroke:#1b5e20
    style DE fill:#b71c1c,color:#fff,stroke:#7f0000

Por qué el ejemplo a == b da true y engaña a mucha gente. Como ambos son literales idénticos, el pool de strings hace que a y b apunten al mismo ejemplar (tal y como vimos en el punto 5.1), así que == "acierta" por pura coincidencia de cómo funciona el pool, no porque == esté comparando contenido. En cuanto uno de los dos String se crea de otra forma (leyendo de un fichero, uniendo trozos de texto en tiempo de ejecución, o con new String(...)), esa coincidencia desaparece y == te puede dar false aunque el contenido sea idéntico.

Regla de oro, sin excepciones. Para comparar el contenido de dos String (o de cualquier objeto, como verás con equals() y hashCode() en UD4), usa siempre .equals(). No uses nunca == para esta comparación, ni siquiera aunque "parezca que funciona" en una prueba rápida. Que funcione por casualidad hoy no significa que vaya a funcionar mañana, y este es exactamente el tipo de error que se cuela en producción y es una pesadilla de depurar.


Programa completo: todas las piezas juntas

package dam.programacion.ud1;

/**
 * Ejemplo de la Sección 5: String, concatenación, text blocks,
 * formatted() y la trampa == frente a equals().
 *
 * @author Alumno o alumna de DAM
 */
public class TextoBasico {

    public static void main(String[] args) {

        String nombre = "Ana";
        int edad = 22;

        // --- Concatenación con + ---
        String saludo = "Hola, " + nombre + ". Tienes " + edad + " años.";

        // --- String.format() ---
        String saludoFormateado = String.format("Hola, %s. Tienes %d años.", nombre, edad);

        // --- formatted() ---
        String saludoFluido = "Hola, %s. Tienes %d años.".formatted(nombre, edad);

        // --- Text block multilínea ---
        String fichaAlumno = """
                Ficha del alumno
                -----------------
                Nombre: %s
                Edad:   %d
                """.formatted(nombre, edad);

        // --- La trampa == frente a equals() ---
        String copiaLiteral = "Ana";
        String copiaNueva = new String("Ana");

        System.out.println(saludo);
        System.out.println(saludoFormateado);
        System.out.println(saludoFluido);
        System.out.println(fichaAlumno);
        System.out.println("nombre == copiaLiteral: " + (nombre == copiaLiteral)); // true
        System.out.println("nombre == copiaNueva: " + (nombre == copiaNueva));     // false
        System.out.println("nombre.equals(copiaNueva): " + nombre.equals(copiaNueva)); // true
    }
}

Resumen de la sección

Idea clave En una frase
String es tipo de referencia La variable guarda una "papeleta" (referencia), no el texto directamente
Pool de strings Java reutiliza un único ejemplar para literales idénticos, como una biblioteca compartida
Concatenación + Rápida y directa para casos simples
String.format() Control preciso del formato mediante especificadores
Text blocks """...""" Texto multilínea legible, sin escapes ni concatenaciones
formatted() Igual que String.format(), pero con estilo fluido sobre el propio texto
== vs .equals() == compara referencias (memoria); .equals() compara contenido. Usa siempre .equals()

Comprueba lo que has entendido

  1. ¿Por qué String se considera un tipo de referencia y no un tipo primitivo, a diferencia de int o double?
  2. Si String x = "Café"; String y = "Café";, ¿por qué x == y da true? ¿Y qué pasaría si y se hubiera creado con new String("Café")?
  3. Reescribe usando un text block esta cadena escapada: "Línea 1\nLínea 2\nLínea 3".
  4. ¿Cuál es la diferencia práctica entre String.format("Hola, %s", nombre) y "Hola, %s".formatted(nombre)?

Qué viene ahora

Ya dominas los dos grandes bloques de "tipos de dato" de UD1: los primitivos (Sección 4) y el String (esta sección). En la Sección 6 de UD1 vamos a ver las constantes y literales: cómo fijar con final y static final valores que no deben cambiar nunca durante la ejecución del programa, y por qué evitar los temidos magic numbers (números "mágicos" sueltos en el código sin explicación) es una de las señas de identidad de un código bien escrito.