Muitos Valores sob um Nome — Arrays

Muitos Valores sob um Nome — Arrays

Arrays em Java: criação, índices, valores padrão, limites vigiados pela JVM, for-each, a variável que guarda uma referência e não o array, cópias, passagem por valor com arrays, a classe Arrays, arrays de arrays, o String[] args e as estatísticas de uma turma.
Java

• • 17 min de leitura

Como guardar e percorrer uma sequência de valores, e por que uma variável de array não guarda o array em si.

Até aqui, cada variável guardou um único valor. Mas muitos problemas lidam com coleções: as notas de uma turma, as temperaturas de um mês, as casas de um tabuleiro. Criar uma variável por valor (nota1, nota2, nota3...) não escala e não permite percorrer os valores com um laço. A estrutura mais básica do Java para isso é o array, uma sequência de tamanho fixo de valores do mesmo tipo. Nesta aula você vai criar, percorrer e manipular arrays, e vai encontrar a primeira consequência séria de uma ideia que vem aparecendo desde "Cada Valor em sua Caixa": uma variável pode guardar não o valor, mas uma referência a ele.

Criando e acessando

Um array é declarado com colchetes depois do tipo. A forma mais direta é escrever os valores entre chaves:

void main() {
    double[] notas = {7.5, 9.0, 4.5, 8.0, 6.5};

    IO.println("Quantidade: " + notas.length);
    IO.println("Primeira: " + notas[0]);
    IO.println("Última: " + notas[notas.length - 1]);

    notas[2] = 5.0;   // substitui o terceiro valor

    double soma = 0;
    for (int i = 0; i < notas.length; i++) {
        soma += notas[i];
    }
    IO.println("Média: " + soma / notas.length);
}
Quantidade: 5
Primeira: 7.5
Última: 6.5
Média: 7.2

Cada valor é um elemento, acessado pela posição, o índice, entre colchetes. Como nos textos de "Operadores, Texto e Conversa com o Usuário", os índices começam em zero: um array de 5 elementos tem índices de 0 a 4. O tamanho fica em notas.length, sem parênteses, porque é um campo e não um método, uma diferença de "Moldes e Instâncias". A última posição é sempre length - 1.

O laço for (int i = 0; i < notas.length; i++) é a forma padrão de percorrer um array pelo índice. Repare no < e não <=: com <=, a última volta tentaria acessar a posição 5, que não existe.

Quando os valores não são conhecidos de antemão, o array é criado com new e um tamanho:

int[] inteiros = new int[3];
double[] reais = new double[2];
boolean[] marcas = new boolean[2];
String[] nomes = new String[2];

IO.println(inteiros[0] + " " + reais[0] + " " + marcas[0] + " " + nomes[0]);
// 0 0.0 false null

Os elementos de um array novo são preenchidos com valores padrão: zero para números, false para boolean, o caractere de código zero para char e null para textos e outros objetos. O null significa "nenhum objeto" e vai ser explicado na próxima aula. Diferente das variáveis locais, que o compilador obriga a inicializar, os elementos de um array sempre têm um valor definido.

O tamanho é fixo e os limites são vigiados

Um array nasce com um tamanho e nunca muda de tamanho. Tentar acessar uma posição que não existe interrompe o programa:

