Módulo 09: Cadenas de caracteres

Objetivos del módulo

  • Entender que String es un objeto inmutable y qué es el String pool
  • Crear y concatenar cadenas conociendo las trampas del operador +
  • Dominar los métodos principales de String
  • Comparar cadenas con equals, equalsIgnoreCase y compareTo, y saber por qué == no sirve
  • Recorrer una cadena carácter a carácter y usar la clase Character
  • Convertir entre cadenas y números, y tratar NumberFormatException
  • Construir cadenas eficientemente con StringBuilder
  • Escribir textos de varias líneas con text blocks (Java 15) y formatear con String.format y formatted
  • Validar datos con expresiones regulares básicas (DNI, email, código postal)
  • Resolver problemas clásicos con cadenas: palíndromos, anagramas, cifrado César…

1. String: un objeto inmutable

Desde el primer «Hola, mundo» has usado cadenas, pero String es distinto de int o double: no es un tipo primitivo, sino una clase. Cada cadena es un objeto, y una variable String guarda una referencia a ese objeto, igual que las variables de tipo array del módulo 08.

📘 Concepto: Un String es una secuencia inmutable de caracteres: una vez creado, no se puede modificar. Todos los métodos que parecen cambiarlo (toUpperCase, replace, trim, substring…) en realidad crean y devuelven un String nuevo y dejan el original intacto.

public class Inmutable {
    public static void main(String[] args) {
        String s = "Hola";
        s.toUpperCase();                 // crea "HOLA"... y se pierde
        System.out.println(s);           // s no ha cambiado

        String t = s.toUpperCase();      // hay que guardar el resultado
        System.out.println(t);

        s = s + " mundo";                // crea un String NUEVO y s pasa a apuntarlo
        System.out.println(s);
    }
}
Hola
HOLA
Hola mundo

La segunda línea de main es un error muy frecuente: llamar a s.toUpperCase() sin guardar el resultado no sirve de nada.

Antes y después de s = s + " mundo": s pasa a apuntar a un objeto nuevo y el antiguo queda sin referencias

Figura 1. Concatenar no modifica el objeto original: crea uno nuevo.

¿Por qué Java hace inmutables los String? Porque así se pueden compartir sin peligro (nadie puede cambiar el texto que otro está usando), son seguros entre hilos de ejecución y pueden guardarse en un almacén común, el String pool.

1.1. El String pool

Los literales de texto (las cadenas escritas entre comillas en el código) se guardan en una zona especial del montón llamada String pool. Si dos literales tienen el mismo texto, Java los guarda una sola vez y las dos variables apuntan al mismo objeto. En cambio, new String("hola") fuerza la creación de un objeto nuevo, y las cadenas que se construyen al ejecutar (leídas del teclado, concatenadas con variables…) también son objetos nuevos.

Las variables a y b apuntan al mismo literal hola del String pool y c apunta a otro objeto creado con new

Figura 2. Dos literales iguales comparten objeto en el pool; new String crea otro distinto.

public class StringPool {
    public static void main(String[] args) {
        String a = "hola";                   // literal: va al String pool
        String b = "hola";                   // reutiliza el MISMO objeto del pool
        String c = new String("hola");       // fuerza un objeto nuevo fuera del pool
        String d = "ho" + "la";              // constantes: se une al compilar -> pool
        String parte = "ho";
        String e = parte + "la";             // se une al ejecutar -> objeto nuevo

        System.out.println("a == b: " + (a == b));
        System.out.println("a == c: " + (a == c));
        System.out.println("a == d: " + (a == d));
        System.out.println("a == e: " + (a == e));
        System.out.println("a.equals(c): " + a.equals(c));
        System.out.println("a.equals(e): " + a.equals(e));
        System.out.println("a == e.intern(): " + (a == e.intern()));
    }
}
a == b: true
a == c: false
a == d: true
a == e: false
a.equals(c): true
a.equals(e): true
a == e.intern(): true
  • "ho" + "la" son dos constantes y el compilador las une al compilar: el resultado es el literal "hola", que está en el pool.
  • parte + "la" se calcula al ejecutar y produce un objeto nuevo.
  • intern() devuelve la versión del pool de una cadena (la añade si no estaba). Se usa muy poco; lo importante es entender el concepto.

💡 Consejo: No escribas nunca new String("texto"): crea un objeto innecesario. Escribe simplemente "texto".


2. Crear y concatenar cadenas

El operador + concatena cadenas. Si uno de los operandos es un String, el otro se convierte automáticamente a texto. Pero ojo, porque + se evalúa de izquierda a derecha:

public class Concatenar {
    public static void main(String[] args) {
        String nombre = "Ada";
        int edad = 36;
        String frase = "Me llamo " + nombre + " y tengo " + edad + " años";
        System.out.println(frase);

        // + se evalúa de izquierda a derecha
        System.out.println(1 + 2 + "3");        // (1 + 2) + "3"  -> "33"
        System.out.println("1" + 2 + 3);        // ("1" + 2) + 3  -> "123"
        System.out.println("1" + (2 + 3));      // paréntesis     -> "15"
        System.out.println('a' + 'b' + "!");    // char + char es int: 97 + 98

        String s = "Java";
        s += " 25";                             // s = s + " 25"
        s = s.concat(" LTS");
        System.out.println(s);
        System.out.println("null: " + null);    // concatenar null no falla
    }
}
Me llamo Ada y tengo 36 años
33
123
15
195!
Java 25 LTS
null: null
Expresión Resultado Por qué
1 + 2 + "3" "33" Primero 1 + 2 = 3 (números); luego 3 + "3"
"1" + 2 + 3 "123" "1" + 2 = "12"; luego "12" + 3
"1" + (2 + 3) "15" Los paréntesis fuerzan la suma primero
'a' + 'b' + "!" "195!" char + char es una suma de enteros: 97 + 98

⚠️ Importante: Un String puede valer null (no apunta a ningún objeto). Concatenar null produce el texto "null", pero llamar a un método sobre él (s.length() con s == null) lanza NullPointerException. Distingue bien entre null (no hay cadena) y "" (cadena vacía, de longitud 0).


3. Métodos principales de String

Cada carácter de una cadena tiene un índice, empezando en 0, como en los arrays:

La cadena Programar en Java con sus índices de 0 a 16 y ejemplos de substring, charAt, indexOf y lastIndexOf

Figura 3. Índices de una cadena y resultado de algunos métodos.

import java.util.Arrays;

