← Java: linguagem e decisões de aplicação
05 / 10 · 45 MIN

Coleções, generics e chaves

Escolhe estruturas, cópias e assinaturas que preservam ordem, identidade, propriedade dos dados e regras de repetição.

Escolher a estrutura pelo contrato

Uma lista representa uma sequência: a ordem, a posição e as repetições podem ter significado. Um mapa associa cada chave a um valor. Um conjunto representa pertença sem duplicados, segundo a sua regra de equivalência. Antes de trocar uma estrutura por outra, escreve o que precisa de sobreviver. Um recibo com A, B, A perde informação se eliminar a segunda ocorrência de A. Um índice por identificador pode querer exatamente o contrário: detetar essa repetição e recusá-la.

Map.put não implementa por si só a política de rejeitar duplicados. Num mapa vazio, put("A", 3) seguido de put("A", 8) deixa uma associação e valor 8. O registo anterior foi substituído. Se a política é preservar a primeira ocorrência, a aplicação deve escolher a operação e a condição adequadas, considerando também se null é permitido.

O tipo da interface não promete todas as características da implementação. List não significa necessariamente lista modificável; Map não promete uma ordem de iteração comum a todas as implementações. A escolha deve especificar ordem, mutabilidade, tratamento de null e propriedade dos elementos. Nos excertos, assume os imports java.util necessários.

Operações parecidas, efeitos diferentes

Uma List<Integer> tem dois remove com significados diferentes. remove(int) usa uma posição; remove(Object) procura um valor. Para uma ArrayList com 4, 9, 1, remove(1) remove 9 e deixa 4, 1. Já remove(Integer.valueOf(1)) procura o valor 1 e deixa 4, 9. O tipo do argumento escolhe o overload. Antes de aceitar uma correção, usa um exemplo em que posição e valor levem a resultados diferentes.

Arrays.asList liga uma lista ao array recebido. Com String[] raw = {"east", "west"}, substituir view.set(0, "north") também altera raw[0]. Alterar raw[1] é visível por view.get(1). A lista tem tamanho fixo, pelo que add que aumentaria o tamanho falha. Tamanho fixo não é o mesmo que proibir set.

List.of e List.copyOf produzem listas não modificáveis e não admitem elementos null. new ArrayList<>(List.of(...)) cria outra estrutura, modificável; não herda a restrição de modificação da origem. Estes detalhes decidem se uma operação é válida, lança uma exceção ou altera um objeto que outro componente ainda está a usar.

Vista, cópia e elementos partilhados

Collections.unmodifiableList(staging) impede a alteração através da referência devolvida. Continua, contudo, a ser uma vista da lista de suporte. Se o produtor esvaziar staging, o consumidor observa essa mudança. Este mecanismo é apropriado para uma vista viva com controlo de acesso; não satisfaz um recibo que tem de conservar o estado anterior.

List.copyOf(staging) conserva os elementos pela ordem da origem e não reflete alterações posteriores à estrutura dessa origem. A lista devolvida não permite add, remove ou set. A fábrica pode reutilizar uma lista adequada; não bases o contrato em == ou na promessa de uma alocação nova. A questão útil é que alterações permanecem observáveis.

Nenhuma cópia estrutural é uma cópia profunda automática. Se o elemento for um StringBuilder partilhado, acrescentar texto nesse objeto é visível nas duas listas. Se forem strings, o seu conteúdo é imutável. Por isso, no caso do recibo, List.copyOf satisfaz a estabilidade pedida com elementos String não nulos e origem estável durante a cópia. Com elementos mutáveis seria necessário decidir que dados copiar para valores independentes.

Generics e segurança das fronteiras

Integer é um Number, mas List<Integer> não é List<Number>. Se essa atribuição fosse permitida, um método que recebe List<Number> poderia adicionar Double à lista que o chamador continua a usar como List<Integer>. A verificação protege esse contrato antes de ler ou inserir elementos; a lista estar vazia não altera os tipos.

Uma assinatura deve expressar a utilização necessária. Um método que lê números de várias listas pode receber List<? extends Number>. O tipo concreto do elemento é desconhecido, mas está limitado a Number: get pode ser lido como Number. Acrescentar Integer não é seguro através dessa referência, porque a lista concreta poderia conter apenas Double. Também não é possível atribuir diretamente get a uma variável Integer sem informação adicional.

Evita resolver a rejeição retirando os parâmetros de tipo ou forçando casts. Isso troca uma fronteira explícita por uma confiança que pode falhar mais tarde. A melhoria é restringir o que o método pode fazer ao necessário para a tarefa. Os tutoriais oficiais citados nesta parte documentam regras estáveis de generics; os exemplos desta aula usam a base Java SE 25.

