Strings en Java — equals, substring y split si vienes de Python (y por qué == a veces te engaña)

Cuando pasé de Python a Java, escribí algo como if (nombre == "Sergio") y me pareció de lo más natural: en Python se hace así. A veces funcionaba y a veces no. Lo más desconcertante era que dos textos idénticos daban false, y no entendía por qué. Tardé un buen rato en descubrir que en Java == no compara el contenido de los textos, compara si son el mismo objeto. Este artículo es para que no pierdas esa tarde.

Vamos a ver cómo se crean los String, cómo se comparan, cómo se accede a sus caracteres y trozos, cómo se buscan cosas dentro y cómo se dividen. Como en otros artículos, iremos comparando con Python, porque casi todo lo que sabes se traduce, pero con nombres y trampas distintos. Si todavía no has visto los arrays, pasa antes por los ejercicios de arrays: split te va a devolver uno.

Qué es un String en Java

Un String es una secuencia de caracteres. Se escribe entre comillas dobles:

String saludo = "Hola mundo";

Hay una diferencia importante con Python: las comillas simples no sirven para textos. En Java se reservan para un carácter suelto, que es otro tipo distinto llamado char:

String texto = "a";   // un String que contiene una letra
char letra = 'a';     // un char: un único carácter

"a" y 'a' parecen lo mismo, pero no lo son: uno es un objeto y el otro un valor primitivo. Lo notarás enseguida con charAt.

Un String vacío se escribe con dos comillas sin nada en medio (o con new String(), que es lo mismo pero más largo):

String vacio1 = "";
String vacio2 = new String();

Y para convertir otro valor en texto hay varias formas, todas equivalentes:

int numero = 10;
String texto1 = String.valueOf(numero);    // "10"
String texto2 = Integer.toString(numero);  // "10"
String texto3 = "" + numero;               // "10" (el truco rápido)

Secuencias de escape

Igual que en Python, la barra invertida \ permite escribir caracteres especiales dentro de un texto, y las secuencias son las mismas:

SecuenciaSignificado
\nNueva línea
\tTabulador
\"Comilla doble
\\Barra invertida
System.out.println("Pedro dijo \"hola\"");   // Pedro dijo "hola"
System.out.println("Línea 1\nLínea 2");       // dos líneas
System.out.println("Col1\tCol2");              // Col1    Col2
System.out.println("C:\\Users\\ana");          // C:\Users\ana

La regla de oro: un String no se modifica

En Java (igual que en Python), los String son inmutables: una vez creados, no cambian. Lo que parecen «modificaciones» son en realidad métodos que devuelven un String nuevo y dejan el original como estaba.

String nombre = "ana";
nombre.toUpperCase();                 // crea "ANA"... y lo tira a la basura
System.out.println(nombre);           // ana  (¡no ha cambiado!)

nombre = nombre.toUpperCase();        // ahora sí guardamos el resultado
System.out.println(nombre);           // ANA

Llamar a un método de String y no guardar el resultado. nombre.toUpperCase(); sola en una línea no hace nada útil: no da error, no cambia nombre, y es uno de los fallos más difíciles de ver. Recuerda: nombre = nombre.toUpperCase();.

Longitud: length() con paréntesis

String s1 = "esto es una string";
System.out.println(s1.length());   // 18

En Python escribías len(s1); aquí es s1.length().

Con los arrays, length no lleva paréntesis (array.length), y con los String, sí (texto.length()). Es la confusión más típica al empezar: los dos significan «cuántos hay», pero uno es un atributo y el otro un método. Si el compilador te dice «cannot find symbol», mira esto primero.

Comparar Strings: equals, no ==

Esta es la sección más importante del artículo, y la de mi anécdota.

String s1 = "esto es una string";
String s2 = "esto es otra string";
String s3 = "esto es una string";

System.out.println(s1.equals(s2));   // false
System.out.println(s1.equals(s3));   // true