public class MetodosString {
    public static void main(String[] args) {
        String s = "Programar en Java";
        System.out.println("length():           " + s.length());
        System.out.println("charAt(0):          " + s.charAt(0));
        System.out.println("charAt(13):         " + s.charAt(13));
        System.out.println("substring(13):      " + s.substring(13));
        System.out.println("substring(0, 9):    " + s.substring(0, 9));
        System.out.println("indexOf('a'):       " + s.indexOf('a'));
        System.out.println("indexOf(\"en\"):      " + s.indexOf("en"));
        System.out.println("indexOf('a', 6):    " + s.indexOf('a', 6));
        System.out.println("lastIndexOf('a'):   " + s.lastIndexOf('a'));
        System.out.println("indexOf('z'):       " + s.indexOf('z'));
        System.out.println("contains(\"Java\"):   " + s.contains("Java"));
        System.out.println("startsWith(\"Pro\"):  " + s.startsWith("Pro"));
        System.out.println("endsWith(\"va\"):     " + s.endsWith("va"));
        System.out.println("toUpperCase():      " + s.toUpperCase());
        System.out.println("toLowerCase():      " + s.toLowerCase());
        System.out.println("replace('a', '4'):  " + s.replace('a', '4'));
        System.out.println("replace(\"Java\", \"C\"): " + s.replace("Java", "C"));

        String conEspacios = "   hola   ";
        System.out.println("trim():             [" + conEspacios.trim() + "]");
        System.out.println("strip():            [" + conEspacios.strip() + "]");
        System.out.println("stripLeading():     [" + conEspacios.stripLeading() + "]");
        System.out.println("isEmpty() de \"  \":  " + "  ".isEmpty());
        System.out.println("isBlank() de \"  \":  " + "  ".isBlank());

        String csv = "rojo,verde,azul";
        String[] colores = csv.split(",");
        System.out.println("split(\",\"):         " + Arrays.toString(colores));
        System.out.println("join(\" | \"):        " + String.join(" | ", colores));
        System.out.println("repeat(3):          " + "ab".repeat(3));
        System.out.println("chars() de 'a':     " + s.chars().filter(c -> c == 'a').count());
    }
}
length():           17
charAt(0):          P
charAt(13):         J
substring(13):      Java
substring(0, 9):    Programar
indexOf('a'):       5
indexOf("en"):      10
indexOf('a', 6):    7
lastIndexOf('a'):   16
indexOf('z'):       -1
contains("Java"):   true
startsWith("Pro"):  true
endsWith("va"):     true
toUpperCase():      PROGRAMAR EN JAVA
toLowerCase():      programar en java
replace('a', '4'):  Progr4m4r en J4v4
replace("Java", "C"): Programar en C
trim():             [hola]
strip():            [hola]
stripLeading():     [hola   ]
isEmpty() de "  ":  false
isBlank() de "  ":  true
split(","):         [rojo, verde, azul]
join(" | "):        rojo | verde | azul
repeat(3):          ababab
chars() de 'a':     4
Método Devuelve Ejemplo con s = "Programar en Java"
length() Número de caracteres 17
charAt(i) El carácter de la posición i (char) charAt(13) → 'J'
substring(i) Desde i hasta el final substring(13) → "Java"
substring(i, j) Desde i (incluido) hasta j (excluido) substring(0, 9) → "Programar"
indexOf(x) Primera posición de x (char o String), o -1 indexOf("en") → 10
indexOf(x, desde) Igual, empezando a buscar en desde indexOf('a', 6) → 7
lastIndexOf(x) Última posición de x, o -1 lastIndexOf('a') → 16
contains(t) ¿Contiene el texto t? contains("Java") → true
startsWith(t) / endsWith(t) ¿Empieza / termina por t? startsWith("Pro") → true
toUpperCase() / toLowerCase() Copia en mayúsculas / minúsculas "PROGRAMAR EN JAVA"
trim() Copia sin espacios al principio ni al final (solo ASCII) " hola " → "hola"
strip() (Java 11) Como trim, pero reconoce todos los espacios Unicode También stripLeading() y stripTrailing()
isEmpty() ¿Longitud 0? " ".isEmpty() → false
isBlank() (Java 11) ¿Vacía o solo espacios? " ".isBlank() → true
replace(a, b) Copia con todas las apariciones de a cambiadas por b replace('a', '4')
split(regex) Array de trozos separados por el patrón "a,b".split(",") → [a, b]
String.join(sep, partes) Une las partes con el separador join(" \| ", colores)
repeat(n) (Java 11) La cadena repetida n veces "ab".repeat(3) → "ababab"
chars() Los caracteres como flujo de enteros (stream) s.chars().filter(c -> c == 'a').count() → 4
toCharArray() Array char[] con los caracteres Útil para recorrer o ordenar

💡 Consejo: La regla «el inicio se incluye y el final se excluye» de substring(i, j) tiene una ventaja: la longitud del resultado es j - i. Esta misma convención la usan Arrays.copyOfRange, Random.nextInt(a, b) y muchos otros métodos de Java.

📘 Concepto: chars() devuelve un stream, y c -> c == 'a' es una expresión lambda. Son herramientas de programación funcional que estudiarás en el Curso de Java Avanzado; aquí solo las mencionamos para que las reconozcas.


4. Comparar cadenas

Ya lo adelantamos en el módulo 05: nunca compares el contenido de dos String con ==. Ahora que conoces el pool, entiendes el motivo: == compara referencias, y dos cadenas con el mismo texto pueden ser objetos distintos (o el mismo, si ambas vienen del pool, lo que hace que == «funcione» a veces y engañe).

Método Pregunta Resultado
a.equals(b) ¿Mismo texto? boolean
a.equalsIgnoreCase(b) ¿Mismo texto sin distinguir mayúsculas? boolean
a.compareTo(b) ¿Qué cadena va antes en orden lexicográfico? int: negativo, 0 o positivo
a.compareToIgnoreCase(b) Igual, sin distinguir mayúsculas int
a == b ¿Es el mismo objeto? boolean (casi nunca es lo que quieres)
public class Comparar {
    public static void main(String[] args) {
        String a = "casa", b = "Casa", c = "cosa";

        System.out.println("equals:            " + a.equals(b));
        System.out.println("equalsIgnoreCase:  " + a.equalsIgnoreCase(b));

        // compareTo: negativo si a va antes, 0 si son iguales, positivo si va después
        System.out.println("casa vs cosa:      " + a.compareTo(c));   // 'a' - 'o'
        System.out.println("cosa vs casa:      " + c.compareTo(a));
        System.out.println("casa vs casa:      " + a.compareTo("casa"));
        System.out.println("casa vs Casa:      " + a.compareTo(b));   // 'c' - 'C'
        System.out.println("ignorando mayúsc.: " + a.compareToIgnoreCase(b));
        System.out.println("casa vs casas:     " + a.compareTo("casas")); // más corta, antes

        if (a.compareTo(c) < 0) {
            System.out.println(a + " va antes que " + c);
        }
    }
}
equals:            false
equalsIgnoreCase:  true
casa vs cosa:      -14
cosa vs casa:      14
casa vs casa:      0
casa vs Casa:      32
ignorando mayúsc.: 0
casa vs casas:     -1
casa va antes que cosa

