Blog

Strings, text blocks e arrays em Java

Strings em Java nunca mudam, então toda edição cria uma nova. Aprenda os métodos de String e suas armadilhas, por que += num laço copia tudo, StringBuilder, text blocks, Unicode e arrays com os helpers de Arrays.

Uma String em Java não pode ser mudada depois de criada. Todo método que parece editar uma devolve uma string nova. Esse fato explica um bug clássico, um laço lento clássico e por que o StringBuilder existe.

Este post cobre os métodos de String do dia a dia e suas armadilhas, comparação de strings, StringBuilder, Unicode, text blocks e arrays com os helpers de Arrays. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um você mesmo, salve como Main.java e execute java Main.java.

Strings são imutáveis

Um objeto String guarda seus caracteres para sempre, e nenhum método os muda. toUpperCase não transforma uma string em maiúsculas. Ele devolve uma string nova, em maiúsculas, e se você não guardar esse valor de retorno, ele se perde:

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);
}

Ele imprime:

ada
ADA
ada
ADA

A segunda linha de main é o bug. Ela compila, roda e não faz nada, e o javac não avisa, nem com -Xlint:all. Guarde o resultado, como fazem as últimas linhas. Mesmo assim, name = name.toUpperCase() não muda "ada". Faz name se referir a outra string.

Os métodos de String do dia a dia

Quase todo trabalho com strings é achar uma posição, recortar um pedaço ou conferir o que tem dentro. As posições, chamadas de índices, começam em 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("!"));
}

Ele imprime:

12
h
d
world
hello
4
8
-1
true
heLLo, worLd
true false

Algumas regras para tirar dessa saída:

  • O último índice é length() - 1. Uma string de 12 caracteres tem índices de 0 a 11.
  • substring(begin, end) inclui begin e para antes de end. O tamanho do resultado é sempre end - begin. substring(7) vai até o fim.
  • indexOf devolve -1 quando não acha nada. Confira isso antes de usar o resultado como índice.
  • replace troca todas as ocorrências de um texto simples. replaceAll parece igual, mas recebe uma expressão regular.

Um índice fora do intervalo lança uma exceção

substring e charAt conferem seus índices, e um índice inválido para o programa:

void main() {
    String word = "hello";
    IO.println(word.substring(1, 5));
    IO.println(word.substring(2, 9));
}

Ele imprime e para:

ello
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [2, 9) out of bounds for length 5

substring(1, 5) funciona, porque um fim igual ao tamanho quer dizer “até o fim”. A mensagem escreve o intervalo inválido em notação matemática: [2, 9) inclui 2 e exclui 9, que é a regra do fim exclusivo escrita por extenso.

split recebe uma expressão regular

split corta uma string em cada ocorrência de um separador e devolve um array com os pedaços. O separador é uma expressão regular, não texto simples, e isso pega quase todo mundo pelo menos uma 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(",")));
}

Ele imprime:

[a, b, c]
[]
[1, 2, 3]
[r, e, d, |, g, r, e, e, n]
[red, green]
[a, b]
[a, b, , ]
[, a]

Três surpresas, em ordem:

  • split(".") devolve um array vazio. . quer dizer “qualquer caractere”, então todo pedaço fica vazio, e pedaços vazios no final são descartados. Escape o ponto como "\\.".
  • split("|") separa entre todos os caracteres. | quer dizer “ou”, e “nada ou nada” casa em todo lugar. Pattern.quote faz qualquer texto casar literalmente.
  • Strings vazias no final somem. "a,b,," tem quatro campos, mas split(",") devolve dois. As do começo ficam, como ",a" mostra. Passe um limite de -1 para manter todos os campos, por exemplo numa linha de CSV com as últimas colunas vazias.

Aparar, conferir se está em branco, repetir, juntar e formatar

Esses métodos limpam e montam strings. strip, isBlank e repeat chegaram no Java 11, e formatted no 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));
}

