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.
Tabla de Contenidos
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:
| Secuencia | Significado |
|---|---|
\n | Nueva línea |
\t | Tabulador |
\" | 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é quiero | Python | Java |
|---|---|---|
| ¿Contiene un trozo? | "la mu" in s | s.contains("la mu") |
| Posición de la primera aparición | s.find("x") | s.indexOf("x") |
| Posición de la última aparición | s.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:
indexOfdevuelve-1si no encuentra nada. Es la señal de «no está», exactamente comofinden Python. (Es el mismo-1que vimos en la práctica de arrays.)- Funciona con un
Stringo con unchar. - El segundo parámetro es desde dónde empezar a buscar. En
lastIndexOfse 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 (comostrip()de Python), pero no los de en medio.replacesustituye 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
| Python | Java |
|---|---|
len(s) | s.length() |
s == t | s.equals(t) |
s[3] | s.charAt(3) (devuelve char) |
s[1:4] | s.substring(1, 4) |
s[5:] | s.substring(5) |
"x" in s | s.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 * 3 | s.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:nombresigue apuntando a"ana". - Con
grande = nombre.toUpperCase(), ahora sí hay una variable que apunta al"ANA". Los dos textos conviven como objetos distintos. - Con
ayb, los dos textos literales idénticos apuntan al mismo objeto ya == bdatrue. Conc, creado connew, hay un objeto distinto ya == cdafalse, aunquea.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
Stringse escribe con comillas dobles; las comillas simples son para unchar. - Los
Stringson inmutables: los métodos devuelven uno nuevo, y hay que guardarlo (s = s.trim();). - Longitud:
s.length()(con paréntesis, a diferencia dearray.length). - Para comparar contenido,
equals(oequalsIgnoreCase);==compara referencias. Para ordenar,compareTo. +concatena, pero se evalúa de izquierda a derecha:"a" + 1 + 2da"a12".charAt(i)devuelve unchar;substring(inicio, fin)no incluye el final; fuera de rango lanzaStringIndexOutOfBoundsException.indexOf/lastIndexOfdevuelven-1si no encuentran nada;contains,startsWithyendsWithdevuelven un booleano.trim,toLowerCase,toUpperCaseyreplacedevuelven Strings nuevos.splitdevuelve 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 datruey a vecesfalsecon textos iguales? - ¿Recuerdas cuándo
lengthlleva paréntesis y cuándo no? - ¿Sabes qué devuelve
charAty por qué se puede comparar con==? - ¿Sabes extraer un trozo con
substringsin 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 →