compareTo compara carácter a carácter hasta encontrar una diferencia y devuelve la resta de sus códigos ('a' - 'o' = 97 - 111 = -14). Si una cadena es el principio de la otra, devuelve la diferencia de longitudes. Lo único que debes mirar es el signo: a.compareTo(b) < 0 significa «a va antes que b».

⚠️ Importante: compareTo usa los códigos Unicode, así que las mayúsculas van antes que las minúsculas ("Z" < "a") y las letras con tilde y la ñ van después de la z. Para ordenar palabras en español correctamente existe la clase java.text.Collator, que verás en el curso avanzado.


5. Recorrer los caracteres y la clase Character

Hay dos formas de recorrer una cadena: con un for y charAt(i), o con un for-each sobre toCharArray(). Para clasificar cada carácter se usan los métodos static de la clase Character (módulo 07):

Método Pregunta / acción
Character.isLetter(c) ¿Es una letra? (incluye ñ y letras acentuadas)
Character.isDigit(c) ¿Es un dígito?
Character.isLetterOrDigit(c) ¿Letra o dígito?
Character.isUpperCase(c) / isLowerCase(c) ¿Mayúscula / minúscula?
Character.isWhitespace(c) ¿Espacio, tabulador o salto de línea?
Character.toUpperCase(c) / toLowerCase(c) Devuelve el carácter convertido
import java.util.Scanner;

public class Caracteres {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Escribe una frase: ");
        String frase = sc.nextLine();

        int letras = 0, mayusculas = 0, digitos = 0, espacios = 0, otros = 0;
        for (int i = 0; i < frase.length(); i++) {
            char c = frase.charAt(i);
            if (Character.isLetter(c)) {
                letras++;
                if (Character.isUpperCase(c)) {
                    mayusculas++;
                }
            } else if (Character.isDigit(c)) {
                digitos++;
            } else if (Character.isWhitespace(c)) {
                espacios++;
            } else {
                otros++;
            }
        }
        System.out.println("Letras: " + letras + " (mayúsculas: " + mayusculas + ")");
        System.out.println("Dígitos: " + digitos + ", espacios: " + espacios + ", otros: " + otros);

        // Recorrido con for-each sobre el array de caracteres
        StringBuilder alterno = new StringBuilder();
        boolean mayus = true;
        for (char c : frase.toCharArray()) {
            if (Character.isLetter(c)) {
                alterno.append(mayus ? Character.toUpperCase(c) : Character.toLowerCase(c));
                mayus = !mayus;
            } else {
                alterno.append(c);
            }
        }
        System.out.println("Alternando: " + alterno);

        // Aritmética con char: un char es un número (su código Unicode)
        char letra = 'C';
        System.out.println("Posición de 'C' en el alfabeto: " + (letra - 'A' + 1));
        System.out.println("Valor del dígito '7': " + ('7' - '0'));
        System.out.println("Siguiente letra: " + (char) (letra + 1));
    }
}
Escribe una frase: ¡Hola Mundo! Java 25 en 2026.
Letras: 15 (mayúsculas: 3)
Dígitos: 6, espacios: 5, otros: 3
Alternando: ¡HoLa MuNdO! jAvA 25 eN 2026.
Posición de 'C' en el alfabeto: 3
Valor del dígito '7': 7
Siguiente letra: D

Un char es en realidad un número (su código Unicode), así que se puede operar con él:

  • letra - 'A' + 1 da la posición de una mayúscula en el alfabeto inglés ('C' → 3).
  • '7' - '0' convierte un carácter dígito en su valor (7), porque los dígitos tienen códigos consecutivos.
  • (char) (letra + 1) da la letra siguiente: el cast es necesario porque letra + 1 es un int.

6. Conversión entre String y números

Lo que el usuario escribe, lo que se lee de un fichero o lo que llega por la red es siempre texto. Para operar con ello hay que convertirlo:

Conversión Método Ejemplo
String → int Integer.parseInt(s) Integer.parseInt("1234") → 1234
String → double Double.parseDouble(s) Double.parseDouble("3.75") → 3.75
String → long / boolean Long.parseLong(s) / Boolean.parseBoolean(s)  
String en otra base → int Integer.parseInt(s, base) Integer.parseInt("ff", 16) → 255
número → String String.valueOf(x) o Integer.toString(x) String.valueOf(3.5) → "3.5"
int → binario / hexadecimal Integer.toBinaryString(x) / Integer.toHexString(x) "1010", "ff"
char → String / String → char String.valueOf(c) / s.charAt(0)  
public class Conversiones {
    public static void main(String[] args) {
        // De String a número
        int n = Integer.parseInt("1234");
        double d = Double.parseDouble("3.75");
        long grande = Long.parseLong("9000000000");
        boolean b = Boolean.parseBoolean("TRUE");
        System.out.println(n + 1);           // ahora sí suma: 1235
        System.out.println(d * 2);
        System.out.println(grande + " " + b);
        System.out.println(Integer.parseInt("  42 ".strip()));   // quitar espacios antes
        System.out.println(Integer.parseInt("ff", 16));          // en base 16

        // De número a String
        String s1 = String.valueOf(3.5);
        String s2 = Integer.toString(255);
        String s3 = "" + 99;                  // funciona, pero es menos claro
        System.out.println(s1 + s2 + s3);     // concatenación de textos
        System.out.println(Integer.toBinaryString(10) + " " + Integer.toHexString(255));

        // char <-> String
        String deChar = String.valueOf('x');
        char deString = "xyz".charAt(0);
        System.out.println(deChar + deString);
    }
}
1235
7.5
9000000000 true
42
255
3.525599
1010 ff
xx

📘 Concepto: Integer, Double, Long, Boolean… son las clases envoltorio (wrapper classes) de los tipos primitivos. Además de estos métodos de conversión ofrecen constantes como Integer.MAX_VALUE. Las estudiarás en el módulo 11.

6.1. NumberFormatException

Si el texto no representa un número válido, parseInt lanza una excepción:

public class NumeroInvalido {
    public static void main(String[] args) {
        String texto = "12a";
        int n = Integer.parseInt(texto);
        System.out.println(n);
    }
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "12a"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at NumeroInvalido.main(NumeroInvalido.java:4)

En el módulo 15 aprenderás a capturar esta excepción con try/catch. Mientras tanto, puedes comprobar el texto antes de convertirlo, por ejemplo con una expresión regular (sección 9). Es una técnica muy útil para leer números sin que el programa se detenga por un error de tecleo: se lee la línea completa como texto y solo se convierte si es correcta.

import java.util.Scanner;

public class LeerConParse {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int edad = -1;
        while (edad < 0) {
            System.out.print("Edad: ");
            String linea = sc.nextLine().strip();
            if (linea.matches("\\d+")) {          // solo dígitos (sección 9)
                edad = Integer.parseInt(linea);
            } else {
                System.out.println("'" + linea + "' no es un número entero válido.");
            }
        }
        System.out.println("Dentro de 10 años tendrás " + (edad + 10));
    }
}
Edad: veinte
'veinte' no es un número entero válido.
Edad: 20 años
'20 años' no es un número entero válido.
Edad: 20
Dentro de 10 años tendrás 30

⚠️ Importante: Double.parseDouble usa siempre el punto como separador decimal, sin importar la configuración regional del sistema: Double.parseDouble("3,75") lanza NumberFormatException. Scanner.nextDouble(), en cambio, sí depende de la configuración regional (módulo 04).


7. StringBuilder: cadenas modificables

Como los String son inmutables, construir un texto a base de += dentro de un bucle crea un objeto nuevo en cada vuelta y copia todo el texto anterior. Para eso existe StringBuilder: un objeto que representa una cadena modificable, con un búfer interno que crece según hace falta.

Con String cada concatenación crea un objeto nuevo; con StringBuilder se añade al mismo búfer

Figura 4. Concatenar en un bucle: muchos objetos con String, uno solo con StringBuilder.

Método Acción
new StringBuilder() / new StringBuilder("texto") Crea uno vacío o con un texto inicial
append(x) Añade al final (cualquier tipo). Devuelve el propio StringBuilder, así que se puede encadenar
insert(i, x) Inserta en la posición i
delete(i, j) / deleteCharAt(i) Borra el tramo [i, j) / el carácter i
replace(i, j, texto) Sustituye el tramo [i, j)
setCharAt(i, c) Cambia un carácter
reverse() Invierte el contenido
length(), charAt(i), indexOf(t) Como en String
toString() Devuelve el String resultante
public class Constructor {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Java");
        sb.append(" es").append(" genial");        // encadenar llamadas
        System.out.println(sb);
        sb.insert(0, "¡");                         // insertar en una posición
        sb.append('!');
        System.out.println(sb + "  (longitud " + sb.length() + ")");
        sb.setCharAt(1, 'j');                      // cambiar un carácter
        System.out.println(sb);
        sb.deleteCharAt(0);                        // borrar un carácter
        sb.delete(4, 7);                           // borrar de 4 a 6
        System.out.println(sb);
        sb.replace(0, 4, "Kotlin");                // sustituir un tramo
        System.out.println(sb);
        System.out.println(sb.reverse());          // invertir (modifica sb)

        String resultado = sb.toString();          // convertir a String
        System.out.println(resultado.toUpperCase());
    }
}
Java es genial
¡Java es genial!  (longitud 16)
¡java es genial!
java genial!
Kotlin genial!
!laineg niltoK
!LAINEG NILTOK

7.1. ¿Cuánto se nota?

public class Rendimiento {
    public static void main(String[] args) {
        final int N = 20_000;

        long inicio = System.nanoTime();
        String s = "";
        for (int i = 0; i < N; i++) {
            s += "x";                       // crea un String nuevo en cada vuelta
        }
        long msString = (System.nanoTime() - inicio) / 1_000_000;

        inicio = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < N; i++) {
            sb.append("x");                 // modifica el mismo objeto
        }
        String t = sb.toString();
        long msBuilder = (System.nanoTime() - inicio) / 1_000_000;

        System.out.println("Con String y +=:   " + msString + " ms (" + s.length() + " caracteres)");
        System.out.println("Con StringBuilder: " + msBuilder + " ms (" + t.length() + " caracteres)");
    }
}

Una ejecución real (los tiempos dependen del ordenador, pero la proporción se mantiene):

Con String y +=:   511 ms (20000 caracteres)
Con StringBuilder: 11 ms (20000 caracteres)

Con += el tiempo crece con el cuadrado del número de vueltas (en la vuelta k hay que copiar k caracteres): O(n²). Con StringBuilder crece de forma lineal: O(n). Con el doble de vueltas, la versión con String tardaría unas cuatro veces más.

💡 Consejo: Usa + para concatenaciones sueltas ("Hola, " + nombre): es más legible y el compilador lo optimiza. Usa StringBuilder cuando construyas un texto dentro de un bucle. (Existe también StringBuffer, una versión antigua y más lenta pensada para varios hilos; en código nuevo usa StringBuilder.)


8. Text blocks y formato

8.1. Text blocks (Java 15)

Un text block es un literal de varias líneas delimitado por tres comillas """. Es ideal para JSON, HTML, SQL o menús, porque no hay que escribir \n ni escapar las comillas:

public class TextBlocks {
    public static void main(String[] args) {
        // Antes de Java 15
        String antiguo = "{\n" +
                "  \"nombre\": \"Ada\",\n" +
                "  \"lenguaje\": \"Java\"\n" +
                "}";
        // Con un text block
        String json = """
                {
                  "nombre": "Ada",
                  "lenguaje": "Java"
                }""";
        System.out.println(json);
        System.out.println("¿Iguales? " + antiguo.equals(json));

        // La sangría común se elimina; el cierre """ marca dónde empieza el texto
        String menu = """
                MENÚ
                  1. Nuevo
                  2. Abrir
                """;
        System.out.print(menu);

        // \ al final de línea: no inserta salto; formatted() rellena huecos
        String carta = """
                Estimado/a %s: \
                su pedido %d sale hoy.
                """.formatted("Ada", 1024);
        System.out.print(carta);
    }
}
{
  "nombre": "Ada",
  "lenguaje": "Java"
}
¿Iguales? true
MENÚ
  1. Nuevo
  2. Abrir
Estimado/a Ada: su pedido 1024 sale hoy.