Ele 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 e strip concordam em espaços comuns. Discordam em \u2003, um em space, que costuma chegar em texto colado de uma página web. trim remove só os caracteres até o espaço comum na tabela de caracteres, então manteve os dois e devolveu 7 caracteres. strip conhece todos os espaços em branco do Unicode. Em código novo, use strip.

isBlank é verdadeiro para uma string só de espaços em branco, que costuma ser a verificação que você quer para entrada do usuário. isEmpty é verdadeiro só para tamanho 0.

formatted faz o mesmo trabalho que String.format, chamado no próprio padrão. %s é uma string, %d um número inteiro, %.2f duas casas decimais, %-6s alinhado à esquerda em seis colunas, %,d um número com separadores e %% um sinal de porcentagem.

Comparando strings

equals compara duas strings caractere por caractere. A parte sobre valores e referências mostrou por que == é errado para strings: ele confere se é o mesmo objeto, não o mesmo texto. Mais dois métodos cuidam de maiúsculas e de ordem:

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));
}

Ele imprime:

false
true
-1
1
0
-2
-7
25
false

compareTo ordena duas strings. Só o sinal importa: negativo quer dizer que a primeira string vem antes, zero quer dizer iguais, positivo quer dizer depois. O número é a diferença entre os primeiros caracteres que diferem, ou entre os tamanhos, como com "app" e "apple".

"Zebra" vem antes de "apple", porque toda letra maiúscula tem um código de caractere menor que toda letra minúscula. compareToIgnoreCase dá a ordem que uma pessoa espera.

A última linha é um hábito que vale copiar. "Java".equals(missing) devolve false para null, enquanto missing.equals("Java") lançaria NullPointerException.

O Java mantém uma cópia compartilhada de cada literal de string num string pool, então dois literais idênticos são o mesmo objeto e == entre eles por acaso dá true. Strings montadas em tempo de execução, a partir de entrada ou de um StringBuilder, não vão para o pool. É por isso que == em strings passa num teste rápido e falha com dados reais.

Por que += num laço copia tudo

Como uma string não pode crescer, s += "!" cria uma string nova com um caractere a mais, copia para ela cada caractere da antiga, acrescenta "!" e faz s apontar para o resultado. A string antiga vira lixo. Num laço, cada passo copia mais que o anterior.

Este programa conta as cópias em vez de medir o tempo. Para +=, cada passo copia a string atual inteira. Para o StringBuilder, ele conta quantas vezes o builder passou para um buffer maior, e o que copiou nessas vezes:

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));
}

Ele imprime:

+= made 10000 strings and copied 49995000 characters
StringBuilder grew 10 times and copied 18394 characters
same text: true

Os dois laços montam os mesmos 10.000 caracteres. O laço com += copiou 0, depois 1, depois 2, até 9.999, o que dá cerca de n² / 2. Dobre n e as cópias ficam quatro vezes maiores. Isso é O(n²).

Um StringBuilder guarda seus caracteres num buffer com espaço sobrando, e append escreve na próxima vaga livre. Quando o buffer enche, o builder aloca um com cerca do dobro do tamanho e copia tudo uma vez. Isso aconteceu só 10 vezes, então as cópias ficam perto de n.

Uma expressão única como "Hi " + name + ", you have " + n + " messages" não precisa de StringBuilder. Desde o Java 9, o javac compila a expressão inteira numa chamada que monta a string uma vez só. O custo só aparece quando um laço repete +=, porque cada passada começa de novo a partir da string inteira.

Usando um StringBuilder

Um StringBuilder é uma string que você pode mudar. Você vai montando o conteúdo e depois chama toString uma vez para ter uma String comum:

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));
}

Ele imprime:

hello world! x3
Hello world! x has 14 characters
desserts
false
true
true

append aceita strings, caracteres e números e devolve o mesmo builder, então as chamadas se encadeiam. insert coloca texto num índice, e reverse inverte o builder no lugar.