Para saber si dos textos son iguales, se usa equals. Si no te importan las mayúsculas, equalsIgnoreCase:

System.out.println("Hola".equalsIgnoreCase("hola"));   // true

¿Y qué pasa con ==?

Veamos por qué mi comparación «a veces funcionaba»:

String a = "Sergio";
String b = "Sergio";
String c = new String("Sergio");
String parte = "Ser";
String d = parte + "gio";

System.out.println(a == b);        // true   (!)
System.out.println(a == c);        // false  (!)
System.out.println(a == d);        // false  (!)
System.out.println(a.equals(c));   // true
System.out.println(a.equals(d));   // true

== compara direcciones, no contenido. Y aquí está la trampa: cuando escribes dos textos literales iguales (a y b), Java los reutiliza y los hace apuntar al mismo objeto, así que a == b da true por casualidad. Pero en cuanto un texto se construye de otra manera (new String(...), una concatenación en tiempo de ejecución, algo que lees con Scanner…) pasa a ser otro objeto distinto, y == da false aunque el contenido sea idéntico.

Por eso a mí «a veces funcionaba»: con textos escritos a mano coincidía, y con textos leídos del teclado no.

La regla es la misma que con los arrays: == compara referencias, y para comparar contenido hay que usar un método. Con arrays, Arrays.equals; con String, equals. Cuando entiendas esto, entenderás también cómo funcionan los objetos en general.

Ordenar: compareTo

Para saber cuál va antes alfabéticamente, se usa compareTo. Devuelve 0 si son iguales, un número negativo si el primero va antes y uno positivo si va después:

System.out.println(s1.compareTo(s2));      // 6
System.out.println(s2.compareTo(s1));      // -6
System.out.println("a".compareTo("b"));    // -1
System.out.println("hola".compareTo("hola mundo"));   // -6

El número sale de restar los códigos de la primera pareja de caracteres distinta ('u' − 'o' = 117 − 111 = 6 en el primer ejemplo), o, si uno es el principio del otro, de la diferencia de longitudes. Lo importante en la práctica es solo el signo. Existe también compareToIgnoreCase.

Si vas a comparar una variable con un texto fijo, escríbelo al revés: "Sergio".equals(nombre) en vez de nombre.equals("Sergio"). Si nombre es null, la primera da false tan tranquila y la segunda lanza un NullPointerException.

Concatenar

Igual que en Python, con +:

String s3 = "Hola" + " " + "mundo";                  // "Hola mundo"
String s4 = "Hola".concat(" ").concat("mundo");       // lo mismo con concat

Pero con una trampa que en Python no existe, porque Python no deja sumar números y textos sin convertir:

System.out.println("Total: " + 1 + 2);       // Total: 12
System.out.println(1 + 2 + " total");        // 3 total
System.out.println("Total: " + (1 + 2));     // Total: 3

El + se evalúa de izquierda a derecha. En el primer caso, "Total: " + 1 ya es un texto, y a un texto le pegas el 2: sale «12». En el segundo, 1 + 2 se suma primero porque aún son números. Si quieres sumar dentro de un texto, usa paréntesis.

Acceder a caracteres: charAt

En Python escribías s[3]. En Java no hay corchetes para los String; se usa charAt:

String h = "Hola mundo";
char c1 = h.charAt(3);                 // 'a'
System.out.println(c1 == 'a');         // true

Fíjate en dos cosas. Primero, charAt devuelve un char, no un String (en Python el resultado sí sería un texto de un carácter). Segundo, como char es un tipo primitivo, con los char sí se puede usar == sin problema.

Los índices empiezan en 0 como siempre, y no existen los negativos: charAt(-1) no da el último carácter (eso era de Python), da error.

h.charAt(10);   // StringIndexOutOfBoundsException: Index 10 out of bounds for length 10

El texto exacto del mensaje de error depende de la versión de Java (esta es la de Java 21), pero la excepción es siempre la misma: StringIndexOutOfBoundsException. Y la regla, la de siempre: el último índice es length() - 1.

