Nas suas aplicações Java, você geralmente 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 Vectores para armazenar e gerenciar um grupo de objetos. Mas eles tinham suas próprias desvantagens.

O Framework de Coleções Java 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 Framework de Coleções Java. Os Generics permitem que você garanta a segurança de tipos para objetos armazenados em uma coleção, o que aumenta a robustez de suas aplicações. Você pode ler mais sobre Generics em Java aqui.

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

  • Mecanismos internos

  • Tratamento de duplicatas

  • Suporte para valores nulos

  • Ordenação

  • Sincronização

  • Desempenho

  • Métodos-chave

  • Implementações comuns

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

Sumário:

  1. Compreensão do Framework de Coleções Java

  2. Interfaces de Coleção Java

  3. Classe de Utilitário de Coleções

  4. Conclusão

Compreendendo o Framework de Coleções 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 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, buscar e ordenar objetos de forma eficaz usando o Framework de Coleções 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 implementadora deve fornecer implementações concretas para todos os métodos declarados na interface.

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

Decodificando a Hierarquia do Framework de Coleções Java

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

Vamos começar de cima para baixo para que você possa entender o que este diagrama está mostrando:

  1. No topo do Framework de Coleções 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 ela herda as propriedades e comportamentos da interface Iterable e adiciona seus próprios comportamentos 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 implementando 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 faz 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 Framework de Coleções do Java gira em torno da diferença entre Collection e Collections. Collection é uma interface no framework, enquanto Collections é uma classe de utilitário. A classe Collections fornece métodos estáticos que realizam operações nos elementos de uma coleção.

Interfaces de Coleção do Java

Até agora, 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 dos elementos durante a inserção e seu suporte para 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 é seu desempenho durante operações comuns como inserção, exclusão e recuperação. O desempenho é geralmente expresso usando notação Big-O. Você pode saber mais sobre isso aqui.

Listas

Uma Lista é uma coleção ordenada ou sequencial de elementos. Ela 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 Lista é suportada internamente 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 Lista redimensiona-se dinamicamente com 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 múltiplos 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 Lista 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-chave: Aqui estão alguns métodos-chave de uma interface List: add(E elemento), get(int índice), set(int índice, E elemento), remove(int índice) e tamanho(). Vamos ver como usar 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
             List<String> lista = novo ArrayList<>();
    
             // add(E elemento)
             lista.add("Maçã");
             lista.add("Banana");
             lista.add("Cereja");
    
             // get(int índice)
             String segundoElemento = lista.get(1); // "Banana"
    
             // set(int índice, E elemento)
             lista.set(1, "Mirtilo");
    
             // remove(int índice)
             lista.remover(0); // Remove "Maçã"
    
             // tamanho()
             int tamanho = lista.tamanho(); // 2
    
             // Imprimir a lista
             Sistema.fora.println(lista); // Saída: [Mirtilo, Cereja]
    
             // Imprimir o tamanho da lista
             Sistema.fora.println(tamanho); // Saída: 2
         }
     }
    
  7. Implementações comuns: ArrayList, LinkedList, Vector, Stack

  8. Desempenho: Normalmente, 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ápido no final – O(1) amortizado, lento no início ou meio – O(n) Rápido no início ou meio – O(1), lento no final – O(n)
Exclusão Rápido no final – O(1) amortizado, lento no início ou meio – O(n) Rápido – O(1) se a posição for conhecida
Recuperação Rápido – O(1) para acesso aleatório Lento – 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 é internamente suportado 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. Duplicatas: 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 do TreeSet, onde valores nulos não são permitidos.

  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 na 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 principais: Aqui estão alguns métodos principais de uma interface Set: add(E element), 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("Maçã");
             set.add("Banana");
             set.add("Cereja");
    
             // Remover um elemento do conjunto
             set.remove("Banana");
    
             // Verificar se o conjunto contém um elemento
             boolean containsApple = set.contains("Maçã");
             System.out.println("Contém Maçã: " + 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 desempenho rápido 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ápida – O(1) Rápida – O(1) Mais lenta – O(log n)
Exclusão Rápida – O(1) Rápida – O(1) Mais lenta – O(log n)
Recuperação Rápida – O(1) Rápida – O(1) Mais lenta – O(log n)

Filas

Uma Fila é uma coleção linear de elementos usada para armazenar vários 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 na outra, então o primeiro elemento adicionado à fila é o primeiro a ser removido.

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

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

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

    • ArrayDeque – é implementada usando um array que se expande ou encolhe conforme os 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 que múltiplas instâncias do mesmo valor sejam inseridas

  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 em 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 thread.

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

     import java.util.LinkedList;
     import java.util.Queue;
    
     public class ExemploFila {
         public static void main(String[] args) {
             // Criar uma fila usando LinkedList
             Queue<String> fila = new LinkedList<>();
    
             // Usar o método add para inserir elementos, lança exceção se a inserção falhar
             fila.add("Elemento1");
             fila.add("Elemento2");
             fila.add("Elemento3");
    
             // Usar o método offer para inserir elementos, retorna falso se a inserção falhar
             fila.offer("Elemento4");
    
             // Exibir fila
             System.out.println("Fila: " + fila);
    
             // Espiar o primeiro elemento (não o remove)
             String primeiroElemento = fila.peek();
             System.out.println("Espiar: " + primeiroElemento); // saída "Elemento1"
    
             // Retirar o primeiro elemento (recupera e remove)
             String elementoRetirado = fila.poll();
             System.out.println("Retirar: " + elementoRetirado); // saída "Elemento1"
    
             // Exibir fila após retirar
             System.out.println("Fila após retirar: " + fila);
         }
     }
    
  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ápido no início ou meio – O(1), lento no final – O(n) Mais lento – O(log n) Rápido – O(1), Lento – O(n), se envolver redimensionamento do array interno
Exclusão Rápido – O(1) se a posição for conhecida Mais lento – O(log n) Rápido – O(1), Lento – O(n), se envolver redimensionamento do array interno
Recuperação Lento – O(n) para acesso aleatório, pois envolve percorrer Rápido – O(1) Rápido – O(1)

Mapas

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

  1. Mecanismo interno: Um Map funciona internamente usando uma HashTable com base no conceito de hashing. Escrevi um artigo detalhado sobre este tópico, então leia para uma compreensão mais profunda.

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

  3. Nulo: Como chaves duplicadas não são permitidas, um Map pode ter apenas uma chave nula. Como valores duplicados são permitidos, ele pode ter múltiplos valores nulos. Na implementação do TreeMap, chaves não podem ser nulas porque ele ordena 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 – Os elementos são inseridos com base na ordem natural deles. 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 garantir a segurança em threads.

  6. Métodos chave: Aqui estão alguns métodos chave de uma interface Map: 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 programa de exemplo.

     import java.util.HashMap;
     import java.util.Map;
     import java.util.Set;
    
     public class ExemploMapMethods {
         public static void main(String[] args) {
             // Criar um novo HashMap
             Map<String, Integer> map = new HashMap<>();
    
             // put(K key, V value) - Insere pares chave-valor no mapa
             map.put("Maçã", 1);
             map.put("Banana", 2);
             map.put("Laranja", 3);
    
             // get(Object key) - Retorna o valor associado à chave
             Integer valor = map.get("Banana");
             System.out.println("Valor para 'Banana': " + valor);
    
             // remove(Object key) - Remove o par chave-valor para a chave especificada
             map.remove("Laranja");
    
             // containsKey(Object key) - Verifica se o mapa contém a chave especificada
             boolean temMaca = map.containsKey("Maçã");
             System.out.println("Contém 'Maçã': " + temMaca);
    
             // keySet() - Retorna uma visualização de conjunto das chaves contidas no mapa
             Set<String> chaves = map.keySet();
             System.out.println("Chaves no mapa: " + chaves);
         }
     }
    
  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 mostradas na tabela abaixo.

Operação HashMap LinkedHashMap TreeMap
Inserção Rápida – O(1) Rápida – O(1) Mais lenta – O(log n)
Exclusão Rápida – O(1) Rápida – O(1) Mais lenta – O(log n)
Recuperação Rápida – O(1) Rápida – O(1) Mais lenta – O(log n)

Classe de Utilitário de Coleções

Conforme destacado no início deste artigo, a classe de utilitário 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 algumas características e métodos principais, juntamente com o que fazem, listados brevemente:

  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>, key) – este método é usado para buscar um elemento específico em uma lista ordenada e retornar 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 Min/Max: Collections.min(Collection<T>) e Collections.max(Collection<T>) – esses métodos são usados para encontrar, respectivamente, os elementos mínimo e máximo em uma coleção.

  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, evitando modificações.

Aqui está um exemplo de programa Java 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);

        // Busca
        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 de Mínimo/Máximo
        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, busca, reversão, encontrar valores mínimos e máximos, sincronização e criação de uma lista imutável usando a classe utilitária Collections.

Conclusão

Neste 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, obtendo insights valiosos para escolher as estruturas de dados certas para suas necessidades.

Ao entender esses conceitos, você pode utilizar plenamente o Java Collections Framework, permitindo que 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 se conectar comigo no LinkedIn.