As três últimas linhas escondem uma armadilha. StringBuilder não sobrescreve equals, então dois builders com o mesmo texto não são iguais. Compare as strings deles, ou use String.contentEquals. Você também pode encontrar StringBuffer, uma versão mais antiga e synchronized. Use StringBuilder.

Explicado como se você tivesse dez anos

Uma String é uma placa impressa. Ninguém consegue mudar as letras dela. Para fazer “Sale” virar “Sale!”, você imprime uma placa inteira nova e joga a antiga fora.

Agora acrescente uma letra por dia. Todo dia você imprime uma placa nova com todas as letras antigas mais uma, e a lixeira de reciclagem vai enchendo. No fim do mês, você imprimiu a primeira letra 30 vezes.

Um StringBuilder é uma lousa branca. Você escreve a próxima letra depois da última, e nada é impresso de novo. Quando termina, você imprime uma placa a partir da lousa.

A versão precisa

Uma String guarda um array privado de caracteres em que nada escreve depois da construção. replace, toUpperCase e + alocam, todos eles, uma String nova com um array novo.

Um StringBuilder guarda um array cuja capacidade é pelo menos o seu tamanho. append escreve na parte não usada. Quando uma escrita não cabe, o builder aloca um array maior, com o dobro da capacidade antiga mais 2 neste JDK (16, 34, 70, 142 e assim por diante), e copia os caracteres. Como o tamanho dobra, cada caractere é copiado só algumas vezes em média, então n appends custam O(n). toString copia a parte usada para uma String nova, então mudanças posteriores no builder não chegam nela.

Onde a analogia falha: uma lousa de verdade tem tamanho fixo. Um StringBuilder se troca por um maior quando enche, e essa troca copia tudo, só que raramente. Se você sabe o tamanho final, new StringBuilder(10_000) evita as trocas.

Vendo += copiar e append preencher um buffer

A animação roda s += "!" duas vezes e depois faz o mesmo trabalho com um StringBuilder:

com s += "!" s ref g o g o ! g o ! ! lixo lixo já copiados: 0 caracteres já copiados: 2 caracteres já copiados: 2 + 3 = 5 caracteres com sb.append('!') sb g o ! ! toString() g o ! ! buffer: 8 vagas, usa 2 mesmo buffer, 4 vagas usadas, sem cópia toString(): uma cópia de 4 caracteres s = "go": um objeto String, e s aponta para ele s += "!": uma String nova, com "go" copiado nela s += "!" de novo: os 3 caracteres copiados numa 3ª String um StringBuilder guarda "go" num buffer com folga sb.append('!') 2 vezes: cada ! vai na próxima vaga sb.toString() copia o buffer uma vez, numa String

Cada s += “!” cria uma string nova, copia todos os caracteres da antiga e deixa a antiga como lixo. Um StringBuilder escreve cada ! numa vaga livre de um único buffer, e toString copia uma vez só, no final.

Aqui estão esses passos em palavras, caso a animação não rode para você:

  1. s se refere a uma String que guarda "go".
  2. s += "!" aloca uma string de três caracteres, copia g e o para ela e acrescenta !. Agora s se refere a ela, e o "go" antigo vira lixo. Dois caracteres copiados.
  3. s += "!" de novo aloca quatro caracteres e copia os três de "go!" antes de acrescentar !. "go!" também vira lixo. São 5 caracteres copiados para montar quatro.
  4. Um StringBuilder guarda "go" num buffer de 8 vagas.
  5. sb.append('!') duas vezes escreve ! na vaga 3, depois na vaga 4. É o mesmo buffer, e nada se move.
  6. sb.toString() copia os 4 caracteres para uma única String nova, a única cópia que o builder faz.

Um char nem sempre é um caractere