Recorrer un String

Como no hay corchetes, se recorre con charAt y un for clásico, o con un for-each sobre toCharArray(), que convierte el texto en un array de char:

String h = "Hola mundo";

int vocales = 0;
for (int i = 0; i < h.length(); i++) {
    char ch = h.charAt(i);
    if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
        vocales++;
    }
}
System.out.println(vocales);   // 4

// Lo mismo con for-each
int vocales2 = 0;
for (char ch : h.toCharArray()) {
    if ("aeiou".indexOf(ch) != -1) {   // ¿está ch dentro de "aeiou"?
        vocales2++;
    }
}
System.out.println(vocales2);  // 4

Trozos de un String: substring

El slicing de Python (s[1:4]) se convierte en substring(inicio, fin). Igual que en Python, el índice final no se incluye:

String h = "Hola mundo";
System.out.println(h.substring(1, 4));   // ola   (posiciones 1, 2 y 3)
System.out.println(h.substring(5));      // mundo (desde la 5 hasta el final)
System.out.println(h.substring(5, 5));   // (vacío: de la 5 a la 5 no hay nada)

Con un solo número, copia hasta el final. Y lo que no perdona, a diferencia de Python, son los índices fuera de rango:

h.substring(4, 2);     // error: el inicio no puede ser mayor que el final
h.substring(0, 100);   // error: el final se sale del texto

Ambos lanzan StringIndexOutOfBoundsException. En Python, s[0:100] simplemente te daría todo el texto; en Java no hay esa generosidad.

Olvidar que el final no se incluye. Para quedarte con las 3 primeras letras no es substring(0, 2), es substring(0, 3). Y para el texto entero, substring(0, h.length()).

Buscar dentro de un String

La siguiente tabla recoge los equivalentes con Python:

Qué quieroPythonJava
¿Contiene un trozo?"la mu" in ss.contains("la mu")
Posición de la primera aparicións.find("x")s.indexOf("x")
Posición de la última aparicións.rfind("x")s.lastIndexOf("x")
¿Empieza por…?s.startswith("x")s.startsWith("x")
¿Termina en…?s.endswith("x")s.endsWith("x")
String h = "Hola mundo";
System.out.println(h.contains("la mu"));   // true
System.out.println(h.contains("casa"));    // false

String bien = "Hola, bienvenidos a mi mundo";
System.out.println(bien.indexOf("bienvenidos"));       // 6
System.out.println(bien.indexOf("bienvenidos", 9));    // -1  (buscando desde la 9, ya no está)
System.out.println(bien.indexOf('o'));                 // 1
System.out.println(bien.indexOf('o', 2));              // 15 (la siguiente 'o')
System.out.println(bien.indexOf("xyz"));               // -1

String ar = "La araña con maña teje la telaraña";
System.out.println(ar.lastIndexOf("araña"));           // 29
System.out.println(ar.lastIndexOf("araña", 9));        // 3   (buscando hacia atrás desde la 9)

String correo = "ana.perez@alu.ulpgc.es";
System.out.println(correo.startsWith("ana"));              // true
System.out.println(correo.endsWith("@alu.ulpgc.es"));      // true
System.out.println(correo.startsWith("Ana"));              // false (distingue mayúsculas)

Lo que conviene recordar:

  • indexOf devuelve -1 si no encuentra nada. Es la señal de «no está», exactamente como find en Python. (Es el mismo -1 que vimos en la práctica de arrays.)
  • Funciona con un String o con un char.
  • El segundo parámetro es desde dónde empezar a buscar. En lastIndexOf se busca hacia atrás desde esa posición.

Usar el resultado de indexOf sin comprobar que no es -1. Si haces correo.substring(0, correo.indexOf('@')) con un texto sin arroba, indexOf devuelve -1 y substring(0, -1) lanza una excepción. Comprueba primero.

