Nas suas aplicações Java, normalmente você trabalhará com vários tipos de objetos. E você pode querer realizar operações como ordenação, busca e iteração nesses objetos.

Antes da introdução do framework Collections no JDK 1.2, você teria usado Arrays e Vectors para armazenar e gerenciar um grupo de objetos. Mas eles tinham suas próprias desvantagens.

O Java Collections Framework tem como objetivo superar esses problemas, fornecendo implementações de alto desempenho de estruturas de dados comuns. Isso permite que você se concentre em escrever a lógica da aplicação em vez de se concentrar em operações de baixo nível.

Em seguida, a introdução de Generics no JDK 1.5 melhorou significativamente o Java Collections Framework. Os Generics permitem que você aplique segurança de tipo aos objetos armazenados em uma coleção, o que melhora a robustez das suas aplicações. Você pode ler mais sobre Generics em Java aqui.

Neste artigo, vou orientá-lo sobre como usar o Java Collections Framework. Vamos discutir os diferentes tipos de coleções, como Listas, Conjuntos, Filas e Mapas. Também vou fornecer uma breve explicação de suas principais características, como:

  • Mecanismos internos

  • Tratamento de duplicados

  • Suporte para valores nulos

  • Ordenação

  • Sincronização

  • Desempenho

  • Métodos chave

  • Implementações comuns

Também iremos percorrer alguns exemplos de código para uma melhor compreensão, e vou abordar a classe utilitária Collections e seu uso.

Sumário:

  1. Compreendendo o Framework de Coleções do Java

  2. Interfaces de Coleção Java

  3. Classe de Utilidade de Coleções

  4. Conclusão

Compreendendo o Framework de Coleções do Java

De acordo com a documentação do Java, “Uma coleção é um objeto que representa um grupo de objetos. Um framework de coleções é uma arquitetura unificada para representar e manipular coleções.”

Em termos simples, o Framework de Coleções do Java ajuda a gerenciar um grupo de objetos e realizar operações sobre eles de forma eficiente e organizada. Ele facilita o desenvolvimento de aplicações oferecendo vários métodos para lidar com grupos de objetos. Você pode adicionar, remover, pesquisar e ordenar objetos de forma eficaz usando o Framework de Coleções do Java.

Interfaces de Coleção

Em Java, uma interface especifica um contrato que deve ser cumprido por qualquer classe que a implemente. Isso significa que a classe que implementa deve fornecer implementações concretas para todos os métodos declarados na interface.

No Framework de Coleções do Java, várias interfaces de coleção como Set, List e Queue estendem a interface Collection, e elas devem aderir ao contrato definido pela interface Collection.

Descodificando a Hierarquia do Framework de Coleções do Java

Confira este diagrama interessante deste artigo que ilustra a Hierarquia de Coleções do Java:

Vamos começar do topo e trabalhar para baixo para que você possa entender o que este diagrama está mostrando:

  1. No topo do Framework de Coleções do Java está a interface Iterable, que permite iterar sobre os elementos de uma coleção.

  2. A interface Collection estende a interface Iterable. Isso significa que herda as propriedades e comportamentos da interface Iterable e adiciona seu próprio comportamento para adicionar, remover e recuperar elementos.

  3. Interfaces específicas como List, Set e Queue estendem ainda mais a interface Collection. Cada uma dessas interfaces possui outras classes que implementam seus métodos. Por exemplo, ArrayList é uma implementação popular da interface List, HashSet implementa a interface Set, e assim por diante.

  4. A interface Map é parte do Framework de Coleções do Java, mas não estende a interface Collection, ao contrário das outras mencionadas acima.

  5. Todas as interfaces e classes neste framework fazem parte do pacote java.util.

Nota: Uma fonte comum de confusão no Java Collections Framework gira em torno da diferença entre Collection e Collections. Collection é uma interface no framework, enquanto Collections é uma classe utilitária. A classe Collections fornece métodos estáticos que realizam operações nos elementos de uma coleção.

Interfaces de Coleção Java

Neste ponto, você está familiarizado com os diferentes tipos de coleções que formam a base do framework de coleções. Agora vamos dar uma olhada mais de perto nas interfaces List, Set, Queue e Map.