Um char em Java é uma code unit UTF-16 de 16 bits, não uma letra como uma pessoa a vê. A maioria das letras, inclusive as acentuadas, cabe num char. Emojis ocupam dois, o que se chama surrogate pair, e length() conta 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));
}

Ele imprime:

4 2
1
true
1f44d
👍
4 3
👍ko
ok?

Um code point é o número que o Unicode dá a um caractere, aqui U+1F44D. O joinha é um code point guardado em dois chars, e charAt(0) devolve só a primeira metade, o “high surrogate”.

As duas últimas linhas nos surpreenderam. StringBuilder.reverse mantém os surrogate pairs juntos, então o emoji sobreviveu. substring(0, 3) cortou o par ao meio, e um surrogate sozinho não tem codificação em UTF-8, então o Java imprimiu ?. Cortar texto de usuário num índice fixo pode corromper o texto desse jeito.

Nem os code points contam a história toda. Uma bandeira como 🇮🇳 são dois code points que quem lê vê como um símbolo só. Lembre disso quando for truncar nomes ou contar caracteres contra um limite.

Text blocks: strings de várias linhas sem os escapes

Um text block é um literal de string que ocupa várias linhas, entre dois grupos de três aspas duplas. Ele virou final no Java 15, e javac --release 14 o rejeita com text blocks are not supported in -source 14. Ele deixa JSON, SQL e HTML legíveis:

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 + "]");
}

Ele imprime:

{
    "name": "Ana",
    "score": 95
}
    {
        "name": "Ana"
    }
[one line]

As aspas de dentro não precisaram de barra invertida. A indentação merece um olhar mais atento, porque o código-fonte está indentado com 8 espaços e o primeiro resultado não está:

  • A indentação acidental é removida. O Java acha a menor indentação entre as linhas de conteúdo e a linha do """ de fechamento, e tira essa quantidade de todas as linhas. A indentação de "name" em relação a { sobrevive.
  • O """ de fechamento define a margem esquerda. Em shifted, ele fica 4 espaços à esquerda do conteúdo, então cada linha mantém 4 espaços.
  • O """ de abertura precisa terminar a linha. """hello""" numa linha só falha com illegal text block open delimiter sequence, missing line terminator.
  • Um """ de fechamento na própria linha acrescenta uma quebra de linha final. Coloque-o logo depois do texto, como em noNewline, para evitar isso.

Continuação de linha com \ e espaços mantidos com \s

Dois escapes existem só para text blocks. Um \ no fim de uma linha junta essa linha com a próxima, e \s é um espaço que o Java não remove:

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());
}

Ele imprime:

SELECT name, score FROM results WHERE score > 90 ORDER BY score DESC
red...
green.
She said "hi", then wrote """ on the board.

O SQL ocupa quatro linhas no código-fonte e sai como uma, porque cada \ removeu uma quebra de linha. formatted funciona num text block como em qualquer string.

O Java remove os espaços no fim de cada linha, porque editores os acrescentam e removem sem ninguém ver. \s é um espaço que marca o fim da linha, então os espaços antes dele sobrevivem, como mostram os pontos depois de red. Para colocar três aspas dentro, escape a primeira como \""".

Arrays: um número fixo de posições

Um array guarda um número fixo de valores de um tipo, escolhido quando você o cria. As posições novas começam com o valor zero do tipo, os mesmos valores padrão que os campos recebem:

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));
}

Ele 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} cria e preenche um array, e o compilador conta o tamanho. A forma longa, new String[] {...}, é necessária quando você não está atribuindo direto a uma variável declarada, como quando passa o array para um método.

scores.length não tem parênteses: num array, length é um campo, e numa string é um método. Um array não cresce. Para uma quinta contagem, você criaria um array novo e copiaria tudo.

Imprimir um array direto mostra um hash

Arrays não sobrescrevem toString, então IO.println(scores) não imprime o conteúdo. Imprime [I@ seguido de alguns dígitos hexadecimais. [I é o nome interno do Java para “array de int“, e os dígitos vêm de um identity hash code, que pode mudar entre execuções. Um String[] imprime como [Ljava.lang.String;@ seguido de dígitos.

Um detalhe nos surpreendeu. System.out.println tem uma versão especial para char[] que imprime as letras. IO.println aceita qualquer Object, então um char[] imprime como [C@ e um hash. Use new String(letters).

Um índice fora do intervalo lança uma exceção

O Java confere todo acesso a array, e um índice abaixo de 0, ou igual ou maior que length, lança 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]);
}

Ele imprime e para:

first: 72
last: 88
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3

Índices negativos não contam a partir do fim, como em Python. A parte sobre fluxo de controle mostra a outra causa comum, um laço que usa <= em vez de <.

A classe helper Arrays

java.util.Arrays reúne o que os arrays não têm como métodos: ordenar, preencher, copiar e comparar. Um arquivo-fonte compacto não precisa de import para ela:

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));
}

Ele 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

Linha por linha:

  • Arrays.sort ordena no lugar e não devolve nada, então o programa copiou scores antes.
  • Strings são ordenadas por compareTo, então "Banana" vem primeiro por causa do B maiúsculo.
  • copyOf completa com valores zero ou corta o final. copyOfRange usa a mesma regra do fim exclusivo que substring.
  • fill coloca um mesmo valor em todas as posições.
  • == e a.equals(b) comparam referências, porque arrays herdam equals de Object. Arrays.equals compara os elementos. a == c é true porque c = a copiou a referência.

Arrays bidimensionais

O Java não tem um tipo de grade de verdade. Um array bidimensional é um array cujos elementos são arrays, e é por isso que as linhas podem ter tamanhos diferentes:

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));
}

Ele 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] quer dizer linha 1, depois coluna 2 dessa linha. grid.length conta as linhas, e grid[0].length é o tamanho da linha 0.

A última linha pega muita gente. Arrays.equals compara os elementos de fora, que são arrays de linha, com ==, então diz false. Arrays aninhados precisam de Arrays.deepEquals, assim como são impressos com Arrays.deepToString.

Arrays ou List?

Arrays servem para um tamanho fixo e conhecido: 64 casas de xadrez, dados de pixels ou o String[] que split devolve. Para uma coleção que cresce e diminui, use uma List. Ela muda de tamanho, imprime seu conteúdo e tem um 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");
}

Ele imprime e para:

[Ana, Ben, Chen] 3
Zoe
Exception in thread "main" java.lang.UnsupportedOperationException

new ArrayList<>(List.of(fixed)) copia o array para uma lista que pode crescer. Arrays.asList não copia nada. Ele embrulha o array numa view de List, então set mudou fixed[0] também, e add lança uma exceção porque o array não pode crescer.

Uma List<int> não é permitida, então uma lista de números guarda objetos Integer com boxing, enquanto um int[] guarda os números diretamente. A parte sobre equals, hashCode e coleções trata de List, Set e Map como se deve.

O que lembrar

  • Uma String nunca muda. s.toUpperCase() sozinho não faz nada, então guarde o resultado.
  • substring(begin, end) para antes de end. split recebe uma expressão regular e descarta strings vazias no final.
  • Compare strings com equals. compareTo coloca maiúsculas antes de minúsculas.
  • += num laço copia a string inteira a cada passada, o que é O(n²). Use um StringBuilder e chame toString uma vez.
  • Um char é uma code unit UTF-16. Um emoji tem length() 2, e substring pode cortá-lo ao meio.
  • Text blocks removem a indentação até o """ de fechamento. \ junta linhas, e \s mantém um espaço no final.
  • Arrays têm tamanho fixo. Imprima com Arrays.toString, compare com Arrays.equals e use uma List quando o tamanho muda.

Toda mudança numa String cria uma String nova, então monte texto que cresce num StringBuilder.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.