Reglas:

  • Tras las """ de apertura debe haber un salto de línea; el texto empieza en la línea siguiente.
  • Java elimina la sangría común de todas las líneas. La posición de las """ de cierre también cuenta: si las pones en su propia línea, el texto termina con un salto de línea (como en menu); si las pones al final de la última línea, no (como en json).
  • \ al final de una línea evita el salto de línea; \s representa un espacio que no se elimina.

8.2. String.format y formatted

String.format funciona exactamente igual que printf (módulo 04), pero en lugar de escribir el resultado lo devuelve como String. Desde Java 15, formatted hace lo mismo llamado sobre la propia plantilla:

public class Formatear {
    public static void main(String[] args) {
        String nombre = "Ada";
        double saldo = 12345.678;
        int puntos = 1500000;

        String s1 = String.format("Cliente: %s, saldo: %.2f €", nombre, saldo);
        String s2 = "Cliente: %s, puntos: %,d".formatted(nombre, puntos);   // Java 15
        System.out.println(s1);
        System.out.println(s2);

        // Tablas alineadas: ancho fijo, - alinea a la izquierda
        System.out.println(String.format("|%-10s|%8s|", "Producto", "Precio"));
        System.out.println(String.format("|%-10s|%8.2f|", "Café", 1.5));
        System.out.println(String.format("|%-10s|%8.2f|", "Bocadillo", 4.25));
        System.out.println(String.format("Código: %05d", 42));       // relleno con ceros
        System.out.println(String.format("%s en mayúsculas: %S", nombre, nombre));
    }
}
Cliente: Ada, saldo: 12345.68 €
Cliente: Ada, puntos: 1,500,000
|Producto  |  Precio|
|Café      |    1.50|
|Bocadillo |    4.25|
Código: 00042
Ada en mayúsculas: ADA
Especificador Significado
%s / %S Texto / texto en mayúsculas
%d, %5d, %05d, %,d Entero; con ancho 5; relleno con ceros; con separador de miles
%.2f, %8.2f Decimal con 2 decimales; con ancho 8
%-10s Texto alineado a la izquierda en 10 posiciones
%c, %b, %n, %% Carácter, booleano, salto de línea, símbolo %

9. Expresiones regulares básicas

Una expresión regular (regex) es un patrón que describe un conjunto de cadenas: «ocho dígitos seguidos de una letra», «uno o más espacios», «algo, una arroba, algo, un punto y al menos dos letras». Se usan para validar, buscar y reemplazar texto.

Elemento Significado Ejemplo Encaja con
abc Esos caracteres literales hola hola
[abc] Uno de esos caracteres [aeiou] Una vocal
[a-z], [A-Z0-9] Un carácter del rango [0-9] Un dígito
[^abc] Cualquier carácter excepto esos [^0-9] Un no dígito
. Cualquier carácter a.c abc, a7c
\d / \D Un dígito / un no dígito \d\d 42
\w Letra (sin tildes), dígito o _ \w+ user_1
\s / \S Un espacio en blanco / un no espacio a\sb a b
X? X opcional (0 o 1 vez) colou?r color, colour
X* 0 o más veces ab* a, abbb
X+ 1 o más veces \d+ 7, 2026
X{n}, X{n,m}, X{n,} Exactamente n veces; entre n y m; al menos n \d{8} 12345678
(X\|Y) X o Y (grupo con alternativas) (0[1-9]\|1[0-2]) Un mes de 01 a 12

⚠️ Importante: En Java, la barra invertida \ también es el carácter de escape de los literales de texto. Por eso en el código hay que duplicarla: el patrón \d{8} se escribe "\\d{8}" y \s+ se escribe "\\s+".

Métodos de String que aceptan expresiones regulares:

Método Uso
s.matches(regex) ¿El texto completo encaja con el patrón?
s.replaceAll(regex, reemplazo) Sustituye todos los fragmentos que encajan
s.split(regex) Divide por los fragmentos que encajan
import java.util.Arrays;

public class Regex {
    public static void main(String[] args) {
        // matches: ¿el texto COMPLETO encaja con el patrón?
        String[] dnis = {"12345678Z", "1234567Z", "12345678z", "X1234567L"};
        for (String dni : dnis) {
            System.out.println(dni + " -> formato DNI: " + dni.matches("\\d{8}[A-Z]"));
        }

        String patronEmail = "[\\w.+-]+@[\\w-]+(\\.[\\w-]+)*\\.[a-zA-Z]{2,}";
        String[] emails = {"ada@ejemplo.com", "luis.perez@correo.edu.es", "mal@dominio", "@falta.com"};
        for (String e : emails) {
            System.out.println(e + " -> email: " + e.matches(patronEmail));
        }

        // Código postal español: provincias 01 a 52 + 3 dígitos
        String patronCP = "(0[1-9]|[1-4]\\d|5[0-2])\\d{3}";
        String[] cps = {"03001", "28080", "53000", "7080"};
        for (String cp : cps) {
            System.out.println(cp + " -> código postal: " + cp.matches(patronCP));
        }

        // replaceAll y split con expresiones regulares
        String sucio = "  Esto   tiene    demasiados   espacios ";
        System.out.println("[" + sucio.strip().replaceAll("\\s+", " ") + "]");
        System.out.println("[" + "Tel: 965-12-34-56".replaceAll("\\D", "") + "]");
        System.out.println(Arrays.toString("uno, dos;tres ,cuatro".split("\\s*[,;]\\s*")));
    }
}
12345678Z -> formato DNI: true
1234567Z -> formato DNI: false
12345678z -> formato DNI: false
X1234567L -> formato DNI: false
ada@ejemplo.com -> email: true
luis.perez@correo.edu.es -> email: true
mal@dominio -> email: false
@falta.com -> email: false
03001 -> código postal: true
28080 -> código postal: true
53000 -> código postal: false
7080 -> código postal: false
[Esto tiene demasiados espacios]
[965123456]
[uno, dos, tres, cuatro]

Cómo leer los patrones del ejemplo:

  • DNI: \d{8}[A-Z] → ocho dígitos y una letra mayúscula. Solo valida el formato; la letra debe comprobarse además con un cálculo (ejercicio 3).
  • Email: [\w.+-]+@[\w-]+(\.[\w-]+)*\.[a-zA-Z]{2,} → uno o más caracteres permitidos, @, un dominio, opcionalmente más subdominios y un punto con al menos dos letras. Validar un email de forma exhaustiva es muy complicado; este patrón sencillo detecta los errores típicos.
  • Código postal: (0[1-9]|[1-4]\d|5[0-2])\d{3} → los dos primeros dígitos son la provincia (de 01 a 52) y luego tres dígitos más.
  • replaceAll("\\s+", " ") sustituye cualquier grupo de espacios por uno solo, y replaceAll("\\D", "") borra todo lo que no sea un dígito.
  • split("\\s*[,;]\\s*") divide por comas o puntos y comas, con o sin espacios alrededor.