Nesta seção, discutiremos cada uma dessas interfaces enquanto exploramos seus mecanismos internos. Vamos examinar como lidam com elementos duplicados e se suportam a inserção de valores nulos. Também vamos entender a ordenação de elementos durante a inserção e seu suporte à sincronização, que lida com o conceito de segurança de threads. Em seguida, vamos percorrer alguns métodos-chave dessas interfaces e concluir revisando implementações comuns e seu desempenho para várias operações.

Antes de começarmos, vamos falar brevemente sobre Sincronização e Desempenho.

  • A Sincronização controla o acesso a objetos compartilhados por várias threads, garantindo sua integridade e prevenindo conflitos. Isso é crucial para manter a segurança de threads.

  • Ao escolher um tipo de coleção, um fator importante é o desempenho durante operações comuns como inserção, exclusão e recuperação. O desempenho é geralmente expresso usando a notação Big-O. Você pode aprender mais sobre isso aqui.

Listas

Uma List é uma coleção ordenada ou sequencial de elementos. Segue a indexação baseada em zero, permitindo que os elementos sejam inseridos, removidos ou acessados usando sua posição de índice.

  1. Mecanismo interno: Uma List é internamente suportada por um array ou uma lista encadeada, dependendo do tipo de implementação. Por exemplo, um ArrayList usa um array, enquanto um LinkedList usa uma lista encadeada internamente. Você pode ler mais sobre LinkedList aqui. Uma List redimensiona dinamicamente a si mesma após a adição ou remoção de elementos. A recuperação baseada em indexação a torna um tipo de coleção muito eficiente.

  2. Duplicatas: Elementos duplicados são permitidos em uma List, o que significa que pode haver mais de um elemento em uma List com o mesmo valor. Qualquer valor pode ser recuperado com base no índice em que está armazenado.

  3. Nulo: Valores nulos também são permitidos em uma List. Como duplicatas são permitidas, você também pode ter vários elementos nulos.

  4. Ordenação: Uma List mantém a ordem de inserção, o que significa que os elementos são armazenados na mesma ordem em que são adicionados. Isso é útil quando você deseja recuperar elementos na ordem exata em que foram inseridos.

  5. Sincronização: Uma List não é sincronizada por padrão, o que significa que não possui uma maneira integrada de lidar com o acesso por várias threads ao mesmo tempo.

  6. Métodos principais: Aqui estão alguns métodos principais de uma interface List: add(E elemento), get(int índice), set(int índice, E elemento), remove(int índice) e tamanho(). Vamos ver como utilizar esses métodos com um programa de exemplo.

     importar java.util.ArrayList;
     importar java.util.List;
    
     público classe ExemploLista {
         público estático vazio principal(String[] args) {
             // Criar uma lista
             Lista<String> lista = novo ArrayList<>();
    
             // add(E elemento)
             lista.adicionar("Maçã");
             lista.adicionar("Banana");
             lista.adicionar("Cereja");
    
             // get(int índice)
             String segundoElemento = lista.obter(1); // "Banana"
    
             // set(int índice, E elemento)
             lista.definir(1, "Mirtilo");
    
             // remove(int índice)
             lista.remover(0); // Remove "Maçã"
    
             // tamanho()
             int tamanho = lista.tamanho(); // 2
    
             // Imprimir a lista
             Sistema.paraFora.println(lista); // Saída: [Mirtilo, Cereja]
    
             // Imprimir o tamanho da lista
             Sistema.paraFora.println(tamanho); // Saída: 2
         }
     }
    
  7. Implementações comuns: ArrayList, LinkedList, Vector, Stack

  8. Desempenho: Tipicamente, as operações de inserção e exclusão são rápidas tanto em ArrayList quanto em LinkedList. Mas a recuperação de elementos pode ser lenta, pois é necessário percorrer os nós.

Operação ArrayList LinkedList
Inserção Rápida no final – O(1) amortizado, lenta no início ou meio – O(n) Rápida no início ou meio – O(1), lenta no final – O(n)
Exclusão Rápida no final – O(1) amortizado, lenta no início ou meio – O(n) Rápida – O(1) se a posição for conhecida
Recuperação Rápida – O(1) para acesso aleatório Lenta – O(n) para acesso aleatório, pois envolve percorrer

Conjuntos

