Strings en Java

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 22 min de lectura ✓ Nivel principiante ★ ★ ★ ★ ★ (5/5)

📝 ¿Qué es un String en Java?

En Java, un String es un objeto que representa una secuencia de caracteres. A diferencia de los tipos primitivos como int o double, String es una clase definida en el paquete java.lang, lo que significa que cada cadena de texto es un objeto con métodos y propiedades. Sin embargo, Java ofrece una sintaxis especial que permite trabajar con Strings casi como si fueran tipos primitivos, creándolos directamente con comillas dobles.

La clase String es una de las más utilizadas en cualquier programa Java. Desde la lectura de datos introducidos por el usuario hasta la construcción de consultas SQL, la manipulación de URLs o el formateo de mensajes de salida, los Strings están presentes en prácticamente todas las aplicaciones. Comprender cómo funcionan internamente —su inmutabilidad, el String Pool y las diferencias entre equals() y ==— es fundamental para escribir código eficiente y libre de errores.

💡 Dato clave: La clase String implementa las interfaces Serializable, Comparable<String> y CharSequence. Esto le permite ser serializada, ordenada naturalmente y utilizada de forma intercambiable con StringBuilder y StringBuffer a través de la interfaz común CharSequence.

🔧 Formas de crear un String

Java ofrece varias maneras de crear objetos String. Las dos principales son el literal de cadena (con comillas dobles) y el constructor new String(). Aunque ambas producen un String funcional, su comportamiento respecto a la memoria es muy diferente.

▶️ Mediante literal de cadena

Es la forma más habitual y recomendada. El compilador almacena el String en el String Pool, una zona especial de memoria que permite reutilizar cadenas idénticas:

Creación con literal
String saludo = "Hola, mundo";
String otroSaludo = "Hola, mundo";

// Ambas variables apuntan al MISMO objeto en el String Pool
System.out.println(saludo == otroSaludo); // true

▶️ Mediante el constructor new

Con new String() se crea siempre un objeto nuevo en el heap, aunque exista un String idéntico en el Pool:

Creación con new
String s1 = new String("Hola, mundo");
String s2 = new String("Hola, mundo");

// Son objetos DIFERENTES en memoria, aunque tengan el mismo contenido
System.out.println(s1 == s2);      // false
System.out.println(s1.equals(s2)); // true

▶️ Desde un array de caracteres

Creación desde char[]
char[] letras = {'J', 'a', 'v', 'a'};
String nombre = new String(letras);
System.out.println(nombre); // Java

// También se puede crear desde una porción del array
String sub = new String(letras, 0, 2); // "Ja"

▶️ Desde un array de bytes

Creación desde byte[]
byte[] datos = {72, 111, 108, 97};
String texto = new String(datos, java.nio.charset.StandardCharsets.UTF_8);
System.out.println(texto); // Hola
✅ Buena práctica: Utiliza siempre literales de cadena salvo que necesites explícitamente un objeto nuevo (caso muy raro). Los literales aprovechan el String Pool y ahorran memoria.

🏊 El String Pool de Java

El String Pool (también conocido como intern pool o String constant pool) es una estructura de datos especial dentro del heap de la JVM que almacena referencias a objetos String. Su objetivo principal es ahorrar memoria reutilizando cadenas con contenido idéntico en lugar de crear copias independientes.

Cuando el compilador Java encuentra un literal de cadena en el código fuente, comprueba si ya existe un String con el mismo contenido en el Pool. Si existe, reutiliza esa referencia. Si no existe, crea un nuevo objeto String y lo almacena en el Pool. Este mecanismo es posible gracias a la inmutabilidad de los Strings: al no poder modificarse, es seguro compartir la misma instancia entre múltiples variables.

🔹 El método intern()

Los Strings creados con new String() no se añaden automáticamente al Pool. Sin embargo, se puede forzar su inclusión mediante el método intern():

Uso de intern()
String s1 = "Java";
String s2 = new String("Java");
String s3 = s2.intern();