Produzir valores e receber valores

Para um destino que deve receber Integer, List<? super Integer> aceita listas de Integer, Number ou Object. O método pode acrescentar Integer com segurança de tipos porque qualquer desses destinos o aceita. O caminho inverso tem menos informação: uma leitura arbitrária desse destino só está garantida como Object, porque a lista pode conter outros tipos. A disponibilidade de add em execução continua a depender de a implementação permitir a operação.

O atalho habitual associa extends à origem que fornece valores e super ao destino que os recebe. Usa-o para raciocinar sobre operações, não como promessa de imutabilidade. List<? extends Number> ainda permite escrever uma chamada clear do ponto de vista dos tipos; uma ArrayList pode esvaziar-se e uma lista não modificável pode rejeitar a operação. Ler get(0) também pode compilar e falhar por não haver posição zero.

Na seleção de uma assinatura, faz três verificações separadas: aceita os tipos de origem/destino exigidos, permite as operações do corpo e respeita as capacidades da coleção concreta. Uma resposta que só compila não demonstra todos os requisitos de utilização.

Chaves estáveis e equivalência de ordenação

Uma chave de mapa deve manter estáveis os dados usados na igualdade enquanto permanecer no mapa. Se equals e hashCode dependem de um campo alterado por um setter, não há uma garantia geral de que a pesquisa continue correta. Também não se deve substituir esta conclusão por “get devolve sempre null”: o contrato não promete esse resultado específico. Uma chave independente e estável, como um identificador String, evita depender da evolução de um objeto de negócio mutável.

Num TreeSet, a comparação usada para ordenar também define se dois elementos são considerados equivalentes. Se o comparador usa apenas o comprimento, east e west são equivalentes para esse conjunto, apesar de String.equals os distinguir. A segunda adição não aumenta o tamanho. Se o requisito exige conservar ambas as strings, a equivalência escolhida está errada.

Um desempate pelo conteúdo completo pode distinguir strings de igual comprimento; outra opção é manter uma lista ordenada quando as repetições são significativas. Não escolhas TreeSet apenas porque a saída precisa de estar ordenada: confirma primeiro a regra de pertença. A ordem e a unicidade são decisões relacionadas, mas diferentes.

Presença, null e deteção de duplicados

Um HashMap admite valores null. Por isso, get(id) == null pode significar chave ausente ou chave presente sem valor conhecido. containsKey distingue os casos. A diferença é decisiva no parser que usa null como estado pendente: o identificador R7 já foi visto mesmo antes de existir o seu estado final.

putIfAbsent considera uma associação a null elegível para receber o novo valor. Não serve, portanto, como prova de que o identificador nunca existiu neste modelo. O exemplo abaixo consulta primeiro containsKey; se a chave existir, devolve DUPLICATE e não faz put. Se estiver ausente, guarda o valor, incluindo null, e devolve NEW. A política é conservar a primeira associação e registar separadamente a nova ocorrência.

Esta sequência é analisada num mapa privado, sem concorrência nem chamadas externas entre a verificação e a escrita. Não é uma solução de coordenação para vários threads. Se dois participantes puderem escrever, é preciso redesenhar a fronteira e a representação do estado segundo o contrato da implementação escolhida. As respostas desta aula são selecionadas por leitura; não exigem executar o parser.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

final class SeenStates {
    private final Map<String, String> states = new HashMap<>();

    // Sequential owner only. A null value means pending.
    String register(String id, String value) {
        Objects.requireNonNull(id);
        if (states.containsKey(id)) return "DUPLICATE";
        states.put(id, value);
        return "NEW";
    }
}
NA PRÁTICA

Num SeenStates novo, register("R7", null) devolve NEW; depois register("R7", "accepted") devolve DUPLICATE e a associação continua a null. register("R8", "accepted") devolve NEW. A análise assume um único dono sequencial e não executa código.

Armadilhas comuns

Confundir vista não modificável com snapshot; confundir remove por índice com remove por valor; tratar extends como imutabilidade; alterar chaves presentes; interpretar null como ausência sem verificar o contrato.

Tópicos relacionados: Métodos, contratos e fluxo de controlo · Objetos, interfaces e records · Streams, lambdas e ausência · Concorrência, estado e cancelamento

Leva esta ideia contigo

Verifica separadamente tipos, capacidades de alteração, propriedade dos elementos e equivalência. O nome de uma coleção não decide sozinho a política do domínio.

Criar conta

Referência: Java 25 API: List · Java SE 25; no preview features