Ir al contenido
wannacode

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);
Salida
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());
Salida
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 un String normal, 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
  1. report apunta a una cadena vacía.
  2. El primer += crea un String nuevo con una línea: 18 caracteres.
  3. 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.
  4. 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.
  5. StringBuilder es un solo objeto con un búfer. Aquí el búfer tiene sitio para 64 caracteres.
  6. append escribe en el búfer en su sitio. No se copia nada, y el objeto sigue siendo el mismo.
  7. 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, StringBuilder reserva 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 + ": " + price en código eficiente. El problema es solo += en un bucle, donde cada vuelta es una cadena nueva aparte.
  • También existe StringBuffer con 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, usa StringBuilder.

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());
Salida
4532 0151 1283 0366
**** **** **** 0366
0888123456
amor
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);
Respuesta

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"));
Salida
-1
0
32
0
  • 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));
Salida
Alonso, Blanco, Iglesias, Torres

String.join(", ", names) pega los elementos del array con un separador entre ellos.

Más precisamente, para curiosos
  • compareTo compara 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. compareToIgnoreCase no 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)));
Respuesta

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));
Salida
lucia.gomez
empresa.es
19
Lucía Gómez | lucia.gomez@empresa.es | Sevilla
====================
  • strip() quita los espacios de delante y de detrás, como trim(), pero también conoce los espacios «anchos» de otros idiomas. Úsalo en código nuevo.
  • indexOf('@') da el índice de la primera aparición, y indexOf('.', at) empieza a buscar solo desde el índice at en adelante. lastIndexOf busca desde atrás.
  • substring y split ya los conoces. String.join es lo contrario de split.
  • "=".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());
Salida
true false
true true

isEmpty() 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);
Salida
42-2.4

Integer.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));
Salida
Café 2 x 2.40 = 4.80
Cruasán 1 x 3.10 = 3.10
Agua mineral 3 x 1.50 = 4.50
-----------------------------------
Total 12.40 €
  • %-16s es un texto en 16 caracteres, alineado a la izquierda. %5.2f es un número en 5 caracteres con 2 después del punto, alineado a la derecha. %2d es un número entero en 2 caracteres.
  • "plantilla".formatted(...) hace lo mismo que String.format("plantilla", ...). Se llama directamente sobre la plantilla.
Más precisamente, para curiosos
  • a.concat(b) es lo mismo que a + 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") lanza NumberFormatException: aquí el separador decimal es siempre un punto. Pero String.format("%.2f", 2.4) en una computadora con configuración en español da 2,40. Para un texto que leerá otro programa, usa String.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.

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.

Entrada
Anita lava la tina
Salida
Palíndromo
Entrada
Anita lava la ropa
Salida
No es un palíndromo

2. 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.

Entrada
3
Pan;1.20;2
Queso;9.80;1
Huevos;0.35;10
Salida
Pan 2 x 1.20 = 2.40
Queso 1 x 9.80 = 9.80
Huevos 10 x 0.35 = 3.50
------------------------------------
Total 15.70

3. 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.

Entrada
Mañana a las 9 hay reunión
Salida
reunión hay 9 las a Mañana

Má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.es crea l**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la @. Usa indexOf y repeat.
  • Un recorrido por String. Crea una cadena con comillas ("Texto de ejemplo") y otra con new String("Texto de ejemplo"). Compáralas con == y con equals, toma substring(0, 5), divide la segunda por el espacio e imprime las partes, convierte "100" en un número e imprímelo con String.format("Valor: %d", ...), y al final añade " y más" a un StringBuilder con 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 compareTo para que use compareToIgnoreCase, y pruébalo con «lucas», «Bruno» y «ana».

Test

Ponte a prueba

1. ¿Por qué report += line es lento en un bucle con 50 000 vueltas?
2. ¿Qué devuelve "Blanco".compareTo("Alonso")?
3. StringBuilder a = new StringBuilder("12"); StringBuilder b = a; b.append("3"); ¿Qué hay en a?
4. ¿Cómo conviertes un StringBuilder en un String?
5. ¿Qué diferencia hay entre String.format(...) y "...".formatted(...)?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·