System.out.println(s1 == s2); // false — s2 está fuera del Pool
System.out.println(s1 == s3); // true  — intern() devuelve la referencia del Pool
⚠️ Precaución: El uso excesivo de intern() puede causar problemas de rendimiento, ya que el Pool utiliza una tabla hash interna con tamaño fijo. En aplicaciones con millones de cadenas distintas, es preferible gestionar la deduplicación con estructuras de datos propias (como un HashSet<String>).

🔒 Inmutabilidad de los Strings

Los objetos String en Java son inmutables: una vez creado un String, su contenido no puede cambiar. Cualquier operación que aparentemente modifique un String (como toUpperCase(), concat() o replace()) en realidad crea y devuelve un nuevo objeto String, dejando el original intacto.

Demostración de inmutabilidad
String original = "Hola";
String modificado = original.toUpperCase();

System.out.println(original);   // Hola    (NO ha cambiado)
System.out.println(modificado); // HOLA    (nuevo objeto)
System.out.println(original == modificado); // false

🔹 ¿Por qué son inmutables?

La inmutabilidad de String es una decisión de diseño deliberada del lenguaje Java, motivada por tres razones fundamentales:

Razón Explicación
Seguridad Los Strings se usan como parámetros en conexiones de red, carga de clases y apertura de archivos. Si fueran mutables, código malicioso podría alterar una ruta o una URL después de la validación de seguridad.
Rendimiento La inmutabilidad permite el String Pool (compartir instancias) y la caché del hashCode (se calcula una sola vez y se reutiliza), lo que mejora el rendimiento en HashMap y HashSet.
Thread-safety Al no poder modificarse, los Strings son inherentemente seguros entre hilos. Múltiples threads pueden leer el mismo String sin necesidad de sincronización.

⚙️ Métodos principales de String

La clase String ofrece más de 60 métodos. A continuación se presentan los más utilizados, organizados por categorías, con ejemplos de uso para cada uno.

🔹 Métodos de consulta

Método Descripción Ejemplo Resultado
length() Devuelve la longitud de la cadena "Java".length() 4
charAt(int) Devuelve el carácter en la posición indicada "Java".charAt(0) 'J'
isEmpty() Comprueba si la cadena tiene longitud 0 "".isEmpty() true
isBlank() Comprueba si está vacía o solo contiene espacios (Java 11+) " ".isBlank() true
contains(CharSequence) Comprueba si contiene la secuencia indicada "Hola mundo".contains("mundo") true
indexOf(String) Posición de la primera aparición (-1 si no existe) "Java es genial".indexOf("es") 5
lastIndexOf(String) Posición de la última aparición "abcabc".lastIndexOf("abc") 3
startsWith(String) Comprueba si empieza con el prefijo dado "Java".startsWith("Ja") true
endsWith(String) Comprueba si termina con el sufijo dado "archivo.txt".endsWith(".txt") true

🔹 Métodos de transformación

Método Descripción Ejemplo Resultado
toUpperCase() Convierte a mayúsculas "hola".toUpperCase() "HOLA"
toLowerCase() Convierte a minúsculas "JAVA".toLowerCase() "java"
trim() Elimina espacios al inicio y al final " hola ".trim() "hola"
strip() Como trim() pero reconoce todos los espacios Unicode (Java 11+) "\u2000hola\u2000".strip() "hola"
replace(old, new) Reemplaza todas las apariciones de una secuencia "aaa".replace("a", "b") "bbb"
substring(begin, end) Extrae una subcadena (begin incluido, end excluido) "Hola mundo".substring(5, 10) "mundo"
repeat(int) Repite la cadena n veces (Java 11+) "ab".repeat(3) "ababab"

🔹 Métodos de división y unión

split() y String.join()
// split(): divide un String en un array usando un delimitador (regex)
String csv = "Java,Python,C++,JavaScript";
String[] lenguajes = csv.split(",");
// lenguajes = ["Java", "Python", "C++", "JavaScript"]

