Los strings de Java nunca cambian, así que cada edición crea uno nuevo. Aprende los métodos de String y sus trampas, por qué += en un bucle lo copia todo, StringBuilder, text blocks, Unicode y arrays con los helpers de Arrays.
Un String de Java no se puede cambiar después de creado. Cada método que parece editarlo te devuelve un string nuevo. Ese hecho explica un bug clásico, un bucle lento clásico y por qué existe StringBuilder.
Este post cubre los métodos de String de todos los días y sus trampas, cómo comparar strings, StringBuilder, Unicode, los text blocks y los arrays con los helpers de Arrays. Cada programa de abajo se ejecutó en Java 25, y su salida está copiada de esa ejecución. Para ejecutar uno tú mismo, guárdalo como Main.java y ejecuta java Main.java.
Los strings son inmutables
Un objeto String guarda sus caracteres de por vida, y ningún método los cambia. toUpperCase no convierte un string a mayúsculas. Devuelve un string nuevo en mayúsculas, y si no guardas ese valor de retorno, se pierde:
void main() {
String name = "ada";
name.toUpperCase();
IO.println(name);
String shout = name.toUpperCase();
IO.println(shout);
IO.println(name);
name = name.toUpperCase();
IO.println(name);
}
Imprime:
ada
ADA
ada
ADA
La segunda línea de main es el bug. Compila, se ejecuta y no hace nada, y javac no avisa, ni siquiera con -Xlint:all. Guarda el resultado, como hacen las últimas líneas. Aun así, name = name.toUpperCase() no cambia "ada". Hace que name apunte a otro string.
Los métodos de String de todos los días
Casi todo el trabajo con strings es encontrar una posición, recortar un trozo o comprobar qué hay dentro. Las posiciones, llamadas índices, empiezan en 0:
void main() {
String s = "hello, world";
IO.println(s.length());
IO.println(s.charAt(0));
IO.println(s.charAt(s.length() - 1));
IO.println(s.substring(7));
IO.println(s.substring(0, 5));
IO.println(s.indexOf("o"));
IO.println(s.lastIndexOf("o"));
IO.println(s.indexOf("z"));
IO.println(s.contains("world"));
IO.println(s.replace("l", "L"));
IO.println(s.startsWith("hell") + " " + s.endsWith("!"));
}
Imprime:
12
h
d
world
hello
4
8
-1
true
heLLo, worLd
true false
Algunas reglas que aprender de esa salida:
- El último índice es
length() - 1. Un string de 12 caracteres tiene índices del 0 al 11. substring(begin, end)incluyebeginy se detiene antes deend. La longitud del resultado siempre esend - begin.substring(7)llega hasta el final.indexOfdevuelve -1 cuando no hay coincidencia. Compruébalo antes de usar el resultado como índice.replacecambia todas las coincidencias de texto literal.replaceAllse parece, pero recibe una expresión regular.
Un índice fuera de rango lanza una excepción
substring y charAt revisan sus índices, y uno incorrecto detiene el programa:
void main() {
String word = "hello";
IO.println(word.substring(1, 5));
IO.println(word.substring(2, 9));
}
Imprime y se detiene:
ello
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [2, 9) out of bounds for length 5
substring(1, 5) funciona, porque un final igual a la longitud significa “hasta el final”. El mensaje escribe el rango incorrecto en notación matemática: [2, 9) incluye el 2 y excluye el 9, que es la regla del final excluido escrita tal cual.
split recibe una expresión regular
split corta un string en cada coincidencia de un separador y devuelve un array con los trozos. El separador es una expresión regular, no texto literal, y eso atrapa a casi todo el mundo alguna vez:
void main() {
IO.println(Arrays.toString("a,b,c".split(",")));
IO.println(Arrays.toString("1.2.3".split(".")));
IO.println(Arrays.toString("1.2.3".split("\\.")));
IO.println(Arrays.toString("red|green".split("|")));
IO.println(Arrays.toString("red|green".split(Pattern.quote("|"))));
IO.println(Arrays.toString("a,b,,".split(",")));
IO.println(Arrays.toString("a,b,,".split(",", -1)));
IO.println(Arrays.toString(",a".split(",")));
}
Imprime:
[a, b, c]
[]
[1, 2, 3]
[r, e, d, |, g, r, e, e, n]
[red, green]
[a, b]
[a, b, , ]
[, a]
Tres sorpresas, en orden:
split(".")devuelve un array vacío..significa “cualquier carácter”, así que cada trozo queda vacío, y los trozos vacíos del final se descartan. Escapa el punto como"\\.".split("|")corta entre cada carácter.|significa “o”, y “nada o nada” coincide en todas partes.Pattern.quotehace que cualquier texto coincida de forma literal.- Los strings vacíos del final desaparecen.
"a,b,,"tiene cuatro campos, perosplit(",")devuelve dos. Los del principio se quedan, como muestra",a". Pasa un límite de -1 para conservar todos los campos, por ejemplo en una fila CSV con las últimas columnas vacías.
Recortar, comprobar si está en blanco, repetir, unir y dar formato
Estos métodos limpian y construyen strings. strip, isBlank y repeat llegaron en Java 11, y formatted en Java 15:
void main() {
String spaces = " total ";
IO.println("[" + spaces.trim() + "] [" + spaces.strip() + "]");
String emSpaces = "\u2003total\u2003";
IO.println(emSpaces.trim().length() + " " + emSpaces.strip().length());
IO.println("".isEmpty() + " " + " ".isEmpty() + " " + " ".isBlank());
IO.println("ab".repeat(3));
IO.println(String.join(", ", "Ana", "Ben", "Chen"));
IO.println(String.join("/", List.of("usr", "local", "bin")));
IO.println(String.format("%-6s|%5d|%.2f", "tea", 42, 3.14159));
IO.println("%s scored %d%% on %,d questions".formatted("Ana", 95, 1200));
}
Imprime:
[total] [total]
7 5
true false true
ababab
Ana, Ben, Chen
usr/local/bin
tea | 42|3.14
Ana scored 95% on 1,200 questions
trim y strip coinciden con los espacios normales. No coinciden con \u2003, un espacio eme, que suele llegar en texto pegado desde una página web. trim quita solo los caracteres que en la tabla de caracteres llegan hasta el espacio normal, así que conservó los dos y devolvió 7 caracteres. strip conoce todos los espacios en blanco de Unicode. En código nuevo, usa strip.
isBlank es true para un string que solo tiene espacios en blanco, que suele ser la comprobación que quieres para la entrada del usuario. isEmpty es true solo para longitud 0.
formatted hace el mismo trabajo que String.format, pero se llama sobre el propio patrón. %s es un string, %d un número entero, %.2f dos decimales, %-6s alineado a la izquierda en seis columnas, %,d un número con separadores y %% un signo de porcentaje.
Comparar strings
equals compara dos strings carácter por carácter. La parte sobre valores y referencias mostró por qué == está mal para strings: comprueba si es el mismo objeto, no el mismo texto. Dos métodos más cubren las mayúsculas y el orden:
void main() {
String a = "Java";
IO.println(a.equals("java"));
IO.println(a.equalsIgnoreCase("java"));
IO.println("apple".compareTo("banana"));
IO.println("banana".compareTo("apple"));
IO.println("pear".compareTo("pear"));
IO.println("app".compareTo("apple"));
IO.println("Zebra".compareTo("apple"));
IO.println("Zebra".compareToIgnoreCase("apple"));
String missing = null;
IO.println("Java".equals(missing));
}
Imprime:
false
true
-1
1
0
-2
-7
25
false
compareTo ordena dos strings. Solo importa el signo: negativo significa que el primer string va antes, cero que son iguales y positivo que va después. El número es la diferencia entre los primeros caracteres distintos, o entre las longitudes, como con "app" y "apple".
"Zebra" va antes que "apple", porque cada letra mayúscula tiene un código de carácter menor que cada letra minúscula. compareToIgnoreCase da el orden que una persona espera.
La última línea es un hábito que vale la pena copiar. "Java".equals(missing) devuelve false para null, mientras que missing.equals("Java") lanzaría NullPointerException.
Java guarda una sola copia compartida de cada literal de string en un string pool, así que dos literales idénticos son el mismo objeto y == sobre ellos da true por casualidad. Los strings construidos en tiempo de ejecución, a partir de la entrada o de un StringBuilder, no van al pool. Por eso == sobre strings pasa una prueba rápida y falla con datos reales.
Por qué += en un bucle lo copia todo
Como un string no puede crecer, s += "!" crea un string nuevo con un carácter más, copia en él cada carácter del viejo, añade "!" y hace que s apunte al resultado. El string viejo se vuelve basura. En un bucle, cada paso copia más que el anterior.
Este programa cuenta las copias en lugar de medir el tiempo. Para +=, cada paso copia el string actual completo. Para StringBuilder, cuenta cuántas veces el builder pasó a un buffer más grande y qué copió en ese momento:
void main() {
int n = 10_000;
String s = "";
long copiedByPlus = 0;
for (int i = 0; i < n; i++) {
copiedByPlus += s.length();
s += "!";
}
var sb = new StringBuilder();
long copiedByBuilder = 0;
int regrowths = 0;
for (int i = 0; i < n; i++) {
int before = sb.capacity();
sb.append('!');
if (sb.capacity() != before) {
regrowths++;
copiedByBuilder += sb.length() - 1;
}
}
IO.println("+= made " + n + " strings and copied " + copiedByPlus + " characters");
IO.println("StringBuilder grew " + regrowths + " times and copied "
+ copiedByBuilder + " characters");
IO.println("same text: " + s.contentEquals(sb));
}
Imprime:
+= made 10000 strings and copied 49995000 characters
StringBuilder grew 10 times and copied 18394 characters
same text: true
Los dos bucles construyen los mismos 10.000 caracteres. El bucle con += copió 0, luego 1, luego 2, hasta 9.999, que es más o menos n² / 2. Duplica n y las copias se multiplican por cuatro. Eso es O(n²).
Un StringBuilder guarda sus caracteres en un buffer con espacio de sobra, y append escribe en el siguiente hueco libre. Cuando el buffer se llena, el builder reserva uno de más o menos el doble de tamaño y copia todo una vez. Eso pasó solo 10 veces, así que las copias se quedan cerca de n.
Una sola expresión como "Hi " + name + ", you have " + n + " messages" no necesita un StringBuilder. Desde Java 9, javac compila la expresión entera en una sola llamada que construye el string una vez. El costo aparece solo cuando un bucle repite +=, porque cada vuelta empieza otra vez desde el string completo.
Usar un StringBuilder
Un StringBuilder es un string que sí puedes cambiar. Lo vas construyendo y después llamas a toString una vez para obtener un String normal:
void main() {
var sb = new StringBuilder("world");
sb.insert(0, "hello ");
sb.append('!').append(" x").append(3);
IO.println(sb);
sb.setCharAt(0, 'H');
sb.deleteCharAt(sb.length() - 1);
String done = sb.toString();
IO.println(done + " has " + done.length() + " characters");
IO.println(new StringBuilder("stressed").reverse());
var one = new StringBuilder("abc");
var two = new StringBuilder("abc");
IO.println(one.equals(two));
IO.println(one.toString().equals(two.toString()));
IO.println("abc".contentEquals(one));
}
Imprime:
hello world! x3
Hello world! x has 14 characters
desserts
false
true
true
append acepta strings, caracteres y números y devuelve el mismo builder, así que las llamadas se encadenan. insert pone texto en un índice, y reverse invierte el builder en el mismo lugar.
Las tres últimas líneas esconden una trampa. StringBuilder no sobrescribe equals, así que dos builders con el mismo texto no son iguales. Compara sus strings, o usa String.contentEquals. También puedes encontrarte con StringBuffer, una versión más antigua y sincronizada. Usa StringBuilder.
Explicado como si tuvieras diez años
Un String es un letrero impreso. Nadie puede cambiar sus letras. Para que “Sale” diga “Sale!”, imprimes un letrero nuevo completo y tiras el viejo.
Ahora agrega una letra al día. Cada día imprimes un letrero nuevo con todas las letras viejas más una, y el bote de reciclaje se llena. Al final del mes has impreso la primera letra 30 veces.
Un StringBuilder es una pizarra. Escribes la siguiente letra después de la última, y no se vuelve a imprimir nada. Cuando terminas, imprimes un solo letrero a partir de la pizarra.
La versión precisa
Un String guarda un array privado de caracteres en el que nada escribe después de la construcción. replace, toUpperCase y + reservan todos un String nuevo con un array nuevo.
Un StringBuilder guarda un array cuya capacidad es al menos su longitud. append escribe en la parte sin usar. Cuando una escritura no cabe, el builder reserva un array más grande, el doble de la capacidad anterior más 2 en este JDK (16, 34, 70, 142 y así sucesivamente), y copia los caracteres. Como el tamaño se duplica, cada carácter se copia solo unas pocas veces en promedio, así que n llamadas a append cuestan O(n). toString copia la parte usada en un String nuevo, así que los cambios posteriores al builder no pueden alcanzarlo.
Dónde falla la analogía: una pizarra real tiene un tamaño fijo. Un StringBuilder se cambia por uno más grande cuando se llena, y ese cambio sí copia todo, solo que rara vez. Si conoces el tamaño final, new StringBuilder(10_000) se ahorra esos cambios.
Ver cómo += copia y append llena un buffer
La animación ejecuta s += "!" dos veces y después hace el mismo trabajo con un StringBuilder:
Cada s += “!” crea un string nuevo, copia cada carácter del viejo y deja el viejo como basura. Un StringBuilder escribe cada ! en un hueco libre de un único buffer, y toString copia una sola vez al final.
Aquí están esos pasos en palabras, por si la animación no se reproduce:
sapunta a unStringque contiene"go".s += "!"reserva un string de tres caracteres, copiagyoen él y añade!. Ahorasapunta a él, y el viejo"go"es basura. Dos caracteres copiados.s += "!"otra vez reserva cuatro caracteres y copia los tres de"go!"antes de añadir!."go!"también es basura. Son 5 caracteres copiados para construir cuatro.- Un
StringBuilderguarda"go"en un buffer de 8 huecos. sb.append('!')dos veces escribe!en el hueco 3 y después en el hueco 4. Es el mismo buffer, y nada se mueve.sb.toString()copia los 4 caracteres a un soloStringnuevo, la única copia que hace el builder.
Un char no siempre es un carácter
Un char de Java es una unidad de código UTF-16 de 16 bits, no una letra tal como la ve una persona. La mayoría de las letras, incluidas las acentuadas, caben en un char. Los emoji ocupan dos, lo que se llama un par sustituto (surrogate pair), y length() cuenta chars:
void main() {
String word = "café";
String thumb = "👍";
IO.println(word.length() + " " + thumb.length());
IO.println(thumb.codePointCount(0, thumb.length()));
IO.println(Character.isHighSurrogate(thumb.charAt(0)));
IO.println(Integer.toHexString(thumb.codePointAt(0)));
IO.println(Character.toString(0x1F44D));
String text = "ok👍";
IO.println(text.length() + " " + text.codePointCount(0, text.length()));
IO.println(new StringBuilder(text).reverse());
IO.println(text.substring(0, 3));
}
Imprime:
4 2
1
true
1f44d
👍
4 3
👍ko
ok?
Un code point es el número que Unicode le da a un carácter, aquí U+1F44D. El pulgar es un solo code point guardado como dos chars, y charAt(0) devuelve solo la primera mitad, el “high surrogate”.
Las dos últimas líneas nos sorprendieron. StringBuilder.reverse mantiene juntos los pares sustitutos, así que el emoji sobrevivió. substring(0, 3) partió el par por la mitad, y un sustituto suelto no tiene codificación UTF-8, así que Java imprimió ?. Cortar texto del usuario en un índice fijo puede corromperlo de esta forma.
Ni siquiera los code points cuentan toda la historia. Una bandera como 🇮🇳 son dos code points que quien lee ve como un solo símbolo. Tenlo en cuenta cuando recortes nombres o cuentes caracteres contra un límite.
Text blocks: strings de varias líneas sin escapes
Un text block es un literal de string de varias líneas, entre dos grupos de tres comillas dobles. Quedó como definitivo en Java 15, y javac --release 14 lo rechaza con text blocks are not supported in -source 14. Hace legibles el JSON, el SQL y el HTML:
void main() {
String json = """
{
"name": "Ana",
"score": 95
}
""";
IO.print(json);
String shifted = """
{
"name": "Ana"
}
""";
IO.print(shifted);
String noNewline = """
one line""";
IO.println("[" + noNewline + "]");
}
Imprime:
{
"name": "Ana",
"score": 95
}
{
"name": "Ana"
}
[one line]
Las comillas de dentro no necesitaron barras invertidas. La indentación merece una mirada más atenta, porque el código fuente está indentado 8 espacios y el primer resultado no:
- Se quita la indentación incidental. Java busca la indentación más pequeña entre las líneas de contenido y la línea del
"""de cierre, y quita esa cantidad de cada línea. La indentación de"name"respecto a{se conserva. - El
"""de cierre marca el borde izquierdo. Enshifted, está 4 espacios a la izquierda del contenido, así que cada línea conserva 4 espacios. - El
"""de apertura debe terminar su línea."""hello"""en una sola línea falla conillegal text block open delimiter sequence, missing line terminator. - Un
"""de cierre en su propia línea añade un salto de línea final. Ponlo justo después del texto, como ennoNewline, para evitarlo.
Continuar la línea con \ y conservar espacios con \s
Hay dos escapes que solo existen en los text blocks. Un \ al final de una línea la une con la siguiente, y \s es un espacio que Java no quita:
void main() {
String sql = """
SELECT name, score \
FROM results \
WHERE score > %d \
ORDER BY score DESC""".formatted(90);
IO.println(sql);
String padded = """
red \s
green\s
""";
IO.print(padded.replace(' ', '.'));
IO.println("""
She said "hi", then wrote \""" on the board.
""".strip());
}
Imprime:
SELECT name, score FROM results WHERE score > 90 ORDER BY score DESC
red...
green.
She said "hi", then wrote """ on the board.
El SQL ocupa cuatro líneas en el código fuente y sale como una sola, porque cada \ quitó un salto de línea. formatted funciona sobre un text block como sobre cualquier string.
Java quita los espacios del final de cada línea, porque los editores los agregan y los quitan sin que se vea. \s es un espacio que marca el final de la línea, así que los espacios anteriores se conservan, como muestran los puntos después de red. Para poner tres comillas dentro, escapa la primera como \""".
Arrays: un número fijo de huecos
Un array guarda un número fijo de valores de un mismo tipo, que eliges al crearlo. Los huecos nuevos empiezan con el valor cero del tipo, los mismos valores por defecto que reciben los campos:
void main() {
int[] counts = new int[4];
double[] prices = new double[2];
boolean[] seen = new boolean[3];
String[] names = new String[3];
IO.println(Arrays.toString(counts));
IO.println(Arrays.toString(prices));
IO.println(Arrays.toString(seen));
IO.println(Arrays.toString(names));
int[] scores = {72, 95, 88};
String[] days = new String[] {"Mon", "Tue"};
scores[0] = 75;
IO.println(scores.length + " " + days.length);
IO.println(Arrays.toString(scores) + " " + Arrays.toString(days));
}
Imprime:
[0, 0, 0, 0]
[0.0, 0.0]
[false, false, false]
[null, null, null]
3 2
[75, 95, 88] [Mon, Tue]
{72, 95, 88} crea y llena un array, y el compilador cuenta la longitud. La forma larga, new String[] {...}, hace falta cuando no asignas directamente a una variable declarada, por ejemplo cuando pasas el array a un método.
scores.length no lleva paréntesis: en un array, length es un campo, y en un string es un método. Un array no puede crecer. Para un quinto conteo, tendrías que crear un array nuevo y copiar todo.
Imprimir un array directamente muestra un hash
Los arrays no sobrescriben toString, así que IO.println(scores) no imprime el contenido. Imprime [I@ seguido de algunos dígitos hexadecimales. [I es el nombre interno de Java para “array de int“, y los dígitos vienen de un código hash de identidad, que puede cambiar entre ejecuciones. Un String[] se imprime como [Ljava.lang.String;@ y unos dígitos.
Un detalle nos sorprendió. System.out.println tiene una versión especial para char[] que imprime las letras. IO.println recibe cualquier Object, así que un char[] se imprime como [C@ y un hash. Usa new String(letters).
Un índice fuera de rango lanza una excepción
Java revisa cada acceso a un array, y un índice menor que 0, o igual o mayor que length, lanza ArrayIndexOutOfBoundsException:
void main() {
int[] scores = {72, 95, 88};
IO.println("first: " + scores[0]);
IO.println("last: " + scores[scores.length - 1]);
IO.println("before first: " + scores[-1]);
}
Imprime y se detiene:
first: 72
last: 88
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
Los índices negativos no cuentan desde el final como en Python. La parte sobre control de flujo muestra la otra causa común, un bucle que usa <= en lugar de <.
La clase helper Arrays
java.util.Arrays reúne lo que los arrays no tienen como métodos: ordenar, llenar, copiar y comparar. Un archivo fuente compacto no necesita importarla:
void main() {
int[] scores = {88, 72, 95, 61};
int[] sorted = Arrays.copyOf(scores, scores.length);
Arrays.sort(sorted);
IO.println(Arrays.toString(scores) + " -> " + Arrays.toString(sorted));
String[] words = {"pear", "apple", "Banana"};
Arrays.sort(words);
IO.println(Arrays.toString(words));
int[] bigger = Arrays.copyOf(scores, 6);
int[] smaller = Arrays.copyOf(scores, 2);
IO.println(Arrays.toString(bigger) + " " + Arrays.toString(smaller));
IO.println(Arrays.toString(Arrays.copyOfRange(scores, 1, 3)));
char[] line = new char[10];
Arrays.fill(line, '-');
IO.println(new String(line));
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = a;
IO.println((a == b) + " " + a.equals(b) + " " + Arrays.equals(a, b) + " " + (a == c));
}
Imprime:
[88, 72, 95, 61] -> [61, 72, 88, 95]
[Banana, apple, pear]
[88, 72, 95, 61, 0, 0] [88, 72]
[72, 95]
----------
false false true true
Línea por línea:
Arrays.sortordena en el mismo array y no devuelve nada, así que el programa copióscoresprimero.- Los strings se ordenan con
compareTo, así que"Banana"va primero por suBmayúscula. copyOfrellena con valores cero o corta el final.copyOfRangeusa la misma regla del final excluido quesubstring.fillpone todos los huecos en un mismo valor.==ya.equals(b)comparan referencias, porque los arrays heredanequalsdeObject.Arrays.equalscompara los elementos.a == cestrueporquec = acopió la referencia.
Arrays bidimensionales
Java no tiene un tipo de cuadrícula de verdad. Un array bidimensional es un array cuyos elementos son arrays, y por eso sus filas pueden tener longitudes distintas:
void main() {
int[][] grid = new int[2][3];
grid[1][2] = 7;
IO.println(grid.length + " rows, " + grid[0].length + " columns");
IO.println(Arrays.toString(grid[1]));
IO.println(Arrays.deepToString(grid));
int[][] triangle = {{1}, {1, 1}, {1, 2, 1}};
for (int[] row : triangle) {
IO.println(row.length + ": " + Arrays.toString(row));
}
int[][] first = {{1, 2}, {3, 4}};
int[][] second = {{1, 2}, {3, 4}};
IO.println(Arrays.equals(first, second) + " " + Arrays.deepEquals(first, second));
}
Imprime:
2 rows, 3 columns
[0, 0, 7]
[[0, 0, 0], [0, 0, 7]]
1: [1]
2: [1, 1]
3: [1, 2, 1]
false true
grid[1][2] significa la fila 1 y, dentro de esa fila, la columna 2. grid.length cuenta las filas, y grid[0].length es la longitud de la fila 0.
La última línea atrapa a mucha gente. Arrays.equals compara los elementos externos, que son arrays de filas, con ==, así que dice false. Los arrays anidados necesitan Arrays.deepEquals, igual que se imprimen con Arrays.deepToString.
¿Arrays o List?
Los arrays sirven para un tamaño fijo y conocido: las 64 casillas del ajedrez, los datos de píxeles o el String[] que devuelve split. Para una colección que crece y se encoge, usa una List. Cambia de tamaño, imprime su contenido y tiene un equals que funciona:
void main() {
String[] fixed = {"Ana", "Ben"};
var names = new ArrayList<String>(List.of(fixed));
names.add("Chen");
IO.println(names + " " + names.size());
List<String> view = Arrays.asList(fixed);
view.set(0, "Zoe");
IO.println(fixed[0]);
view.add("Dev");
}
Imprime y se detiene:
[Ana, Ben, Chen] 3
Zoe
Exception in thread "main" java.lang.UnsupportedOperationException
new ArrayList<>(List.of(fixed)) copia el array en una lista que puede crecer. Arrays.asList no copia nada. Envuelve el array en una vista List, así que set también cambió fixed[0], y add lanza una excepción porque el array no puede crecer.
No se permite una List<int>, así que una lista de números guarda objetos Integer con boxing, mientras que un int[] guarda los números directamente. La parte sobre equals, hashCode y colecciones cubre bien List, Set y Map.
Qué recordar
- Un
Stringnunca cambia.s.toUpperCase()por sí solo no hace nada, así que guarda el resultado. substring(begin, end)se detiene antes deend.splitrecibe una expresión regular y descarta los strings vacíos del final.- Compara strings con
equals.compareTopone las mayúsculas antes que las minúsculas. +=en un bucle copia el string entero en cada vuelta, lo que es O(n²). Usa unStringBuildery llama atoStringuna vez.- Un
chares una unidad de código UTF-16. Un emoji tienelength()2, ysubstringpuede partirlo por la mitad. - Los text blocks quitan la indentación hasta el
"""de cierre.\une líneas, y\sconserva un espacio al final. - Los arrays tienen una longitud fija. Imprímelos con
Arrays.toString, compáralos conArrays.equalsy usa unaListcuando el tamaño cambia.
Cada cambio a un String crea un String nuevo, así que construye el texto que crece en un StringBuilder.