Limpiar y transformar

Todos estos métodos devuelven un String nuevo (acuérdate de la regla de oro):

System.out.println("[" + "   hola  ".trim() + "]");   // [hola]
System.out.println("Hola".toLowerCase());              // hola
System.out.println("Hola".toUpperCase());              // HOLA
System.out.println("banana".replace('a', 'o'));        // bonono
System.out.println("banana".replace("an", "AN"));      // bANANa
  • trim() quita los espacios de los dos extremos (como strip() de Python), pero no los de en medio.
  • replace sustituye todas las apariciones, no solo la primera.

Dividir un String: split

split parte un texto en trozos y los devuelve en un array de String (en Python, en una lista):

String[] partes = "uno,dos,tres".split(",");
System.out.println(Arrays.toString(partes));   // [uno, dos, tres]
System.out.println(partes[1]);                 // dos
System.out.println(partes.length);             // 3

Hay tres detalles que no son como en Python y que dan problemas:

1. El separador siempre hay que escribirlo

En Python, s.split() sin argumentos divide por espacios. En Java no existe split() sin parámetros: hay que decir el separador, y además dos espacios seguidos dejan un texto vacío en medio:

System.out.println(Arrays.toString("a  b".split(" ")));     // [a, , b]
System.out.println(Arrays.toString("a  b".split("\\s+")));  // [a, b]

La segunda forma ("\\s+", que significa «uno o más espacios») usa una expresión regular; las verás con calma en su propio tema, de momento cópiala si necesitas dividir por espacios.

2. El separador es una expresión regular

Esto es lo que más sorprende. El parámetro de split no es un texto literal, es una expresión regular, y hay caracteres con significado especial, como . y |:

System.out.println(Arrays.toString("1.2.3".split(".")));     // []   (¡array vacío!)
System.out.println(Arrays.toString("1.2.3".split("\\.")));   // [1, 2, 3]
System.out.println(Arrays.toString("a|b".split("|")));       // [a, |, b]
System.out.println(Arrays.toString("a|b".split("\\|")));     // [a, b]

"1.2.3".split(".") devuelve un array vacío, porque en una expresión regular el punto significa «cualquier carácter» y todo el texto son separadores. Para dividir por un punto (o por |, +, *, ?…) hay que «escaparlo» con dos barras: "\\.". Si tu split devuelve cosas raras, sospecha del separador.

3. Los vacíos del final desaparecen

System.out.println(Arrays.toString("a,b,,".split(",")));        // [a, b]
System.out.println(Arrays.toString("a,b,,".split(",", -1)));    // [a, b, , ]

Por defecto, split descarta los trozos vacíos del final. Con un segundo parámetro -1, los conserva. Y con un número positivo, limita cuántos trozos como máximo:

System.out.println(Arrays.toString("uno,dos,tres,cuatro".split(",", 2)));   // [uno, dos,tres,cuatro]

Otras utilidades

Algunas más que vas a usar mucho:

System.out.println("hola".isEmpty());      // false
System.out.println("".isEmpty());          // true
System.out.println("  ".isBlank());        // true (solo espacios)
System.out.println(String.join("-", "a", "b", "c"));   // a-b-c
System.out.println("ab".repeat(3));        // ababab  (el "ab" * 3 de Python)
System.out.println(Integer.parseInt("42") + 1);        // 43  (de texto a número)

Y si el texto no es un número válido, Integer.parseInt("4x2") lanza una NumberFormatException.

Para saber más: StringBuilder

Como los String no se modifican, un bucle que concatena muchas veces (resultado += ...) crea un String nuevo en cada vuelta. Para pocos datos da igual; para muchos, existe StringBuilder, que sí es modificable:

StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
    sb.append(i).append(", ");
}
sb.setLength(sb.length() - 2);          // quitamos la última ", "
System.out.println(sb.toString());      // 1, 2, 3, 4, 5
System.out.println(new StringBuilder("hola").reverse());   // aloh