for (String lang : lenguajes) {
    System.out.println(lang);
}

// String.join(): une elementos con un delimitador
String unido = String.join(" - ", lenguajes);
System.out.println(unido); // Java - Python - C++ - JavaScript

// split() con límite
String datos = "nombre:edad:ciudad:país";
String[] partes = datos.split(":", 3);
// partes = ["nombre", "edad", "ciudad:país"] — solo 3 partes

🔹 Métodos de conversión a arrays

toCharArray() y getBytes()
String texto = "Java";

// Convertir a array de caracteres
char[] caracteres = texto.toCharArray();
// caracteres = ['J', 'a', 'v', 'a']

// Convertir a array de bytes (UTF-8)
byte[] bytes = texto.getBytes(java.nio.charset.StandardCharsets.UTF_8);
// bytes = [74, 97, 118, 97]

⚖️ Comparar Strings: equals vs ==

Este es uno de los puntos que más errores causa entre programadores que empiezan con Java. Comprender la diferencia entre == y equals() es absolutamente esencial para evitar bugs difíciles de diagnosticar.

Operador/Método Qué compara Uso correcto
== Las referencias (si apuntan al mismo objeto en memoria) Comparar con null o verificar identidad de objeto
equals() El contenido (carácter a carácter) Comparar si dos Strings tienen el mismo texto
equalsIgnoreCase() El contenido ignorando mayúsculas/minúsculas Comparaciones insensibles a capitalización
compareTo() El orden lexicográfico (devuelve int) Ordenación de Strings
Diferencia entre == y equals()
String a = "Hola";
String b = "Hola";
String c = new String("Hola");

// Con literales: == funciona (mismo objeto en el Pool)
System.out.println(a == b);       // true
System.out.println(a.equals(b));  // true

// Con new: == falla (objetos diferentes)
System.out.println(a == c);       // false  ← ¡CUIDADO!
System.out.println(a.equals(c));  // true   ← Correcto

// Comparación ignorando mayúsculas
System.out.println("hola".equalsIgnoreCase("HOLA")); // true

// Orden lexicográfico
System.out.println("abc".compareTo("abd")); // -1 (c < d)
System.out.println("abc".compareTo("abc")); //  0 (iguales)
System.out.println("abd".compareTo("abc")); //  1 (d > c)
⚠️ Error clásico: Usar == para comparar el contenido de Strings es el error más frecuente de principiantes en Java. Funciona a veces (cuando ambos son literales del Pool) y falla en otros casos, lo que lo convierte en un bug intermitente muy difícil de detectar. Regla de oro: siempre usa equals() para comparar Strings.
✅ Patrón defensivo: Para evitar NullPointerException, compara el literal primero: "esperado".equals(variable) en lugar de variable.equals("esperado"). Si variable es null, la primera forma devuelve false; la segunda lanza una excepción.

🔗 Concatenación de Strings

La concatenación es la operación de unir dos o más cadenas para formar una nueva. Java ofrece varios mecanismos para concatenar Strings, cada uno con sus ventajas según el contexto.

🔹 Operador +

La forma más intuitiva. El compilador traduce internamente las concatenaciones con + a llamadas a StringBuilder (desde Java 5), por lo que para pocas operaciones fuera de bucles es perfectamente eficiente:

Concatenación con +
String nombre = "María";
int edad = 28;

// Concatenación simple — eficiente fuera de bucles
String mensaje = "Hola, " + nombre + ". Tienes " + edad + " años.";
System.out.println(mensaje);
// Salida: Hola, María. Tienes 28 años.

🔹 Método concat()

Método concat()
String saludo = "Hola".concat(", ").concat("mundo");
System.out.println(saludo); // Hola, mundo

// Nota: concat() solo acepta String. No convierte tipos automáticamente.
// Esto NO compila: "Edad: ".concat(28);
// Sí compila: "Edad: ".concat(String.valueOf(28));

🔹 Peligro: concatenación en bucles

