Fundamentos
Strings
Inmutabilidad, pool y los métodos que de verdad usas cada día.
Desde Java 8
Este capítulo cubre Java 8. Estás leyendo en .
Un objeto que se comporta casi como un primitivo
String es una clase, final desde siempre, pero el lenguaje la trata con privilegios:
tiene literales propios, el + la concatena y switch la acepta. Ningún otro objeto de la
biblioteca tiene ese estatus.
Lo que la define es la inmutabilidad. Un String no cambia nunca: cada método que parece
modificarlo devuelve uno nuevo.
String saludo = "hola";
String mayusculas = saludo.toUpperCase();
System.out.println(saludo); // hola
System.out.println(mayusculas); // HOLAEso permite compartirla sin defensas: pasarla a un método no puede alterarla, se puede usar
como clave de un mapa sin miedo a que cambie, y su hashCode se calcula una vez y se guarda.
El precio es que cada operación de “modificación” crea basura que el recolector tiene que
llevarse.
Literales y el pool
Los literales String viven en una zona compartida, el pool de cadenas, y dos literales
iguales son el mismo objeto:
String a = "hola";
String b = "hola";
String c = new String("hola");
System.out.println(a == b); // true, es el mismo objeto del pool
System.out.println(a == c); // false, new crea otra instancia
System.out.println(a == c.intern()); // true, intern devuelve la del poolLa especificación lo garantiza así: los literales, y en general las cadenas que son valores de
expresiones constantes, están internadas, de modo que un literal siempre se refiere a la misma
instancia. intern() devuelve la instancia canónica y cumple que s.intern() == t.intern()
si y solo si s.equals(t).
Lo que no es constante no se interna:
String parte = "ho";
String armada = parte + "la";
System.out.println(armada == "hola"); // false
System.out.println(armada.intern() == "hola"); // trueparte + "la" se calcula en ejecución, así que no es una expresión constante. Si en cambio
escribes "ho" + "la", el compilador lo pliega y queda "hola", un literal.
Dónde vive cada cadena
-
01
Literal
String a = "hola";Se guarda una vez en el pool. -
02
Pool
"hola"Compartido: b apunta al mismo objeto. -
03
Explícito
new String("hola")Objeto nuevo, contenido igual.
new String(...) casi nunca es lo que quieres: crea una copia que no aporta nada.equals, y el orden de los operandos
if ("hola".equals(entrada)) { ... } // seguro con entrada null
if (entrada.equals("hola")) { ... } // NullPointerException si entrada es nullPoner la constante delante es el patrón de siempre en Java, y hoy hay alternativas:
Objects.equals(entrada, "hola") o "hola".equalsIgnoreCase(entrada). Lo que no cambia es
que equals distingue mayúsculas: para ignorarlas hace falta el método explícito.
Concatenar: + está bien hasta que deja de estarlo
Dentro de una línea, + es cómodo y el compilador lo resuelve bien. Dentro de un bucle es
cuadrático: cada vuelta copia todo lo anterior.
// mal: crea una cadena nueva por iteración
String resultado = "";
for (String palabra : palabras) {
resultado += palabra + " ";
}
// bien: un solo búfer
StringBuilder constructor = new StringBuilder();
for (String palabra : palabras) {
constructor.append(palabra).append(' ');
}
String bueno = constructor.toString();StringBuilder es mutable y no está sincronizado; StringBuffer es lo mismo con synchronized
en cada método y casi nunca merece la pena (si compartes el búfer entre hilos, el problema es
otro). Para unir una lista con separador existe String.join(", ", palabras), que ya lo hace
bien y sin bucle.
Text blocks: texto largo sin escapar (Java 15)
Un bloque de texto empieza con tres comillas y una línea nueva. El contenido se escribe tal
cual, sin \n y sin escapar comillas:
String json = """
{
"nombre": "Ada",
"lenguaje": "Java"
}
""";La indentación incidental se recorta: se toma el mínimo de espacios de las líneas no vacías y
de la línea de cierre, y ese prefijo se quita de todas. Por eso la línea de cierre decide el
margen. Para meter un salto al final, deja la línea de cierre en su propia línea. Para
conservar indentación extra, indent lo hace sobre una cadena ya construida.
Longitud: un char no es un carácter
String guarda UTF-16. length() cuenta unidades de código, no caracteres visibles:
String emoji = "😀";
System.out.println(emoji.length()); // 2
System.out.println(emoji.codePointCount(0, emoji.length())); // 1Un emoji fuera del plano básico ocupa dos char (un par sustituto). Si cuentas posiciones
para mostrar o truncar texto de usuario, codePointCount y offsetByCodePoints son las
herramientas correctas; substring(0, 1) sobre ese emoji parte el par y deja un carácter
inválido.
chars() devuelve un IntStream con las unidades de código, y codePoints() con los puntos,
que es lo que casi siempre buscas al filtrar texto.
Los métodos que de verdad usas
| Necesitas | Método | Detalle |
|---|---|---|
| Longitud | length() | En unidades UTF-16 |
| Carácter en posición | charAt(i) | Lanza IndexOutOfBoundsException |
| Trozo | substring(inicio, fin) | fin exclusivo |
| Buscar | indexOf, lastIndexOf | Devuelven -1 si no está |
| Reemplazar | replace, replaceAll | replaceAll toma una regex |
| Partir | split(regex) | Quita las cadenas vacías del final |
| Unir | String.join(sep, ...) | Acepta varargs o Iterable |
| Formatear | formatted(...) | Desde Java 15, equivale a String.format |
| Recortar | strip() | Espacios Unicode; trim() solo quita <= U+0020 |
| Comprobar vacío | isEmpty(), isBlank() | isBlank desde Java 11, y acepta solo espacios |
| Repetir | repeat(n) | Desde Java 11, negativo lanza excepción |
| Recorrer líneas | lines() | Stream<String> sin terminadores; desde Java 11 |
Dos trampas de esta tabla. split toma una expresión regular, así que "a.b".split(".")
devuelve un array vacío: el punto coincide con todo. Hay que escribir split("\\."). Y
split descarta las cadenas vacías del final, de modo que "a,b,,".split(",") da dos
elementos; si las necesitas, pasa un límite negativo.
Para recortar, strip desde Java 11 es lo que quieres: usa Character.isWhitespace, que
incluye el espacio duro y otros. trim es anterior y solo quita caracteres con código menor o
igual a U+0020, que en la práctica acierta casi siempre y falla en el espacio de no
separación.
Comparar: compareTo no ordena como un humano
compareTo compara por valor Unicode, carácter a carácter:
System.out.println("Zaragoza".compareTo("álbum")); // negativo, 'Z' < 'á'Ese orden no es el alfabético de ningún idioma: pone las mayúsculas antes que las minúsculas y
no sabe nada de tildes. Para ordenar texto que va a leer una persona se usa Collator:
Collator collator = Collator.getInstance(Locale.forLanguageTag("es-ES"));
int orden = collator.compare("Zaragoza", "álbum"); // en español, Zaragoza va despuéscompareTo sigue siendo el correcto para claves técnicas, identificadores y orden estable
entre máquinas.
De texto a número y vuelta
int numero = Integer.parseInt("42");
double decimal = Double.parseDouble("3.5");
String texto = Integer.toString(numero);
String tambien = String.valueOf(42);
String desdeCero = String.format("%,d", 1_234_567);parseInt no admite espacios ni separadores de miles y lanza NumberFormatException si el
texto no es un número. Para entrada de usuario, Integer.parseInt(entrada.strip()) dentro de
un try es el patrón; antes de Java 11, entrada.trim().
Qué te llevas
Stringes inmutable: cada cambio apunta a un objeto nuevo, y eso es lo que lo hace seguro de compartir.- Los literales viven en el pool;
new Stringno, eintern()recupera la instancia canónica. ==compara referencias,equalscompara contenido. Connullen juego, pon la constante delante.+en un bucle es cuadrático:StringBuilder,String.joinoformatted.length()cuenta unidades UTF-16: para caracteres visibles,codePointCount.splittoma una regex y se come las cadenas vacías del final.stripdesde Java 11 es el recorte correcto;trimes el heredado.compareToordena por Unicode;Collatorordena como una persona.- Formatea con
Localeexplícito cuando el texto sale del programa.