Cadenas
En resumen: Una cadena en Java no cambia: cada + crea un texto nuevo, por eso muchos trozos se juntan con StringBuilder, y compareTo compara en orden alfabético.
Problema
El informe que se arrastra
Ya sabes que title.toUpperCase(); por sí solo no hace nada: una cadena es inmutable, y el método devuelve una cadena nueva. Esto tiene un coste, y se ve en un bucle.
El programa de la tienda en línea construye el informe del día línea por línea:
String report = "";for (int i = 1; i <= 3; i++) { report += "Pedido " + i + ": 12.50 €\n";}System.out.print(report);Pedido 1: 12.50 €Pedido 2: 12.50 €Pedido 3: 12.50 €Con tres pedidos es instantáneo. El sábado hay 50 000 pedidos, y el mismo bucle tarda unos 2,5 segundos. Lo medimos en la máquina donde revisamos el código de las lecciones.
¿Por qué? Cada report += ... no añade nada a la cadena vieja. Crea una cadena nueva, copia en ella todo el informe hasta ahora y solo entonces le pega la línea nueva. Hacia el final, cada vuelta copia más de un millón de caracteres.
La idea
StringBuilder: texto que cambia
Para construir texto con muchos trozos existe StringBuilder. Es un objeto que guarda el texto en su propio búfer y añade en su sitio, sin volver a copiarlo todo:
StringBuilder report = new StringBuilder();for (int i = 1; i <= 3; i++) { report.append("Pedido ").append(i).append(": 12.50 €\n");}System.out.print(report.toString());Pedido 1: 12.50 €Pedido 2: 12.50 €Pedido 3: 12.50 €new StringBuilder()crea un «constructor» vacío.append(...)añade al final. Acepta texto, un número o un carácter, y devuelve el mismo objeto, por eso puedes encadenar las llamadas una tras otra.toString()crea con él unStringnormal, una vez, al final.
Con 50 000 pedidos esta versión tarda unos 7 milisegundos en lugar de 2,5 segundos.
Esto es lo que pasa en la memoria con cada forma. Para que se vea que el búfer no se copia, aquí se crea con sitio para 64 caracteres.
String report = "";for (int i = 1; i <= 3; i++) { report += "Pedido " + i + ": 12.50 €\n";}StringBuilder builder = new StringBuilder(64);for (int i = 1; i <= 3; i++) { builder.append("Pedido ").append(i).append(": 12.50 €\n");}System.out.println(report.length() + " " + builder.length());System.out.println(report.equals(builder.toString()));Paso a paso, en palabras
- report apunta a una cadena vacía.
- El primer += crea un String nuevo con una línea: 18 caracteres.
- El segundo += crea otro String. Copia en él la línea vieja (18 caracteres) y solo entonces le pega la nueva. La cadena vieja ya no le sirve a nadie.
- El tercero copia 36 caracteres. Con cada línea se copia más: con 50 000 líneas son más de un millón de caracteres por vuelta.
- StringBuilder es un solo objeto con un búfer. Aquí el búfer tiene sitio para 64 caracteres.
- append escribe en el búfer en su sitio. No se copia nada, y el objeto sigue siendo el mismo.
- El texto es el mismo, 54 caracteres. Con += se copiaron otros 54, y con StringBuilder ni uno solo.
Más precisamente, para curiosos
- El búfer crece solo: cuando se llena,
StringBuilderreserva uno aproximadamente el doble de grande y copia una vez. Por eso copiar es raro. - El compilador convierte por sí mismo una sola expresión como
"Pedido " + i + ": " + priceen código eficiente. El problema es solo+=en un bucle, donde cada vuelta es una cadena nueva aparte. - También existe
StringBuffercon los mismos métodos. Es seguro cuando varios hilos escriben en un mismo objeto a la vez, y por eso es un poco más lento. Los hilos llegan en el módulo «Programas en el mundo real». Hasta entonces, usaStringBuilder.
Míralo
Cambiar en su sitio
StringBuilder puede hacer más que añadir. Aquí tienes un número de tarjeta para imprimirlo en un recibo:
StringBuilder card = new StringBuilder("4532015112830366");card.insert(12, ' ').insert(8, ' ').insert(4, ' ');System.out.println(card);card.replace(0, 14, "**** **** ****");System.out.println(card);
StringBuilder phone = new StringBuilder("0888-123-456");phone.deleteCharAt(8).deleteCharAt(4);System.out.println(phone);System.out.println(new StringBuilder("roma").reverse());4532 0151 1283 0366**** **** **** 03660888123456amor| Método | Qué hace |
|---|---|
insert(i, x) |
inserta x antes del carácter con índice i |
replace(a, b, x) |
reemplaza los caracteres desde a hasta b (sin b) por x |
deleteCharAt(i) |
quita el carácter con índice i |
delete(a, b) |
quita los caracteres desde a hasta b (sin b) |
reverse() |
da la vuelta al texto, de atrás hacia delante |
length(), charAt(i) |
igual que en String |
Los espacios del número de tarjeta se insertan de atrás hacia delante: primero en 12, después en 8, después en 4. Si empiezas por 4, el espacio insertado desplaza todo lo que hay detrás, y 8 ya no es el lugar que quieres. Por la misma razón, los guiones del teléfono se quitan primero por detrás.
Pruébalo
Dos nombres, un objeto
¿Qué imprimirá el programa?
StringBuilder note = new StringBuilder("Mesa 4");StringBuilder copy = note;copy.append(", 2 cafés");System.out.println(note);
String text = "Mesa 4";String other = text;other += ", 2 cafés";System.out.println(text);StringBuilder es un objeto normal. copy = note copia la flecha, las dos variables apuntan al mismo objeto, y append lo cambia en su sitio. Con un String es distinto: other += ... crea una cadena nueva y mueve solo other hacia ella. text sigue apuntando a la vieja.La idea
En orden alfabético: compareTo
equals solo te dice «¿son iguales?». Para ordenar necesitas más: qué cadena va antes que la otra. Eso es lo que te dice compareTo:
System.out.println("Alonso".compareTo("Blanco"));System.out.println("Blanco".compareTo("Blanco"));System.out.println("iglesias".compareTo("Iglesias"));System.out.println("iglesias".compareToIgnoreCase("Iglesias"));-10320- un número negativo: la primera va antes que la segunda;
- 0: son iguales;
- un número positivo: la primera va después de la segunda.
Lo que importa es el signo, no el número en sí. Con él ordenas una lista de nombres, por ejemplo con el ordenamiento de burbuja de la lección sobre arrays:
String[] names = { "Torres", "Alonso", "Iglesias", "Blanco" };for (int i = 0; i < names.length - 1; i++) { for (int j = 0; j < names.length - 1 - i; j++) { if (names[j].compareTo(names[j + 1]) > 0) { String tmp = names[j]; names[j] = names[j + 1]; names[j + 1] = tmp; } }}System.out.println(String.join(", ", names));Alonso, Blanco, Iglesias, TorresString.join(", ", names) pega los elementos del array con un separador entre ellos.
Más precisamente, para curiosos
compareTocompara carácter por carácter, según su código en la tabla de caracteres. Las mayúsculas van antes que las minúsculas, por eso"iglesias".compareTo("Iglesias")es positivo.compareToIgnoreCaseno distingue mayúsculas y minúsculas.- El número es la diferencia entre los códigos de los primeros caracteres distintos (aquí «i» e «I» se diferencian en 32), o entre las longitudes, si una cadena es el principio de la otra.
- Para ordenar según las reglas del español, por ejemplo con la «ñ» y las letras con tilde como «á», existe
java.text.Collator.getInstance(Locale.of("es")). Por el código, «ñ» y «á» van después de la «z». java.util.Arrays.sort(names)ordena un array de cadenas con la misma regla, sin escribir el ordenamiento de burbuja.
Pruébalo
¿Factura 10 o 9?
Los números de las facturas son texto. ¿Cuál va antes?
¿Qué imprimirá el programa?
String a = "INV-10";String b = "INV-9";System.out.println(a.compareTo(b) < 0);System.out.println(Integer.parseInt(a.substring(4)) < Integer.parseInt(b.substring(4)));El texto se compara carácter por carácter. Los cuatro primeros caracteres son iguales, después vienen «1» y «9», y «1» va antes que «9». Por eso "INV-10" va antes que "INV-9", igual que en el diccionario «ba» va antes que «c». Los números 10 y 9 se comparan como números solo después de parseInt. Si ordenas números como texto, rellénalos con ceros: INV-009, INV-010.
La idea
Buscar, cortar, juntar
Aquí tienes una línea de un archivo de contactos: nombre, correo y ciudad, separados con « ; », con espacios de sobra en los extremos:
String line = " Lucía Gómez ; lucia.gomez@empresa.es ; Sevilla ";String[] parts = line.strip().split(" ; ");String email = parts[1];int at = email.indexOf('@');System.out.println(email.substring(0, at));System.out.println(email.substring(at + 1));System.out.println(email.indexOf('.', at));System.out.println(String.join(" | ", parts));System.out.println("=".repeat(20));lucia.gomezempresa.es19Lucía Gómez | lucia.gomez@empresa.es | Sevilla====================strip()quita los espacios de delante y de detrás, comotrim(), pero también conoce los espacios «anchos» de otros idiomas. Úsalo en código nuevo.indexOf('@')da el índice de la primera aparición, yindexOf('.', at)empieza a buscar solo desde el índiceaten adelante.lastIndexOfbusca desde atrás.substringysplitya los conoces.String.joines lo contrario desplit."=".repeat(20)repite el texto 20 veces.
Vacío y «en blanco» no son lo mismo:
String empty = "";String spaces = " ";System.out.println(empty.isEmpty() + " " + spaces.isEmpty());System.out.println(empty.isBlank() + " " + spaces.isBlank());true falsetrue trueisEmpty() es verdadero solo para un texto de longitud 0. isBlank() también es verdadero cuando solo hay espacios. Para revisar un campo que alguien ha rellenado, normalmente quieres isBlank().
La idea
Números y texto
Los números entran en el texto y salen del texto:
int code = Integer.parseInt("0042");double price = Double.parseDouble("2.40");String label = String.valueOf(code) + "-" + String.valueOf(price);System.out.println(label);42-2.4Integer.parseInt y Double.parseDouble ya los conoces. String.valueOf es lo contrario: crea texto a partir de un número, un carácter o un boolean.
Para una tabla con columnas necesitas String.format de la lección sobre la entrada, con un ancho para cada columna. Aquí tienes un recibo de una cafetería:
String[] names = { "Café", "Cruasán", "Agua mineral" };double[] prices = { 2.40, 3.10, 1.50 };int[] qty = { 2, 1, 3 };double total = 0;for (int i = 0; i < names.length; i++) { double sum = prices[i] * qty[i]; total += sum; System.out.println(String.format("%-16s%2d x %5.2f = %6.2f", names[i], qty[i], prices[i], sum));}System.out.println("-".repeat(35));System.out.println("%-26s%9.2f €".formatted("Total", total));Café 2 x 2.40 = 4.80Cruasán 1 x 3.10 = 3.10Agua mineral 3 x 1.50 = 4.50-----------------------------------Total 12.40 €%-16ses un texto en 16 caracteres, alineado a la izquierda.%5.2fes un número en 5 caracteres con 2 después del punto, alineado a la derecha.%2des un número entero en 2 caracteres."plantilla".formatted(...)hace lo mismo queString.format("plantilla", ...). Se llama directamente sobre la plantilla.
Más precisamente, para curiosos
a.concat(b)es lo mismo quea + b. Lo verás en código más antiguo.- El ancho es como mínimo esa cantidad de caracteres. Un texto más largo no se corta: empuja la columna hacia la derecha.
Double.parseDouble("2,40")lanzaNumberFormatException: aquí el separador decimal es siempre un punto. PeroString.format("%.2f", 2.4)en una computadora con configuración en español da2,40. Para un texto que leerá otro programa, usaString.format(Locale.ROOT, ...).
Pruébalo
Encuentra el error
«Reconocer» se lee igual al revés, pero el programa dice «reconocer no es un palíndromo». ¿En qué línea está el error?
Elige la línea con el error.
reversed es un StringBuilder, y word es un String. El equals de String devuelve true solo para otro String con los mismos caracteres. Para un objeto de otra clase es false, tenga lo que tenga dentro. La corrección es word.equals(reversed.toString()).Ejercicio
Texto que tú juntas
Resuelve las tareas en tu editor. No imprimas preguntas.
1. Palíndromo. Lee una línea y di si es un palíndromo, sin tener en cuenta las mayúsculas, los espacios, las comas y los guiones. Esta vez da la vuelta al texto con StringBuilder.reverse() en lugar de con un bucle.
Anita lava la tinaPalíndromoAnita lava la ropaNo es un palíndromo2. Un recibo con columnas. La primera línea tiene el número de artículos. Cada línea siguiente tiene un nombre, un precio y una cantidad, separados con ;. Imprime un recibo con las columnas alineadas y el total al final.
3Pan;1.20;2Queso;9.80;1Huevos;0.35;10Pan 2 x 1.20 = 2.40Queso 1 x 9.80 = 9.80Huevos 10 x 0.35 = 3.50------------------------------------Total 15.703. Palabras al revés. Lee una frase e imprime sus palabras en orden inverso. Entre las palabras puede haber más de un espacio, y en el resultado tiene que haber exactamente uno. Júntalo con un StringBuilder.
Mañana a las 9 hay reuniónreunión hay 9 las a MañanaMás tareas
- Iniciales. Lee un nombre completo («María López Sánchez») e imprime las iniciales con puntos: «M. L. S.» Júntalas con un
StringBuilder. - Correo oculto. A partir de
lucia.gomez@empresa.escreal**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la@. UsaindexOfyrepeat. - Un recorrido por String. Crea una cadena con comillas (
"Texto de ejemplo") y otra connew String("Texto de ejemplo"). Compáralas con==y conequals, tomasubstring(0, 5), divide la segunda por el espacio e imprime las partes, convierte"100"en un número e imprímelo conString.format("Valor: %d", ...), y al final añade" y más"a unStringBuildercon la primera cadena. Predice cada línea antes de ejecutar el programa. - Ordenar sin distinguir mayúsculas. Cambia el ordenamiento de burbuja de la tarjeta de
compareTopara que usecompareToIgnoreCase, y pruébalo con «lucas», «Bruno» y «ana».
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas: trabajar con texto · Clases y objetos · Métodos de instancia · Herencia
- Continúa en: Expresiones regulares · Expresiones regulares: grupos y reemplazo · Archivos · Datos en archivos
- Ejercicios sobre el tema: Texto y archivos