❌ Mal: concatenación en bucle con +
// ❌ INEFICIENTE: crea un nuevo String en cada iteración → O(n²)
String resultado = "";
for (int i = 0; i < 10000; i++) {
    resultado += i + ", ";  // Cada += crea un objeto nuevo
}

// ✅ EFICIENTE: usar StringBuilder → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(", ");
}
String resultado2 = sb.toString();

🏗️ StringBuilder y StringBuffer

Cuando se necesita construir o modificar cadenas de forma intensiva, Java ofrece dos clases mutables: StringBuilder y StringBuffer. A diferencia de String, estas clases permiten modificar el contenido interno sin crear objetos nuevos, lo que resulta mucho más eficiente para operaciones de concatenación repetida.

Característica String StringBuilder StringBuffer
Mutabilidad Inmutable Mutable Mutable
Thread-safe Sí (inmutable) No Sí (synchronized)
Rendimiento Bajo en concatenación Alto Medio (sincronización)
Desde Java 1.0 Java 1.5 Java 1.0
Caso de uso Cadenas constantes Concatenación en un hilo Concatenación multihilo

🔹 Métodos principales de StringBuilder

StringBuilder en acción
StringBuilder sb = new StringBuilder("Hola");

// append(): añade al final
sb.append(" mundo");        // "Hola mundo"

// insert(): inserta en una posición
sb.insert(5, "hermoso ");   // "Hola hermoso mundo"

// replace(): reemplaza un rango
sb.replace(5, 13, "bello"); // "Hola bello mundo"

// delete(): elimina un rango
sb.delete(5, 11);           // "Hola mundo"

// reverse(): invierte la cadena
sb.reverse();               // "odnum aloH"

// toString(): convierte a String inmutable
String resultado = sb.toString();

// Encadenamiento de métodos (method chaining)
String html = new StringBuilder()
    .append("<div>")
    .append("<p>Contenido</p>")
    .append("</div>")
    .toString();
💡 Capacidad inicial: Por defecto, StringBuilder se crea con una capacidad de 16 caracteres. Si sabes de antemano el tamaño aproximado de la cadena resultante, es recomendable indicarlo en el constructor: new StringBuilder(1024). Esto evita redimensionamientos internos del buffer.

🎨 Formateo de Strings

Java ofrece varias formas de crear cadenas con formato. El método más potente es String.format(), que funciona de manera similar a printf() en C.

🔹 String.format()

Formateo con String.format()
String nombre = "Ana";
int edad = 25;
double salario = 2850.5;

// %s = String, %d = entero, %f = decimal
String info = String.format("Nombre: %s, Edad: %d, Salario: %.2f€", nombre, edad, salario);
System.out.println(info);
// Salida: Nombre: Ana, Edad: 25, Salario: 2850,50€

// Alineación y ancho
System.out.println(String.format("|%-15s|%5d|", "Producto", 42));
// Salida: |Producto       |   42|

// Relleno con ceros
System.out.println(String.format("Código: %05d", 42));
// Salida: Código: 00042

🔹 Especificadores de formato más usados

Especificador Tipo Ejemplo Resultado
%s String String.format("%s", "Hola") Hola
%d Entero String.format("%d", 42) 42
%f Decimal String.format("%.2f", 3.14159) 3.14
%b Booleano String.format("%b", true) true
%c Carácter String.format("%c", 'A') A
%n Salto de línea String.format("Línea 1%nLínea 2") (salto de línea del SO)

🔹 Text Blocks (Java 13+)