Um Set é um tipo de coleção que não permite elementos duplicados e representa o conceito de um conjunto matemático.

  1. Mecanismo interno: Um Set é suportado internamente por um HashMap. Dependendo do tipo de implementação, ele é suportado por um HashMap, LinkedHashMap ou TreeMap. Eu escrevi um artigo detalhado sobre como o HashMap funciona internamente aqui. Não deixe de conferir.

  2. Duplicados: Como um Set representa o conceito de um conjunto matemático, elementos duplicados não são permitidos. Isso garante que todos os elementos sejam únicos, mantendo a integridade da coleção.

  3. Nulo: É permitido no máximo um valor nulo em um Set porque duplicatas não são permitidas. Mas isso não se aplica à implementação de TreeSet, onde valores nulos não são permitidos de forma alguma.

  4. Ordenação: A ordenação dos elementos em um Set depende do tipo de implementação.

    • HashSet: A ordem não é garantida, e os elementos podem ser colocados em qualquer posição.

    • LinkedHashSet: Esta implementação mantém a ordem de inserção, então você pode recuperar os elementos na mesma ordem em que foram inseridos.

    • TreeSet: Os elementos são inseridos com base em sua ordem natural. Alternativamente, você pode controlar a ordem de inserção especificando um comparador personalizado.

  5. Sincronização: Um Set não é sincronizado, o que significa que você pode encontrar problemas de concorrência, como condições de corrida, que podem afetar a integridade dos dados se dois ou mais threads tentarem acessar um objeto Set simultaneamente

  6. Métodos chave: Aqui estão alguns métodos chave de uma interface Set: add(E elemento), remove(Object o), contains(Object o) e size(). Vamos ver como usar esses métodos com um programa de exemplo.

     import java.util.HashSet;
     import java.util.Set;
    
     public class SetExample {
         public static void main(String[] args) {
             // Criar um conjunto
             Set<String> set = new HashSet<>();
    
             // Adicionar elementos ao conjunto
             set.add("Apple");
             set.add("Banana");
             set.add("Cherry");
    
             // Remover um elemento do conjunto
             set.remove("Banana");
    
             // Verificar se o conjunto contém um elemento
             boolean containsApple = set.contains("Apple");
             System.out.println("Contém Apple: " + containsApple);
    
             // Obter o tamanho do conjunto
             int size = set.size();
             System.out.println("Tamanho do conjunto: " + size);
         }
     }
    
  7. Implementações comuns: HashSet, LinkedHashSet, TreeSet

  8. Desempenho: As implementações de Set oferecem alto desempenho para operações básicas, exceto para um TreeSet, onde o desempenho pode ser relativamente mais lento porque a estrutura de dados interna envolve a ordenação dos elementos durante essas operações.

Operação HashSet LinkedHashSet TreeSet
Inserção Rápido – O(1) Rápido – O(1) Mais lento – O(log n)
Deleção Rápido – O(1) Rápido – O(1) Mais lento – O(log n)
Recuperação Rápido – O(1) Rápido – O(1) Mais lento – O(log n)

Filas

Uma Queue é uma coleção linear de elementos usada para armazenar múltiplos itens antes do processamento, geralmente seguindo a ordem FIFO (primeiro a entrar, primeiro a sair). Isso significa que os elementos são adicionados em uma extremidade e removidos da outra, então o primeiro elemento adicionado à fila é o primeiro a ser removido.

  1. Mecanismo interno: O funcionamento interno de uma Queue pode diferir com base em sua implementação específica.

    • LinkedList – usa uma lista duplamente encadeada para armazenar elementos, o que significa que você pode percorrer tanto para frente quanto para trás, permitindo operações flexíveis.

    • PriorityQueue – é sustentada internamente por um heap binário, que é muito eficiente para operações de recuperação.

    • ArrayDeque – é implementada usando um array que expande ou diminui à medida que elementos são adicionados ou removidos. Aqui, os elementos podem ser adicionados ou removidos de ambas as extremidades da fila.

  2. Duplicatas: Em uma Fila, elementos duplicados são permitidos, permitindo a inserção de várias instâncias do mesmo valor

  3. Nulo: Você não pode inserir um valor nulo em uma Fila porque, por design, alguns métodos de uma Fila retornam nulo para indicar que está vazia. Para evitar confusão, valores nulos não são permitidos.

  4. Ordenação: Os elementos são inseridos com base na sua ordem natural. Alternativamente, você pode controlar a ordem de inserção especificando um comparador personalizado.

  5. Sincronização: Uma Fila não é sincronizada por padrão. No entanto, você pode usar uma implementação de ConcurrentLinkedQueue ou BlockingQueue para garantir a segurança de threads.

  6. Métodos principais: Aqui estão alguns métodos principais de uma interface Queue: add(E elemento), offer(E elemento), poll() e peek(). Vamos ver como usar esses métodos com um exemplo de programa.

     import java.util.LinkedList;
     import java.util.Queue;
    
     public class QueueExample {
         public static void main(String[] args) {
             // Cria uma fila usando LinkedList
             Queue<String> queue = new LinkedList<>();
    
             // Usa o método add para inserir elementos, lança exceção se a inserção falhar
             queue.add("Elemento1");
             queue.add("Elemento2");
             queue.add("Elemento3");
    
             // Usa o método offer para inserir elementos, retorna falso se a inserção falhar
             queue.offer("Elemento4");
    
             // Exibe a fila
             System.out.println("Fila: " + queue);
    
             // Olha para o primeiro elemento (não o remove)
             String firstElement = queue.peek();
             System.out.println("Olhar: " + firstElement); // produz "Elemento1"
    
             // Remove o primeiro elemento (recupera e remove)
             String polledElement = queue.poll();
             System.out.println("Remover: " + polledElement); // produz "Elemento1"
    
             // Exibe a fila após a remoção
             System.out.println("Fila após remoção: " + queue);
         }
     }
    
  7. Implementações comuns: LinkedList, PriorityQueue, ArrayDeque

  8. Desempenho: Implementações como LinkedList e ArrayDeque geralmente são rápidas para adicionar e remover itens. A PriorityQueue é um pouco mais lenta porque insere itens com base na ordem de prioridade definida.

Operação LinkedList PriorityQueue ArrayDeque
Inserção Rápida no início ou meio – O(1), lenta no final – O(n) Mais lenta – O(log n) Rápida – O(1), Lenta – O(n), se envolver redimensionamento do array interno
Remoção Rápida – O(1) se a posição for conhecida Mais lenta – O(log n) Rápida – O(1), Lenta – O(n), se envolver redimensionamento do array interno
Recuperação Lenta – O(n) para acesso aleatório, pois envolve percorrer Rápida – O(1) Rápida – O(1)

Mapas

Um Map representa uma coleção de pares de chave-valor, com cada chave mapeando para um único valor. Embora o Map faça parte do framework de Coleções do Java, ele não estende a interface java.util.Collection.

  1. Mecanismo interno: Um Mapa funciona internamente usando um HashTable com base no conceito de hash. Escrevi um artigo detalhado sobre esse tópico, então dê uma lida para uma compreensão mais profunda.

  2. Duplicatas: Um Mapa armazena dados como pares de chave-valor. Aqui, cada chave é única, então chaves duplicadas não são permitidas. Mas valores duplicados são permitidos.

  3. Nulo: Como chaves duplicadas não são permitidas, um Mapa pode ter apenas uma chave nula. Como valores duplicados são permitidos, ele pode ter vários valores nulos. Na implementação do TreeMap, as chaves não podem ser nulas porque ele classifica os elementos com base nas chaves. No entanto, valores nulos são permitidos.

  4. Ordenação: A ordem de inserção de um Map varia de acordo com a implementação:

    • HashMap – a ordem de inserção não é garantida, pois ela é determinada com base no conceito de hash.

    • LinkedHashMap – a ordem de inserção é preservada e você pode recuperar os elementos na mesma ordem em que foram adicionados à coleção.

    • TreeMap – Elementos são inseridos com base na ordem natural. Alternativamente, você pode controlar a ordem de inserção especificando um comparador personalizado.

  5. Sincronização: Um Map não é sincronizado por padrão. Mas você pode usar Collections.synchronizedMap() ou implementações de ConcurrentHashMap para alcançar segurança de thread.

  6. Métodos principais: Aqui estão alguns métodos principais de uma Map interface: put(K key, V value), get(Object key), remove(Object key), containsKey(Object key), e keySet(). Vamos ver como usar esses métodos com um exemplo de programa.

     import java.util.HashMap;
     import java.util.Map;
     import java.util.Set;
    
     public class MapMethodsExample {
         public static void main(String[] args) {
             // Cria um novo HashMap
             Map<String, Integer> map = new HashMap<>();
    
             // put(K key, V value) - Insere pares chave-valor no mapa
             map.put("Apple", 1);
             map.put("Banana", 2);
             map.put("Orange", 3);
    
             // get(Object key) - Retorna o valor associado à chave
             Integer value = map.get("Banana");
             System.out.println("Valor de 'Banana': " + value);
    
             // remove(Object key) - Remove o par chave-valor para a chave especificada
             map.remove("Orange");
    
             // containsKey(Object key) - Verifica se o mapa contém a chave especificada
             boolean hasApple = map.containsKey("Apple");
             System.out.println("Contém 'Apple': " + hasApple);
    
             // keySet() - Retorna uma visão em conjunto das chaves contidas no mapa
             Set<String> keys = map.keySet();
             System.out.println("Chaves no mapa: " + keys);
         }
     }
    
  7. Implementações comuns: HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap

  8. Desempenho: A implementação HashMap é amplamente utilizada principalmente devido às suas características de desempenho eficientes retratadas na tabela abaixo.

Operação HashMap LinkedHashMap TreeMap
Inserção Rápido – O(1) Rápido – O(1) Mais lento – O(log n)
Deleção Rápido – O(1) Rápido – O(1) Mais lento – O(log n)
Recuperação Rápido – O(1) Rápido – O(1) Mais lento – O(log n)

Classe Utilitária Collections

Como destacado no início deste artigo, a classe utilitária Collections possui vários métodos estáticos úteis que permitem realizar operações comumente usadas nos elementos de uma coleção. Esses métodos ajudam a reduzir o código repetitivo em sua aplicação e permitem que você se concentre na lógica de negócios.

Aqui estão alguns recursos e métodos principais, juntamente com o que eles fazem, listados de forma breve:

  1. Ordenação: Collections.sort(List<T>) – este método é usado para ordenar os elementos de uma lista em ordem crescente.

  2. Busca: Collections.binarySearch(List<T>, chave) – este método é usado para buscar um elemento específico em uma lista ordenada e retornar o seu índice.

  3. Ordem reversa: Collections.reverse(List<T>) – este método é usado para inverter a ordem dos elementos em uma lista.

  4. Operações de Mín/Máx: Collections.min(Collection<T>) e Collections.max(Collection<T>) – esses métodos são usados para encontrar os elementos mínimo e máximo em uma coleção, respectivamente.

  5. Sincronização: Collections.synchronizedList(List<T>) – este método é usado para tornar uma lista segura para threads, sincronizando-a.

  6. Coleções Imutáveis: Collections.unmodifiableList(List<T>) – este método é usado para criar uma visão somente leitura de uma lista, impedindo modificações.

Aqui está um programa Java de exemplo que demonstra várias funcionalidades da classe utilitária Collections:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(8);
        numbers.add(1);

        // Ordenação
        Collections.sort(numbers);
        System.out.println("Sorted List: " + numbers);

        // Pesquisa
        int index = Collections.binarySearch(numbers, 3);
        System.out.println("Index of 3: " + index);

        // Ordem Reversa
        Collections.reverse(numbers);
        System.out.println("Reversed List: " + numbers);

        // Operações Min/Max
        int min = Collections.min(numbers);
        int max = Collections.max(numbers);
        System.out.println("Min: " + min + ", Max: " + max);

        // Sincronização
        List<Integer> synchronizedList = Collections.synchronizedList(numbers);
        System.out.println("Synchronized List: " + synchronizedList);

        // Coleções Imutáveis
        List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
        System.out.println("Unmodifiable List: " + unmodifiableList);
    }
}

Este programa demonstra ordenação, pesquisa, reversão, busca de valores mínimos e máximos, sincronização e criação de uma lista imutável usando a classe utilitária Collections.

Conclusão

No artigo, você aprendeu sobre o Framework de Coleções do Java e como ele ajuda a gerenciar grupos de objetos em aplicações Java. Exploramos vários tipos de coleções como Listas, Conjuntos, Filas e Mapas e obtivemos uma visão sobre algumas das características principais e como cada um desses tipos as suporta.

Você aprendeu sobre desempenho, sincronização e métodos chave, adquirindo insights valiosos para escolher as estruturas de dados certas para suas necessidades.

Ao entender esses conceitos, você pode aproveitar ao máximo o Java Collections Framework, permitindo que você escreva um código mais eficiente e construa aplicações robustas.

Se você achou este artigo interessante, sinta-se à vontade para conferir meus outros artigos no freeCodeCamp e conectar-se comigo no LinkedIn.