int[] numeros = {10, 20, 30};
IO.println(numeros[3]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Fora.main(Fora.java:3)

Quem vem do C deve apreciar essa mensagem. Em C, ler ou escrever fora dos limites de um vetor é permitido pelo compilador e produz undefined behavior: pode ler lixo, corromper outra variável ou abrir uma brecha de segurança. É a origem de boa parte das vulnerabilidades já descobertas em software escrito em C. A JVM confere cada acesso e, se o índice estiver fora dos limites, para o programa na linha exata, com o índice e o tamanho. Essa verificação tem um custo, que o JIT elimina sempre que consegue provar que o acesso é seguro. É uma troca que quase sempre vale a pena.

for-each: percorrer sem índice

Quando você só precisa de cada valor, e não da posição, existe uma forma mais simples do for, prometida na aula de laços:

String[] dias = {"seg", "ter", "qua"};
for (String dia : dias) {
    IO.print(dia.toUpperCase() + " ");
}
// SEG TER QUA

Lê-se: "para cada dia em dias". Não há índice para errar e o código diz exatamente a intenção. Esta é a forma preferida para percorrer, e foi a usada nos varargs de "Dividir para Resolver".

Um cuidado: a variável do for-each é uma cópia de cada elemento. Alterá-la não altera o array:

int[] nums = {1, 2, 3};
for (int n : nums) {
    n = n * 10;          // altera só a cópia
}
IO.println(Arrays.toString(nums));   // [1, 2, 3]

Para modificar os elementos, use o for com índice e escreva em nums[i].

A variável não é o array

Chegamos ao ponto central da aula. Observe:

void main() {
    int[] original = {1, 2, 3};
    int[] outro = original;
    outro[0] = 99;
    IO.println(Arrays.toString(original));
}
[99, 2, 3]

Alteramos outro e o original mudou. O motivo é que um array é um objeto, e uma variável de array guarda uma referência a ele, uma espécie de endereço, e não os elementos. A linha int[] outro = original copia a referência. Agora há duas variáveis apontando para o mesmo array. Essa situação tem nome: aliasing, dois nomes para a mesma coisa.

original ──┐
           ├──►  [ 99 | 2 | 3 ]
outro ─────┘

É exatamente o que acontecia com o String em "Operadores, Texto e Conversa com o Usuário", só que, como o String é imutável, o compartilhamento não tinha efeitos visíveis. Com arrays, que podem ser alterados, ele tem.

Três consequências práticas:

Copiar exige criar outro array. Para ter uma cópia independente, use Arrays.copyOf:

int[] copia = Arrays.copyOf(original, original.length);
copia[1] = 42;
IO.println(Arrays.toString(original) + " " + Arrays.toString(copia));
// [99, 2, 3] [99, 42, 3]

== compara referências, não conteúdo. Pela mesma razão do String:

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
IO.println(a == b);                  // false: dois arrays diferentes
IO.println(Arrays.equals(a, b));     // true: mesmo conteúdo

Imprimir um array diretamente mostra a referência. IO.println(inteiros) imprime algo como [I@7ff2a664: o [I significa "array de int", e o resto é um código de identificação do objeto. Para ver o conteúdo, use Arrays.toString.

Arrays e métodos: a passagem por valor revisitada

Em "Dividir para Resolver" vimos que os argumentos são passados por valor. Com arrays, o valor copiado é a referência. As duas coisas juntas explicam este resultado, que confunde muita gente:

void main() {
    int[] valores = {1, 2, 3};

    dobrarTodos(valores);
    IO.println(Arrays.toString(valores));   // [2, 4, 6]

    trocarArray(valores);
    IO.println(Arrays.toString(valores));   // [2, 4, 6]
}

void dobrarTodos(int[] numeros) {
    for (int i = 0; i < numeros.length; i++) {
        numeros[i] *= 2;
    }
}

void trocarArray(int[] numeros) {
    numeros = new int[] {0, 0, 0};
}

dobrarTodos recebe uma cópia da referência, mas uma cópia que aponta para o mesmo array. Alterar os elementos através dela altera o array que o main também enxerga. Já trocarArray faz sua cópia da referência apontar para um array novo, o que não afeta a variável do main, que continua apontando para o array antigo.

A regra é uma só e não tem exceção: Java sempre passa por valor. Quando o valor é uma referência, o método pode alterar o objeto apontado, mas não pode fazer a variável de quem o chamou apontar para outro objeto. É a mesma regra que vai valer para todos os objetos a partir da próxima aula.

Isso também explica como um método deve "aumentar" um array. Como o tamanho é fixo, é preciso criar um array maior, copiar e devolver o novo array, para que quem chamou decida guardar a referência:

int[] adicionar(int[] original, int novo) {
    int[] maior = Arrays.copyOf(original, original.length + 1);
    maior[original.length] = novo;
    return maior;
}
int[] lista = {1, 2};
lista = adicionar(lista, 3);   // [1, 2, 3]

Criar um array novo a cada elemento acrescentado é lento para listas grandes. É por isso que, na prática, quase nunca se usa array para coleções que crescem. Para isso existem as listas do Collections Framework, que fazem esse trabalho de forma eficiente e são o assunto de "Listas, Conjuntos e Mapas". Os arrays continuam sendo a base sobre a qual essas listas são construídas, e é por isso que vêm primeiro.

A classe Arrays

A biblioteca padrão tem uma classe utilitária com as operações mais comuns sobre arrays:

int[] numeros = {42, 7, 19, 3, 25};

Arrays.sort(numeros);
IO.println(Arrays.toString(numeros));            // [3, 7, 19, 25, 42]

IO.println(Arrays.binarySearch(numeros, 19));    // 2: a posição do 19

int[] maior = Arrays.copyOf(numeros, 8);         // cópia maior, completa com zeros
Arrays.fill(maior, 5, 8, -1);                    // preenche as posições 5 a 7 com -1
IO.println(Arrays.toString(maior));              // [3, 7, 19, 25, 42, -1, -1, -1]

String[] frutas = {"uva", "Banana", "abacaxi"};
Arrays.sort(frutas);
IO.println(Arrays.toString(frutas));             // [Banana, abacaxi, uva]

Algumas observações. O binarySearch só funciona em arrays ordenados, porque ele descarta metade das posições a cada comparação, uma técnica que você mesmo pode escrever como exercício de recursão. O fill usa a convenção de "início incluído, fim excluído". E a ordenação de textos põe "Banana" antes de "abacaxi" porque compara os códigos dos caracteres, e as maiúsculas vêm antes das minúsculas na tabela Unicode. Ordenar textos do jeito que uma pessoa espera, sem distinguir maiúsculas e respeitando acentos, é assunto de "Ordem e Critério".

Arrays de arrays

Um array pode conter outros arrays, o que serve para representar tabelas e grades:

int[][] tabuleiro = new int[3][3];
tabuleiro[1][1] = 1;
tabuleiro[0][2] = 2;

for (int[] linha : tabuleiro) {
    for (int casa : linha) {
        IO.print(casa + " ");
    }
    IO.println("");
}
0 0 2
0 1 0
0 0 0

tabuleiro[1][1] significa "na linha 1, a coluna 1". Na verdade não há uma matriz: há um array de três elementos, e cada elemento é um array de três inteiros. Por isso as linhas podem ter tamanhos diferentes. O triângulo de Pascal, em que cada número é a soma dos dois acima dele, é um exemplo natural:

int[][] triangulo = new int[4][];            // 4 linhas, sem tamanho definido
for (int i = 0; i < triangulo.length; i++) {
    triangulo[i] = new int[i + 1];           // a linha i tem i + 1 elementos
    triangulo[i][0] = 1;
    triangulo[i][i] = 1;
    for (int j = 1; j < i; j++) {
        triangulo[i][j] = triangulo[i - 1][j - 1] + triangulo[i - 1][j];
    }
}
for (int[] linha : triangulo) {
    IO.println(Arrays.toString(linha));
}
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]

Para imprimir um array de arrays de uma vez, use Arrays.deepToString. O Arrays.toString mostraria só as referências das linhas.

O String[] args do main clássico

Agora dá para entender mais um pedaço da primeira classe do curso, public static void main(String[] args). O parâmetro args é um array de textos com os argumentos de linha de comando, as palavras que você escreve depois do nome do programa. Os arquivos compactos também podem recebê-lo:

void main(String[] args) {
    IO.println("Recebi " + args.length + " argumento(s).");
    for (String arg : args) {
        IO.println("- " + arg);
    }
}
$ java Args.java primeiro "segundo com espaço" 3
Recebi 3 argumento(s).
- primeiro
- segundo com espaço
- 3

As aspas no terminal agrupam palavras num só argumento, e todos chegam como texto, inclusive o 3. Ferramentas de linha de comando, como o javac e o git, recebem suas opções exatamente assim. Faltam as palavras public e static, e elas são explicadas na próxima aula.

Juntando tudo: estatísticas de uma turma

Um programa que lê as notas de uma turma e calcula estatísticas, com um método para cada responsabilidade:

void main() {
    int quantidade = Integer.parseInt(IO.readln("Quantos alunos? "));
    double[] notas = new double[quantidade];

    for (int i = 0; i < notas.length; i++) {
        notas[i] = Double.parseDouble(IO.readln("Nota do aluno " + (i + 1) + ": "));
    }

    double media = media(notas);
    IO.println("Média da turma: %.2f".formatted(media));
    IO.println("Maior nota: " + maior(notas));
    IO.println("Menor nota: " + menor(notas));
    IO.println("Acima da média: " + contarAcimaDe(notas, media));
}

double media(double[] valores) {
    double soma = 0;
    for (double v : valores) {
        soma += v;
    }
    return soma / valores.length;
}

double maior(double[] valores) {
    double resultado = valores[0];
    for (double v : valores) {
        if (v > resultado) {
            resultado = v;
        }
    }
    return resultado;
}

double menor(double[] valores) {
    double resultado = valores[0];
    for (double v : valores) {
        if (v < resultado) {
            resultado = v;
        }
    }
    return resultado;
}

int contarAcimaDe(double[] valores, double limite) {
    int contador = 0;
    for (double v : valores) {
        if (v > limite) {
            contador++;
        }
    }
    return contador;
}
Quantos alunos? 5
Nota do aluno 1: 7.5
Nota do aluno 2: 9
Nota do aluno 3: 4.5
Nota do aluno 4: 8
Nota do aluno 5: 6.5
Média da turma: 7.10
Maior nota: 9.0
Menor nota: 4.5
Acima da média: 3

O array é criado depois de o programa saber quantos alunos há, com new double[quantidade]. O tamanho é fixo depois de criado, mas pode ser decidido durante a execução.

Vale testar o caso extremo: com zero alunos, media divide 0,0 por 0 e devolve NaN, como vimos em "Cada Valor em sua Caixa", e maior tenta ler valores[0] num array vazio e para com ArrayIndexOutOfBoundsException. Um programa robusto precisa decidir o que fazer com uma turma vazia: recusar a entrada, avisar ou definir um resultado. Pensar nos casos extremos, como vazio, um só elemento ou valores repetidos, é um hábito que separa código que funciona no exemplo de código que funciona no mundo real.

Com esta aula termina a primeira fase do curso. Você conhece a história da linguagem e o caminho do código-fonte até o bytecode e a JVM. Sabe guardar valores nos tipos certos e conhece os limites de cada um. Sabe operar sobre eles, formatá-los, ler o que o usuário digita, decidir, repetir, dividir um programa em métodos e guardar sequências de valores em arrays. Com essas ferramentas já dá para escrever programas úteis, e os exercícios destas sete aulas são a prova disso.

Mas repare no programa da turma: as notas estão num array, e se quiséssemos guardar também o nome de cada aluno, precisaríamos de um segundo array, String[] nomes, mantido em paralelo com o primeiro, com a mesma posição valendo para o mesmo aluno. Basta ordenar um deles e esquecer o outro para os dados se misturarem. O que falta é uma forma de agrupar dados que pertencem juntos, o nome e a nota de um aluno, ou o titular e o saldo de uma conta, num único valor, com as operações que fazem sentido sobre ele. Essa forma é a classe, a ideia que faz do Java uma linguagem orientada a objetos.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Sem rodar, diga o que o programa imprime e explique cada linha.

void main() {
    int[] a = {1, 2, 3};
    int[] b = a;
    int[] c = {1, 2, 3};
    b[0] = 99;
    IO.println(a[0]);
    IO.println(a == b);
    IO.println(a == c);
    IO.println(Arrays.equals(b, c));
}
Ver resposta

✓ Resposta: Imprime 99, true, false e false. A atribuição b = a copia a referência, então a e b apontam para o mesmo array, e alterar b[0] altera o que a enxerga: 99. a == b é true porque as duas referências apontam para o mesmo objeto. a == c é false porque c é outro array, embora tenha sido criado com os mesmos valores iniciais. Arrays.equals(b, c) compara conteúdos: b agora é [99, 2, 3] e c é [1, 2, 3], então é false. Antes da alteração, daria true.

Exercício 2

Escreva um método void inverter(int[] numeros) que inverta a ordem dos elementos no próprio array, sem criar outro. Por que o método pode ser void?

Ver resposta

✓ Resposta:

void inverter(int[] numeros) {
    for (int i = 0, j = numeros.length - 1; i < j; i++, j--) {
        int temp = numeros[i];
        numeros[i] = numeros[j];
        numeros[j] = temp;
    }
}

Dois índices partem das pontas e trocam os elementos até se encontrarem no meio. {1, 2, 3, 4, 5} vira {5, 4, 3, 2, 1}. O for pode declarar e atualizar duas variáveis de uma vez, separadas por vírgula. O método pode ser void porque recebe uma cópia da referência que aponta para o mesmo array de quem chamou. As trocas acontecem nesse array, e quem chamou vê o resultado sem que nada precise ser devolvido. Compare com trocar do exercício 2 de "Dividir para Resolver": lá os valores eram primitivos e a troca se perdia. Aqui há um objeto compartilhado.

Exercício 3

Escreva um método int indiceDe(int[] numeros, int procurado) que devolva a posição do valor procurado ou −1 se ele não existir. Por que −1 é uma boa escolha para "não encontrado"?

Ver resposta

✓ Resposta:

int indiceDe(int[] numeros, int procurado) {
    for (int i = 0; i < numeros.length; i++) {
        if (numeros[i] == procurado) {
            return i;
        }
    }
    return -1;
}

indiceDe(new int[] {4, 8, 15, 16, 23, 42}, 16) devolve 3, e procurar 99 devolve −1. O return dentro do laço encerra a busca assim que encontra o valor, sem percorrer o resto. O −1 é uma boa escolha porque nunca é um índice válido, então não se confunde com uma posição real. É a mesma convenção do indexOf de String. A fraqueza dela é que quem chama precisa se lembrar de testar o −1: usar o resultado como índice sem verificar causa ArrayIndexOutOfBoundsException. Formas mais seguras de representar "pode não haver resultado" são o assunto de "Um Valor que Pode Não Existir". Essa busca, que olha elemento por elemento, é a busca linear. Num array ordenado, a busca binária do Arrays.binarySearch é muito mais rápida.

Exercício 4

O método trocarArray da aula não tem efeito nenhum para quem o chama. Reescreva-o de modo que funcione: ele deve fazer quem chamou passar a usar um array novo de três zeros.

Ver resposta

✓ Resposta: Um método não consegue alterar a variável de quem o chamou, porque recebe uma cópia da referência. A solução é devolver o array novo e deixar que quem chamou faça a atribuição:

int[] novoArrayZerado() {
    return new int[] {0, 0, 0};
}
valores = novoArrayZerado();

Se a intenção fosse zerar o array existente, o método poderia alterar os elementos pela referência compartilhada, com Arrays.fill(numeros, 0), e então não precisaria devolver nada. As duas versões fazem coisas diferentes: a primeira cria um objeto novo, a segunda modifica o existente. Se outra variável também apontasse para o array antigo, só a segunda versão a afetaria. Distinguir "trocar o objeto" de "mudar o objeto" é uma das habilidades mais importantes para trabalhar com referências.

Exercício 5

O programa da turma quebra com zero alunos. Modifique o main para que, nesse caso, ele avise "Nenhuma nota para analisar." e termine sem erros. Onde é melhor colocar essa verificação: no main ou em cada método?

Ver resposta

✓ Resposta: No main, logo depois de ler a quantidade:

int quantidade = Integer.parseInt(IO.readln("Quantos alunos? "));
if (quantidade <= 0) {
    IO.println("Nenhuma nota para analisar.");
    return;
}

O return num método void o encerra imediatamente, como vimos em "Dividir para Resolver". O teste <= 0 também cobre números negativos, que fariam new double[quantidade] falhar com uma exceção. Verificar no main é melhor aqui porque é onde a informação chega de fora: validar na entrada impede que dados inválidos se espalhem pelo programa. Os métodos media, maior e menor continuam frágeis para quem os chamar de outro lugar com um array vazio. A forma de um método deixar claro o que ele exige, e reagir quando a exigência não é cumprida, é assunto de "Quando o Contrato se Quebra" e de "Contratos à Vista".

Comentários

Mais em Java

Fazendo Duas Coisas ao Mesmo Tempo — Threads
Fazendo Duas Coisas ao Mesmo Tempo — Threads

O que é uma thread e como criar, esperar e interromper várias delas: start e…

Provando que Funciona — Testes com JUnit e AssertJ
Provando que Funciona — Testes com JUnit e AssertJ

Como escrever testes unitários com JUnit 6 e AssertJ: a estrutura preparar…

Contratos à Vista — Imutabilidade, Programação Defensiva e JSpecify
Contratos à Vista — Imutabilidade, Programação Defensiva e JSpecify

Como uma classe se protege de quem a usa: objetos que não podem ser alterados…