💡 Consejo: Para probar expresiones regulares mientras las escribes, IntelliJ IDEA permite comprobarlas directamente: sitúa el cursor sobre el patrón, pulsa Alt+Intro y elige Check RegExp. Para patrones complejos, Java ofrece las clases Pattern y Matcher del paquete java.util.regex.


10. Ejemplos completos

10.1. Palíndromos

Un palíndromo se lee igual de izquierda a derecha que de derecha a izquierda, sin tener en cuenta espacios, signos, mayúsculas ni tildes. Primero se normaliza el texto y después se compara con su inverso:

import java.util.Scanner;

public class Palindromo {

    static String normalizar(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toLowerCase().toCharArray()) {
            switch (c) {
                case 'á' -> c = 'a';
                case 'é' -> c = 'e';
                case 'í' -> c = 'i';
                case 'ó' -> c = 'o';
                case 'ú', 'ü' -> c = 'u';
                default -> { }
            }
            if (Character.isLetterOrDigit(c)) {     // descarta espacios y signos
                sb.append(c);
            }
        }
        return sb.toString();
    }

    static boolean esPalindromo(String s) {
        String limpio = normalizar(s);
        String alReves = new StringBuilder(limpio).reverse().toString();
        return limpio.equals(alReves);
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Frase: ");
        String frase = sc.nextLine();
        System.out.println("Normalizada: " + normalizar(frase));
        System.out.println(esPalindromo(frase) ? "Es un palíndromo" : "No es un palíndromo");
    }
}
Frase: Dábale arroz a la zorra el abad
Normalizada: dabalearrozalazorraelabad
Es un palíndromo

new StringBuilder(limpio).reverse().toString() es la forma habitual de invertir un String. El switch con flechas sobre la variable c sustituye las vocales acentuadas.

10.2. Contar vocales

import java.util.Scanner;

public class ContarVocales {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Texto: ");
        String texto = sc.nextLine().toLowerCase();

        int a = 0, e = 0, i = 0, o = 0, u = 0;
        for (int k = 0; k < texto.length(); k++) {
            switch (texto.charAt(k)) {
                case 'a', 'á' -> a++;
                case 'e', 'é' -> e++;
                case 'i', 'í' -> i++;
                case 'o', 'ó' -> o++;
                case 'u', 'ú', 'ü' -> u++;
                default -> { }                  // el resto de caracteres no cuenta
            }
        }
        System.out.printf("a: %d  e: %d  i: %d  o: %d  u: %d  -> total: %d%n",
                a, e, i, o, u, a + e + i + o + u);
    }
}
Texto: El murciélago es un animal único y curioso
a: 3  e: 3  i: 4  o: 4  u: 4  -> total: 18

10.3. Invertir palabras

public class InvertirPalabras {
    public static void main(String[] args) {
        String frase = "Java es un lenguaje divertido";

        // 1) Invertir el orden de las palabras
        String[] palabras = frase.split(" ");
        StringBuilder orden = new StringBuilder();
        for (int i = palabras.length - 1; i >= 0; i--) {
            orden.append(palabras[i]);
            if (i > 0) {
                orden.append(' ');
            }
        }
        System.out.println(orden);

        // 2) Invertir las letras de cada palabra, manteniendo el orden
        String[] invertidas = new String[palabras.length];
        for (int i = 0; i < palabras.length; i++) {
            invertidas[i] = new StringBuilder(palabras[i]).reverse().toString();
        }
        System.out.println(String.join(" ", invertidas));

        // 3) Invertir la frase entera
        System.out.println(new StringBuilder(frase).reverse());
    }
}
divertido lenguaje un es Java
avaJ se nu ejaugnel oditrevid
oditrevid ejaugnel nu se avaJ

10.4. Cifrado César

Julio César cifraba sus mensajes desplazando cada letra un número fijo de posiciones en el alfabeto: con desplazamiento 3, la A pasa a ser D, la B pasa a ser E… y la Z vuelve a empezar por la C. La aritmética de char y el operador % lo resuelven:

public class Cesar {

    static String cifrar(String texto, int desplazamiento) {
        int k = ((desplazamiento % 26) + 26) % 26;     // admite negativos
        StringBuilder sb = new StringBuilder();
        for (char c : texto.toCharArray()) {
            if (c >= 'A' && c <= 'Z') {
                sb.append((char) ('A' + (c - 'A' + k) % 26));
            } else if (c >= 'a' && c <= 'z') {
                sb.append((char) ('a' + (c - 'a' + k) % 26));
            } else {
                sb.append(c);                          // espacios, signos, ñ...
            }
        }
        return sb.toString();
    }

    static String descifrar(String texto, int desplazamiento) {
        return cifrar(texto, -desplazamiento);
    }

    public static void main(String[] args) {
        String mensaje = "Ataque al amanecer, Zona Norte";
        String secreto = cifrar(mensaje, 3);
        System.out.println("Original:   " + mensaje);
        System.out.println("Cifrado:    " + secreto);
        System.out.println("Descifrado: " + descifrar(secreto, 3));
    }
}
Original:   Ataque al amanecer, Zona Norte
Cifrado:    Dwdtxh do dpdqhfhu, Crqd Qruwh
Descifrado: Ataque al amanecer, Zona Norte
  • c - 'A' convierte la letra en su posición (0 a 25), se suma el desplazamiento, % 26 hace que dé la vuelta y 'A' + ... la convierte de nuevo en letra.
  • ((desplazamiento % 26) + 26) % 26 normaliza el desplazamiento a un valor entre 0 y 25, incluso si es negativo. Así, descifrar es cifrar con el desplazamiento opuesto.
  • Este cifrado solo trata las 26 letras del alfabeto inglés; la ñ y las vocales acentuadas se dejan como están.

10.5. Validador de contraseñas

import java.util.Scanner;

public class Password {