No lo necesitas para empezar, pero te lo vas a encontrar enseguida, y reverse() es una forma rápida de invertir un texto.

Resumen de equivalencias con Python

PythonJava
len(s)s.length()
s == ts.equals(t)
s[3]s.charAt(3) (devuelve char)
s[1:4]s.substring(1, 4)
s[5:]s.substring(5)
"x" in ss.contains("x")
s.find("x")s.indexOf("x")
s.upper() / s.lower()s.toUpperCase() / s.toLowerCase()
s.strip()s.trim()
s.split(",")s.split(",") (devuelve un array; el separador es una regex)
s * 3s.repeat(3)

Un ejemplo completo: fichas de alumno

Juntemos varias piezas en un programa pequeño. Recibimos líneas de texto con datos mezclados (con espacios de más) y queremos extraer el nombre, las iniciales, el usuario del correo universitario y la asignatura en mayúsculas.

public class FichaAlumno {

    // "  Ana Pérez ; correo ; fp2 " -> {"Ana Pérez", "correo", "fp2"} sin espacios sobrantes
    static String[] partir(String linea) {
        String[] campos = linea.split(";");
        for (int i = 0; i < campos.length; i++) {
            campos[i] = campos[i].trim();        // trim devuelve una String NUEVA: hay que guardarla
        }
        return campos;
    }

    // "Ana Pérez Suárez" -> "APS"
    static String iniciales(String nombreCompleto) {
        String resultado = "";
        String[] palabras = nombreCompleto.split(" ");
        for (String palabra : palabras) {
            if (!palabra.isEmpty()) {            // si hay espacios dobles, split deja Strings vacías
                resultado += palabra.substring(0, 1).toUpperCase();
            }
        }
        return resultado;
    }

    static boolean esCorreoUniversitario(String correo) {
        int arroba = correo.indexOf('@');        // -1 si no hay arroba
        return arroba > 0 && correo.endsWith("ulpgc.es");
    }

    // Lo que hay antes de la arroba (solo llamar si esCorreoUniversitario es true)
    static String usuario(String correo) {
        return correo.substring(0, correo.indexOf('@'));
    }

    public static void main(String[] args) {
        String[] lineas = {
            "  Ana Pérez Suárez ; ana.perez@alu.ulpgc.es ; fp2  ",
            "Luis   Gómez;luis.gomez@gmail.com;ic2",
            "Marta Ruiz ; marta.ruiz.ulpgc.es ; FP1"
        };

        for (String linea : lineas) {
            String[] campos = partir(linea);
            String nombre = campos[0];
            String correo = campos[1];
            String asignatura = campos[2].toUpperCase();

            System.out.println("Alumno:     " + nombre + " (" + iniciales(nombre) + ")");
            if (esCorreoUniversitario(correo)) {
                System.out.println("Usuario:    " + usuario(correo));
            } else {
                System.out.println("Correo no universitario: " + correo);
            }
            System.out.println("Asignatura: " + asignatura);
            System.out.println();
        }
    }
}

Salida:

Alumno:     Ana Pérez Suárez (APS)
Usuario:    ana.perez
Asignatura: FP2

Alumno:     Luis   Gómez (LG)
Correo no universitario: luis.gomez@gmail.com
Asignatura: IC2

Alumno:     Marta Ruiz (MR)
Correo no universitario: marta.ruiz.ulpgc.es
Asignatura: FP1

Fíjate en cómo se combinan las piezas: split devuelve un array que recorremos con un for; trim devuelve un String nuevo y por eso lo guardamos en campos[i]; indexOf devuelve -1 si no hay arroba y por eso comprobamos arroba > 0 antes de llamar a substring; y en iniciales, la segunda línea («Luis Gómez», con tres espacios) deja Strings vacías al dividir, así que las saltamos con isEmpty().

Visualízalo con Python Tutor