Desde Java 13 (estables en Java 15), los Text Blocks permiten escribir Strings multilínea de forma legible, delimitados por triple comilla """:

Text Blocks (Java 15+)
// Antes de Java 13
String json = "{\n" +
              "  \"nombre\": \"Ana\",\n" +
              "  \"edad\": 25\n" +
              "}";

// Con Text Blocks (Java 15+)
String jsonModerno = """
        {
          "nombre": "Ana",
          "edad": 25
        }
        """;

// Se pueden combinar con format
String plantilla = """
        Estimado/a %s:
        Su pedido #%d ha sido confirmado.
        Total: %.2f€
        """.formatted("Carlos", 1234, 99.95);

🔄 Conversión entre tipos y String

Convertir entre tipos primitivos (o sus wrappers) y String es una operación constante en la programación Java. Es fundamental conocer los métodos correctos para cada dirección de conversión.

🔹 De tipo primitivo a String

Primitivo → String
int numero = 42;
double decimal = 3.14;
boolean flag = true;

// Método 1: String.valueOf() — el más recomendado
String s1 = String.valueOf(numero);   // "42"
String s2 = String.valueOf(decimal);  // "3.14"
String s3 = String.valueOf(flag);     // "true"

// Método 2: concatenación con cadena vacía
String s4 = "" + numero;  // "42" — funcional pero menos explícito

// Método 3: métodos de las clases wrapper
String s5 = Integer.toString(numero);        // "42"
String s6 = Integer.toString(numero, 16);    // "2a" (hexadecimal)
String s7 = Integer.toBinaryString(numero);  // "101010" (binario)

🔹 De String a tipo primitivo

String → Primitivo
// Conversión con control de errores
try {
    int n = Integer.parseInt("42");           // 42
    double d = Double.parseDouble("3.14");    // 3.14
    long l = Long.parseLong("1000000");       // 1000000
    boolean b = Boolean.parseBoolean("true"); // true
    
    // Nota: parseInt lanza NumberFormatException si la cadena no es válida
    int error = Integer.parseInt("abc"); // ¡NumberFormatException!
    
} catch (NumberFormatException e) {
    System.err.println("Error de conversión: " + e.getMessage());
}

🔍 Strings y expresiones regulares

La clase String integra varios métodos que utilizan expresiones regulares (regex) internamente, ofreciendo capacidades avanzadas de búsqueda, validación y transformación de texto sin necesidad de usar directamente las clases Pattern y Matcher.

Métodos con regex en String
String texto = "Mi teléfono es 612-345-678 y el de casa 914-222-333";

// matches(): comprueba si el String COMPLETO encaja con el patrón
System.out.println("12345".matches("\\d+")); // true (solo dígitos)
System.out.println("abc12".matches("\\d+")); // false

// replaceAll(): reemplaza TODAS las coincidencias
String sinNumeros = texto.replaceAll("\\d", "*");
System.out.println(sinNumeros);
// Mi teléfono es ***-***-*** y el de casa ***-***-***

// replaceFirst(): reemplaza solo la primera coincidencia
String primerReemplazo = texto.replaceFirst("\\d{3}-\\d{3}-\\d{3}", "[CENSURADO]");
System.out.println(primerReemplazo);
// Mi teléfono es [CENSURADO] y el de casa 914-222-333

// split() con regex
String datos = "nombre:  edad ;  ciudad";
String[] campos = datos.split("[;:]\\s*");
// campos = ["nombre", "edad", "ciudad"]

// Validación de email (patrón simplificado)
String email = "usuario@dominio.com";
boolean esValido = email.matches("[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}");
System.out.println(esValido); // true
⚠️ Rendimiento: Los métodos matches(), replaceAll() y split() compilan la expresión regular internamente en cada llamada. Si se van a usar repetidamente con el mismo patrón (por ejemplo dentro de un bucle), es más eficiente precompilar el patrón con Pattern.compile() y reutilizar el objeto Pattern.

🏢 Ejemplo integrador: validador de formularios

El siguiente ejemplo reúne la mayoría de conceptos vistos en el artículo para construir un validador de datos de registro de usuario. Utiliza comparación con equals(), métodos de consulta, expresiones regulares, StringBuilder y conversión de tipos.

ValidadorFormulario.java — Ejemplo integrador completo
public class ValidadorFormulario {

    /**
     * Valida un nombre de usuario.
     * Reglas: 3-20 caracteres, solo letras, números y guiones bajos.
     */
    public static String validarUsuario(String usuario) {
        if (usuario == null || usuario.isBlank()) {
            return "El nombre de usuario no puede estar vacío";
        }
        String limpio = usuario.trim();
        if (limpio.length() < 3 || limpio.length() > 20) {
            return "El usuario debe tener entre 3 y 20 caracteres";
        }
        if (!limpio.matches("[a-zA-Z0-9_]+")) {
            return "Solo se permiten letras, números y guiones bajos";
        }
        return null; // null = sin errores
    }

    /**
     * Valida un email con patrón regex.
     */
    public static String validarEmail(String email) {
        if (email == null || email.isBlank()) {
            return "El email no puede estar vacío";
        }
        String limpio = email.trim().toLowerCase();
        if (!limpio.matches("[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}")) {
            return "Formato de email no válido";
        }
        return null;
    }

    /**
     * Valida que la contraseña sea fuerte.
     * Reglas: mín 8 chars, al menos una mayúscula, una minúscula,
     *         un dígito y un carácter especial.
     */
    public static String validarPassword(String password) {
        if (password == null || password.length() < 8) {
            return "La contraseña debe tener al menos 8 caracteres";
        }
        StringBuilder errores = new StringBuilder();
        if (password.equals(password.toLowerCase())) {
            errores.append("- Falta al menos una letra mayúscula\n");
        }
        if (password.equals(password.toUpperCase())) {
            errores.append("- Falta al menos una letra minúscula\n");
        }
        if (!password.matches(".*\\d.*")) {
            errores.append("- Falta al menos un dígito\n");
        }
        if (!password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\",.<>?/].*")) {
            errores.append("- Falta al menos un carácter especial\n");
        }
        return errores.length() > 0 ? errores.toString() : null;
    }

    /**
     * Valida una edad proporcionada como String.
     */
    public static String validarEdad(String edadStr) {
        if (edadStr == null || edadStr.isBlank()) {
            return "La edad no puede estar vacía";
        }
        try {
            int edad = Integer.parseInt(edadStr.trim());
            if (edad < 18 || edad > 120) {
                return "La edad debe estar entre 18 y 120 años";
            }
        } catch (NumberFormatException e) {
            return "La edad debe ser un número entero válido";
        }
        return null;
    }

    /**
     * Ejecuta todas las validaciones y genera un informe.
     */
    public static String validarFormulario(String usuario, String email,
                                           String password, String edad) {
        StringBuilder informe = new StringBuilder();
        informe.append(String.format("=== Validación de registro ===%n"));
        informe.append(String.format("Usuario: %s%n", usuario));
        informe.append(String.format("Email:   %s%n%n", email));

        String[] campos = {"Usuario", "Email", "Contraseña", "Edad"};
        String[] errores = {
            validarUsuario(usuario),
            validarEmail(email),
            validarPassword(password),
            validarEdad(edad)
        };

        boolean formularioValido = true;
        for (int i = 0; i < campos.length; i++) {
            if (errores[i] != null) {
                informe.append(String.format("❌ %s: %s%n", campos[i], errores[i]));
                formularioValido = false;
            } else {
                informe.append(String.format("✅ %s: OK%n", campos[i]));
            }
        }

        informe.append(String.format("%nResultado: %s%n",
            formularioValido ? "REGISTRO VÁLIDO" : "REGISTRO RECHAZADO"));

        return informe.toString();
    }

    public static void main(String[] args) {
        // Caso 1: formulario válido
        System.out.println(validarFormulario(
            "ana_garcia", "ana@empresa.com", "Segura#2026", "28"));

        System.out.println("---");

        // Caso 2: formulario con errores
        System.out.println(validarFormulario(
            "ab", "correo-invalido", "1234", "dieciocho"));
    }
}

Salida esperada del programa:

Salida del validador
=== Validación de registro ===
Usuario: ana_garcia
Email:   ana@empresa.com

✅ Usuario: OK
✅ Email: OK
✅ Contraseña: OK
✅ Edad: OK

Resultado: REGISTRO VÁLIDO
---
=== Validación de registro ===
Usuario: ab
Email:   correo-invalido

❌ Usuario: El usuario debe tener entre 3 y 20 caracteres
❌ Email: Formato de email no válido
❌ Contraseña: - Falta al menos una letra mayúscula
- Falta al menos una letra minúscula
- Falta al menos un carácter especial

❌ Edad: La edad debe ser un número entero válido

Resultado: REGISTRO RECHAZADO

🚫 Errores frecuentes con Strings

❌ Error 1: Comparar Strings con ==

❌ Mal vs ✅ Bien
String input = scanner.nextLine();

// ❌ MAL: puede fallar si input no está en el Pool
if (input == "salir") { ... }

// ✅ BIEN: compara contenido siempre
if ("salir".equals(input)) { ... }

❌ Error 2: No capturar NumberFormatException

❌ Mal vs ✅ Bien
// ❌ MAL: se rompe si el usuario escribe texto
int edad = Integer.parseInt(scanner.nextLine());

// ✅ BIEN: con control de errores
try {
    int edad = Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
    System.out.println("Por favor, introduce un número válido.");
}

❌ Error 3: Concatenar en bucle con +

❌ Mal vs ✅ Bien
// ❌ MAL: O(n²) — crea un nuevo String en cada iteración
String csv = "";
for (String item : lista) {
    csv += item + ",";
}

// ✅ BIEN: O(n) — modifica un buffer mutable
String csv = String.join(",", lista);

// ✅ TAMBIÉN BIEN: con StringBuilder para lógica compleja
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lista.size(); i++) {
    if (i > 0) sb.append(",");
    sb.append(lista.get(i));
}
String csv = sb.toString();

❌ Error 4: Ignorar que los métodos de String devuelven un nuevo objeto

❌ Mal vs ✅ Bien
String nombre = "  Ana García  ";

// ❌ MAL: trim() NO modifica nombre, devuelve un nuevo String
nombre.trim();
nombre.toUpperCase();
System.out.println(nombre); // "  Ana García  " — ¡sin cambios!

// ✅ BIEN: asignar el resultado
nombre = nombre.trim();
nombre = nombre.toUpperCase();
System.out.println(nombre); // "ANA GARCÍA"

❌ Error 5: NullPointerException al llamar métodos sobre un String null

❌ Mal vs ✅ Bien
String valor = obtenerValor(); // puede devolver null

// ❌ MAL: si valor es null, lanza NullPointerException
if (valor.equals("esperado")) { ... }

// ✅ BIEN: literal primero (patrón defensivo)
if ("esperado".equals(valor)) { ... }

// ✅ TAMBIÉN BIEN: comprobación explícita de null
if (valor != null && valor.equals("esperado")) { ... }

✏️ Ejercicios prácticos

Ejercicio 1: Contador de vocales y consonantes

Escribe un método contarLetras(String texto) que reciba un String y devuelva un mensaje indicando cuántas vocales y cuántas consonantes contiene, ignorando espacios, números y caracteres especiales. Las vocales acentuadas (á, é, í, ó, ú) deben contarse como vocales.

Ejemplo: contarLetras("Hola Mundo 2026")"Vocales: 4, Consonantes: 5"

Ejercicio 2: Palíndromo avanzado

Escribe un método esPalindromo(String texto) que determine si un texto es un palíndromo, ignorando mayúsculas/minúsculas, espacios, tildes y signos de puntuación. Debe normalizar las vocales acentuadas (á→a, é→e, etc.).

Ejemplos:

esPalindromo("Anita lava la tina")true

esPalindromo("Dábale arroz a la zorra el abad")true

esPalindromo("Hola mundo")false

Ejercicio 3: Cifrado César con Strings

Implementa un cifrado César que desplace cada letra un número de posiciones dado. El cifrado debe preservar mayúsculas/minúsculas, no modificar caracteres que no sean letras (espacios, números, puntuación), y funcionar con desplazamientos negativos (descifrado).

Ejemplo: cifrarCesar("Hola Mundo!", 3)"Krod Pxqgr!"

cifrarCesar("Krod Pxqgr!", -3)"Hola Mundo!"

Ejercicio 4: Analizador de texto (avanzado)

Crea una clase AnalizadorTexto que reciba un String en el constructor y ofrezca los siguientes métodos: contarPalabras(), palabraMasFrecuente(), promedioLongitudPalabras() y resumen(). Usa split(), StringBuilder, String.format() y colecciones si lo deseas.

❓ Preguntas frecuentes sobre Strings en Java

Las dudas más comunes respondidas de forma clara y directa.

El operador == compara las referencias en memoria (si ambas variables apuntan al mismo objeto), mientras que equals() compara el contenido carácter a carácter. Para comparar el texto de dos Strings siempre se debe usar equals() o equalsIgnoreCase(). Con == se pueden obtener resultados inesperados, especialmente cuando uno de los Strings se crea con new String().
El String Pool (también llamado intern pool) es una zona especial de la memoria heap donde Java almacena los literales String. Cuando se crea un String con comillas dobles, la JVM comprueba primero si ya existe un String con el mismo contenido en el pool. Si existe, reutiliza esa referencia en lugar de crear un objeto nuevo. Esto ahorra memoria y mejora el rendimiento. Los Strings creados con new String() no se almacenan automáticamente en el pool, pero se pueden añadir manualmente con el método intern().
Los Strings son inmutables por tres razones principales: seguridad (evitan que código malicioso modifique valores sensibles como rutas de archivos o cadenas de conexión), rendimiento (permiten el String Pool y la caché del hashCode), y seguridad en hilos (al ser inmutables son inherentemente thread-safe y pueden compartirse entre hilos sin sincronización). Cuando se invoca un método como toUpperCase(), se crea un nuevo objeto String sin modificar el original.
Se debe usar StringBuilder cuando se concatenan Strings dentro de un bucle o cuando se realizan muchas operaciones de concatenación secuenciales. En un bucle, cada concatenación con + crea un nuevo objeto String en cada iteración, lo que resulta en O(n²) en complejidad de tiempo y genera basura para el recolector. StringBuilder mantiene un buffer interno mutable y ofrece rendimiento O(n). Para concatenaciones simples de pocas variables fuera de bucles, el operador + es aceptable porque el compilador lo optimiza internamente.
Ambos son clases mutables para manipular cadenas, pero StringBuffer es thread-safe (sus métodos están sincronizados con synchronized), mientras que StringBuilder no lo es. En aplicaciones de un solo hilo o cuando la sincronización no es necesaria, StringBuilder es más rápido al no tener la sobrecarga de la sincronización. Desde Java 5, StringBuilder es la opción recomendada en la mayoría de casos. StringBuffer se reserva para escenarios multihilo donde varios hilos modifican la misma cadena.
Para convertir un número a String se puede usar String.valueOf(numero), Integer.toString(numero) o concatenar con cadena vacía: "" + numero. Para convertir un String a número se usa Integer.parseInt(cadena) para int, Double.parseDouble(cadena) para double, o Long.parseLong(cadena) para long. Estos métodos lanzan NumberFormatException si la cadena no contiene un número válido, por lo que se recomienda envolver la conversión en un bloque try-catch.
Se utiliza el método equalsIgnoreCase(). Por ejemplo: "Hola".equalsIgnoreCase("hola") devuelve true. Es preferible a convertir ambas cadenas con toLowerCase() o toUpperCase() antes de comparar, ya que equalsIgnoreCase() es más eficiente y maneja correctamente caracteres especiales de distintos idiomas. Para ordenación que ignore mayúsculas se puede usar String.CASE_INSENSITIVE_ORDER como Comparator.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Strings en Java? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!