    static String problemas(String p) {
        boolean mayus = false, minus = false, digito = false, especial = false;
        for (char c : p.toCharArray()) {
            if (Character.isUpperCase(c)) mayus = true;
            else if (Character.isLowerCase(c)) minus = true;
            else if (Character.isDigit(c)) digito = true;
            else if (!Character.isWhitespace(c)) especial = true;
        }
        StringBuilder sb = new StringBuilder();
        if (p.length() < 8) sb.append("\n - Debe tener al menos 8 caracteres");
        if (!mayus) sb.append("\n - Falta una mayúscula");
        if (!minus) sb.append("\n - Falta una minúscula");
        if (!digito) sb.append("\n - Falta un dígito");
        if (!especial) sb.append("\n - Falta un carácter especial (como ! o #)");
        if (p.contains(" ")) sb.append("\n - No puede contener espacios");
        return sb.toString();
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String errores;
        do {
            System.out.print("Nueva contraseña: ");
            errores = problemas(sc.nextLine());
            if (!errores.isEmpty()) {
                System.out.println("Contraseña débil:" + errores);
            }
        } while (!errores.isEmpty());
        System.out.println("Contraseña aceptada.");
    }
}
Nueva contraseña: hola
Contraseña débil:
 - Debe tener al menos 8 caracteres
 - Falta una mayúscula
 - Falta un dígito
 - Falta un carácter especial (como ! o #)
Nueva contraseña: holamundo2026
Contraseña débil:
 - Falta una mayúscula
 - Falta un carácter especial (como ! o #)
Nueva contraseña: Hola Mundo 2026!
Contraseña débil:
 - No puede contener espacios
Nueva contraseña: HolaMundo2026!
Contraseña aceptada.

El método problemas devuelve un texto con todos los fallos (vacío si no hay ninguno), así el usuario sabe qué corregir. Los if sin llaves de una línea son una excepción aceptable cuando todos tienen la misma forma y caben en una línea, pero no abuses.

10.6. Anagramas

Dos palabras son anagramas si tienen exactamente las mismas letras en distinto orden. Truco: si se ordenan las letras de las dos, deben quedar iguales.

import java.util.Arrays;

public class Anagramas {

    static boolean sonAnagramas(String a, String b) {
        char[] x = a.replace(" ", "").toLowerCase().toCharArray();
        char[] y = b.replace(" ", "").toLowerCase().toCharArray();
        Arrays.sort(x);                      // mismas letras -> mismos arrays ordenados
        Arrays.sort(y);
        return Arrays.equals(x, y);
    }

    public static void main(String[] args) {
        System.out.println("Roma / amor: " + sonAnagramas("Roma", "amor"));
        System.out.println("Listen / Silent: " + sonAnagramas("Listen", "Silent"));
        System.out.println("Delira / Lidera: " + sonAnagramas("Delira", "Lidera"));
        System.out.println("Java / Ajo: " + sonAnagramas("Java", "Ajo"));
    }
}
Roma / amor: true
Listen / Silent: true
Delira / Lidera: true
Java / Ajo: false

11. Ejercicios

Los seis primeros están resueltos; intenta hacerlos antes de mirar la solución.

Ejercicio 1: Contar palabras

Escribe un método que cuente las palabras de un texto, teniendo en cuenta que puede haber varios espacios seguidos y espacios al principio y al final.

Ver solución
import java.util.Scanner;

public class ContarPalabras {

    static int contarPalabras(String texto) {
        String limpio = texto.strip();
        if (limpio.isEmpty()) {
            return 0;                         // "".split(...) daría 1 elemento
        }
        return limpio.split("\\s+").length;   // uno o más espacios como separador
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Texto: ");
        String texto = sc.nextLine();
        System.out.println("Palabras: " + contarPalabras(texto));
        System.out.println("Texto vacío: " + contarPalabras("   "));
    }
}
Texto:   Hay   que   contar    bien  las palabras
Palabras: 6
Texto vacío: 0

split("\\s+") usa como separador uno o más espacios. strip() previo es necesario porque un espacio al principio produciría un primer elemento vacío. Y el caso del texto vacío hay que tratarlo aparte: "".split("\\s+") devuelve un array con un elemento (la cadena vacía), no un array vacío.

Ejercicio 2: Capitalizar

Escribe un método que ponga en mayúscula la primera letra de cada palabra y en minúscula el resto.

Ver solución
public class Capitalizar {

    static String capitalizar(String frase) {
        String[] palabras = frase.strip().toLowerCase().split("\\s+");
        StringBuilder sb = new StringBuilder();
        for (String p : palabras) {
            if (sb.length() > 0) {
                sb.append(' ');
            }
            sb.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(capitalizar("el   SEÑOR de los anillos"));
        System.out.println(capitalizar("java"));
    }
}
El Señor De Los Anillos
Java

Para cada palabra se toma charAt(0) en mayúscula y se le añade el resto (substring(1)), que ya está en minúsculas porque se convirtió todo el texto al principio. El if (sb.length() > 0) pone un espacio entre palabras, pero no antes de la primera.

Ejercicio 3: Letra del DNI

La letra del DNI español se obtiene dividiendo el número entre 23: el resto (0 a 22) indica la posición de la letra en la cadena "TRWAGMYFPDXBNJZSQVHLCKE". Escribe un método que calcule la letra y otro que valide un DNI completo (formato y letra).

Ver solución
public class LetraDni {

    static final String LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE";

    static char letraDni(int numero) {
        return LETRAS.charAt(numero % 23);
    }

    static boolean dniValido(String dni) {
        dni = dni.strip().toUpperCase();
        if (!dni.matches("\\d{8}[A-Z]")) {
            return false;
        }
        int numero = Integer.parseInt(dni.substring(0, 8));
        return dni.charAt(8) == letraDni(numero);
    }

    public static void main(String[] args) {
        System.out.println("Letra de 12345678: " + letraDni(12345678));
        String[] pruebas = {"12345678Z", "12345678A", "00000000t", "1234"};
        for (String d : pruebas) {
            System.out.println(d + " -> " + (dniValido(d) ? "válido" : "no válido"));
        }
    }
}
Letra de 12345678: Z
12345678Z -> válido
12345678A -> no válido
00000000t -> válido
1234 -> no válido

La cadena LETRAS funciona como una tabla: charAt(resto) sustituye a un switch de 23 casos. La validación combina las tres herramientas del módulo: una expresión regular para el formato, substring y parseInt para extraer el número, y la comparación de char con == (que sí es correcta, porque char es un tipo primitivo). Los NIE de extranjeros (que empiezan por X, Y o Z) siguen la misma regla sustituyendo esa letra por 0, 1 o 2.

Ejercicio 4: Compresión RLE

Escribe un método que comprima una cadena sustituyendo cada tramo de caracteres repetidos por el carácter y el número de repeticiones: "aaabccdddd" → "a3b1c2d4". Es una versión sencilla del algoritmo Run-Length Encoding.

Ver solución
public class Comprimir {

    static String comprimir(String s) {
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < s.length()) {
            char c = s.charAt(i);
            int repeticiones = 0;
            while (i < s.length() && s.charAt(i) == c) {   // contar el tramo
                repeticiones++;
                i++;
            }
            sb.append(c).append(repeticiones);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(comprimir("aaabccdddd"));
        System.out.println(comprimir("xyz"));
        System.out.println(comprimir("bbbbbbbbbbbb"));
    }
}
a3b1c2d4
x1y1z1
b12

El bucle interno avanza i mientras el carácter se repita, así que el externo no necesita incrementarlo. Con un StringBuilder cada tramo se añade sin crear objetos intermedios. Observa que con "xyz" el resultado es más largo que el original: la compresión RLE solo compensa con muchas repeticiones.

Ejercicio 5: Ocurrencias de una subcadena

Escribe un método que cuente cuántas veces aparece una subcadena dentro de un texto usando indexOf.

Ver solución
public class Ocurrencias {

    static int contar(String texto, String buscado) {
        int veces = 0;
        int pos = texto.indexOf(buscado);
        while (pos != -1) {
            veces++;
            pos = texto.indexOf(buscado, pos + 1);   // buscar a partir de la siguiente
        }
        return veces;
    }

    public static void main(String[] args) {
        String texto = "el perro de san roque no tiene rabo porque ramón ramírez se lo ha robado";
        System.out.println("'ra': " + contar(texto, "ra"));
        System.out.println("'o': " + contar(texto, "o"));
        System.out.println("'aa' en 'aaaa': " + contar("aaaa", "aa"));
    }
}
'ra': 3
'o': 8
'aa' en 'aaaa': 3

indexOf(buscado, pos + 1) sigue buscando justo después de la última aparición encontrada. Así se cuentan también las apariciones solapadas: "aa" aparece 3 veces en "aaaa" (posiciones 0, 1 y 2). Si no quisieras contarlas, avanzarías con pos + buscado.length().

Ejercicio 6: Números romanos

Escribe un método que convierta un número romano en decimal. Recuerda que un símbolo se resta si va delante de otro de mayor valor (IV = 4, XC = 90, CM = 900).

Ver solución
public class Romanos {

    static int valor(char c) {
        return switch (c) {
            case 'I' -> 1;
            case 'V' -> 5;
            case 'X' -> 10;
            case 'L' -> 50;
            case 'C' -> 100;
            case 'D' -> 500;
            case 'M' -> 1000;
            default -> 0;
        };
    }

    static int romanoADecimal(String romano) {
        int total = 0;
        for (int i = 0; i < romano.length(); i++) {
            int actual = valor(romano.charAt(i));
            int siguiente = (i + 1 < romano.length()) ? valor(romano.charAt(i + 1)) : 0;
            if (actual < siguiente) {
                total -= actual;          // IV, IX, XL...: se resta
            } else {
                total += actual;
            }
        }
        return total;
    }

    public static void main(String[] args) {
        String[] pruebas = {"XIV", "MCMXCIX", "MMXXVI", "CDXLIV"};
        for (String r : pruebas) {
            System.out.println(r + " = " + romanoADecimal(r));
        }
    }
}
XIV = 14
MCMXCIX = 1999
MMXXVI = 2026
CDXLIV = 444

Se recorren los símbolos de izquierda a derecha: si el valor del símbolo actual es menor que el del siguiente, se resta; si no, se suma. En MCMXCIX: M (+1000), C (−100, va delante de M), M (+1000), X (−10), C (+100), I (−1), X (+10) = 1999. La expresión switch del método valor es la tabla de símbolos.

Ejercicio 7: Quitar las vocales (propuesto)

Escribe un método que devuelva una cadena sin vocales (con o sin tilde, en mayúscula o minúscula). Hazlo de dos formas: recorriendo los caracteres con un StringBuilder y con una única llamada a replaceAll.

Ejercicio 8: Pangramas (propuesto)

Un pangrama es una frase que contiene todas las letras del alfabeto. Escribe un método boolean esPangrama(String frase) para el alfabeto inglés (26 letras), usando un array de 26 boolean. Prueba con “The quick brown fox jumps over the lazy dog”.

Ejercicio 9: Frecuencia de letras (propuesto)

Lee un texto y muestra cuántas veces aparece cada letra (de la a a la z, sin distinguir mayúsculas), ordenadas de la más frecuente a la menos frecuente, ignorando las que no aparecen. Usa un array de 26 contadores indexado con c - 'a'.

Ejercicio 10: Matrículas (propuesto)

Las matrículas españolas actuales tienen cuatro dígitos y tres consonantes en mayúscula, sin vocales y sin las letras Ñ ni Q (por ejemplo, 1234 BCD). Escribe una expresión regular que las valide, admitiendo que el espacio intermedio sea opcional, y pruébala con casos válidos e inválidos.

Ejercicio 11: De decimal a romano (propuesto)

Escribe el método inverso del ejercicio 6: String decimalARomano(int n) para números entre 1 y 3999. Pista: usa dos arrays paralelos con los valores {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} y sus símbolos, y un StringBuilder.

Ejercicio 12: Cifrado Vigenère (propuesto)

El cifrado Vigenère mejora el de César usando una palabra clave: cada letra del mensaje se desplaza según la letra correspondiente de la clave (A = 0, B = 1…), que se repite tantas veces como haga falta. Con la clave LIMON, el mensaje ATAQUE se cifra como LBMEHP. Escribe los métodos para cifrar y descifrar, reutilizando la idea del método cifrar de César.


Resumen

Concepto Sintaxis / idea clave
Inmutabilidad Los métodos de String devuelven un objeto nuevo: s = s.toUpperCase();
String pool Los literales iguales comparten objeto; new String(...) crea otro
Concatenación + de izquierda a derecha: 1 + 2 + "3" → "33"
Métodos básicos length(), charAt(i), substring(i, j), indexOf, contains, replace, split, strip, repeat
Comparar equals, equalsIgnoreCase, compareTo (mirar el signo); nunca ==
Recorrer for con charAt(i) o for-each sobre toCharArray()
Character isLetter, isDigit, isUpperCase, isWhitespace, toUpperCase
Conversiones Integer.parseInt, Double.parseDouble, String.valueOf; texto inválido → NumberFormatException
StringBuilder append, insert, delete, reverse, toString; para construir texto en bucles
Text blocks (Java 15) """ + salto de línea; se elimina la sangría común
Formato String.format("%-10s %8.2f", ...) o "...".formatted(...)
Expresiones regulares matches, replaceAll, split; \d, \w, \s, [...], +, *, ?, {n}; \\ en Java