Lo de la inmutabilidad y el == se entiende mucho mejor viéndolo. Entra en pythontutor.com/java.html, pega este código y avanza paso a paso con «Next»:

public class Main {
    public static void main(String[] args) {
        String nombre = "ana";
        nombre.toUpperCase();
        String grande = nombre.toUpperCase();
        System.out.println(nombre);
        System.out.println(grande);

        String a = "Sergio";
        String b = "Ser" + "gio";
        String c = new String("Sergio");
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));
    }
}

Qué observar:

  • Tras nombre.toUpperCase(); aparece un String nuevo "ANA" en memoria, pero nadie lo guarda: nombre sigue apuntando a "ana".
  • Con grande = nombre.toUpperCase(), ahora sí hay una variable que apunta al "ANA". Los dos textos conviven como objetos distintos.
  • Con a y b, los dos textos literales idénticos apuntan al mismo objeto y a == b da true. Con c, creado con new, hay un objeto distinto y a == c da false, aunque a.equals(c) dé true.

Es habitual preguntarse…

¿Puedo cambiar un carácter de un String, como s[0] = 'X' en otros lenguajes?

No: los String son inmutables y no existe nada parecido. Lo que se hace es construir un String nuevo (por ejemplo, "X" + s.substring(1)) o usar un StringBuilder, que sí permite modificar caracteres.

¿Qué diferencia hay entre "" y null?

"" es un String real con cero caracteres: puedes llamar a sus métodos ("".length() da 0). null significa que la variable no apunta a ningún String: llamar a cualquier método sobre ella lanza un NullPointerException. Es la misma idea que con los arrays.

¿Cuándo uso char y cuándo String?

Usa char cuando trabajes con un carácter (el resultado de charAt, o al recorrer un texto) y String para cualquier texto, aunque tenga una sola letra. Un char se escribe con comillas simples y se puede comparar con ==; un String, con comillas dobles y se compara con equals.

¿Cómo convierto un texto en número?

Con Integer.parseInt("42") para enteros y Double.parseDouble("3.5") para decimales. Si el texto no es válido, lanzan NumberFormatException, un error que verás cuando trates las excepciones.

Resumen

  • Un String se escribe con comillas dobles; las comillas simples son para un char.
  • Los String son inmutables: los métodos devuelven uno nuevo, y hay que guardarlo (s = s.trim();).
  • Longitud: s.length() (con paréntesis, a diferencia de array.length).
  • Para comparar contenido, equals (o equalsIgnoreCase); == compara referencias. Para ordenar, compareTo.
  • + concatena, pero se evalúa de izquierda a derecha: "a" + 1 + 2 da "a12".
  • charAt(i) devuelve un char; substring(inicio, fin) no incluye el final; fuera de rango lanza StringIndexOutOfBoundsException.
  • indexOf / lastIndexOf devuelven -1 si no encuentran nada; contains, startsWith y endsWith devuelven un booleano.
  • trim, toLowerCase, toUpperCase y replace devuelven Strings nuevos.
  • split devuelve un array, su separador es una expresión regular ("\\." para el punto) y descarta los vacíos del final.

Checklist: ¿lo tienes claro?

  • ¿Sabes por qué nombre.toUpperCase(); sola no hace nada?
  • ¿Sabes explicar por qué == a veces da true y a veces false con textos iguales?
  • ¿Recuerdas cuándo length lleva paréntesis y cuándo no?
  • ¿Sabes qué devuelve charAt y por qué se puede comparar con ==?
  • ¿Sabes extraer un trozo con substring sin olvidar que el final no se incluye?
  • ¿Sabes por qué "1.2.3".split(".") devuelve un array vacío?

Si alguno te ha dejado dudas, vuelve a su sección antes de seguir. En el siguiente artículo lo pondremos en práctica con programas completos.

Siguiente paso: prácticaStrings en Java — práctica con un importador de alumnos, un generador de usuarios y un buscador de contactos (y por qué hay que normalizar antes de comparar)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *