Programação Genérica e Classes de Coleção
As funcionalidades de programação genérica do Java passaram por várias etapas de desenvolvimento.
As versões iniciais do Java não tinham tipos parametrizados,
mas tinham classes para representar estruturas de dados comuns.
Essas classes foram projetadas para trabalhar com Objetos; isto é, elas poderiam conter objetos de qualquer tipo,
e não havia como restringir os tipos de objetos que poderiam ser armazenados em uma determinada estrutura de dados.
Por exemplo, ArrayList
originalmente não era um tipo parametrizado, então qualquer ArrayList
poderia
conter qualquer tipo de objeto. Isso significa que se list
fosse um ArrayList
, então list.get(i)
retornaria um valor do tipo Object
.
Se o programador estivesse usando a lista para armazenar Strings, o valor retornado por list.get(i)
teria que ser convertido para tratar como uma string:
String item = (String)list.get(i);
Isso ainda é um tipo de programação genérica, já que uma classe pode funcionar para qualquer tipo de objeto,
mas estava mais próximo em espírito ao Smalltalk do que ao C++, já que não havia como fazer verificações
de tipo em tempo de compilação. Infelizmente, como no Smalltalk, o resultado é uma categoria de erros que
aparecem apenas em tempo de execução, em vez de em tempo de compilação. Se um programador assume que todos
os itens em uma estrutura de dados são strings e tenta processar esses itens como strings,
um erro de execução ocorrerá se outros tipos de dados foram adicionados inadvertidamente à estrutura de dados.
No Java, o erro provavelmente ocorrerá quando o programa recuperar um Object da estrutura de dados e tentar
convertê-lo para o tipo String. Se o objeto não for realmente do tipo String,
a conversão de tipo ilegal lançará um erro do tipo ClassCastException
.
O Java 5.0 introduziu tipos parametrizados, o que tornou possível criar estruturas de dados genéricas
que podem ser verificadas em tempo de compilação em vez de em tempo de execução.
Por exemplo, se list for do tipo ArrayList
Usarei exclusivamente os tipos parametrizados, mas você deve se lembrar de que seu uso não é obrigatório. Ainda é legal usar uma classe parametrizada como um tipo não parametrizado, como um ArrayList simples. Nesse caso, qualquer tipo de objeto pode ser armazenado na estrutura de dados. (Mas, se isso é o que você realmente deseja fazer, seria preferível usar o tipo ArrayList
Uma classe parametrizada do Java, existe apenas um arquivo de classe compilado.
Por exemplo, existe apenas um arquivo de classe compilado, ArrayList.class, para a classe parametrizada ArrayList.
Os tipos parametrizados ArrayList
Felizmente, a maioria dos programadores não precisa lidar com tais problemas, já que eles aparecem apenas em programação bastante avançada. A maioria das pessoas que usam tipos parametrizados não encontrará os problemas, e elas obterão os benefícios da programação genérica segura de tipo com pouca dificuldade.
Vale a pena notar que, se o parâmetro de tipo em um tipo parametrizado puder ser deduzido pelo compilador,
então o nome do parâmetro de tipo pode ser omitido.
Por exemplo, a palavra “String” é opcional no construtor na seguinte declaração,
porque o ArrayList que é criado deve ser um ArrayList
ArrayList<String> palavras = new ArrayList<>();
O Java Collection Framework (JCF)
Java oferece uma série de tipos parametrizados que implementam estruturas de dados comuns, agrupadas no que chamamos de Java Collection Framework (JCF). O JCF divide-se em duas categorias principais: coleções e mapas.
Coleções
Uma coleção é simplesmente um agrupamento de objetos. No JCF,
coleções são representadas pela interface parametrizada Collection<T>
, onde “T” representa qualquer
tipo de objeto, exceto tipos primitivos.
Existem dois tipos principais de coleções:
- Listas (
List<T>
): Sequências lineares de elementos onde cada elemento tem uma posição. As listas permitem elementos duplicados e são ordenadas pela posição dos elementos. - Conjuntos (
Set<T>
): Coleções que não permitem elementos duplicados. Ao contrário das listas, os conjuntos geralmente não são ordenados de maneira específica.
Mapas
Mapas (Map<K,V>
) associam chaves a valores, semelhante a como um dicionário associa definições a palavras. As chaves são únicas, mas os valores associados a elas podem ser duplicados.
Operações Comuns em Coleções
A interface Collection<T>
define operações básicas aplicáveis a qualquer tipo de coleção, incluindo:
size()
: Retorna o número de elementos na coleção.isEmpty()
: Verifica se a coleção está vazia.add(T elemento)
: Adiciona um elemento à coleção.remove(Object o)
: Remove um elemento da coleção.contains(Object o)
: Verifica se um elemento está na coleção.clear()
: Remove todos os elementos da coleção.
Implementações comuns de Listas e Conjuntos
- **ArrayList
**: Implementa a interface `List ` e oferece uma lista baseada em um array que é redimensionável. - **HashSet
**: Implementa a interface `Set ` e usa uma tabela hash para armazenar os elementos, oferecendo operações rápidas de adição, remoção e verificação de existência.
Eficiência e Uso
A eficiência das operações varia de acordo com a implementação específica da coleção. Por exemplo, ArrayList
oferece acesso rápido a elementos por índice, enquanto LinkedList
é mais eficiente para adicionar ou remover elementos no início ou meio da lista. Escolher a implementação correta é crucial para a eficiência do programa.
Exemplo de Uso
// Criando e manipulando uma ArrayList de Strings
ArrayList<String> frutas = new ArrayList<>();
frutas.add("Maçã");
frutas.add("Banana");
frutas.add("Cereja");
// Iterando sobre a lista
for (String fruta : frutas) {
System.out.println(fruta);
}
// Criando e manipulando um HashSet de inteiros
HashSet<Integer> numeros = new HashSet<>();
numeros.add(1);
numeros.add(2);
numeros.add(3);
// Verificando a presença de um elemento
if (numeros.contains(2)) {
System.out.println("O número 2 está no conjunto.");
}
O JCF oferece uma ampla gama de estruturas de dados e algoritmos genéricos para facilitar a programação em Java, permitindo que desenvolvedores escolham as coleções mais adequadas para suas necessidades específicas.
Métodos de Fábrica Estáticos para Coleções: Introduzidos para List, Set, Map e Map.Entry, esses métodos permitem criar coleções imutáveis de maneira mais concisa.
Exemplo para List:
List<String> friends = List.of("Alice", "Bob", "Charlie");
Exemplo para Set:
Set<String> countries = Set.of("Brazil", "France", "Japan");
Exemplo para Map:
Map<String, Integer> ageOfFriends = Map.of("Alice", 30, "Bob", 25, "Charlie", 22);
Melhorias Locais na Inferência de Tipo com var: Embora não seja uma mudança direta na API de coleções, o uso de var pode simplificar a declaração de variáveis para coleções.
var list = List.of("Java", "Kotlin", "Scala");
Método stream() em Map: Facilita a criação de streams diretamente de entradas de mapas, sem a necessidade de usar entrySet() ou keySet().
Exemplo:
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
map.stream()
.forEach(entry -> System.out.println(entry.getKey() + ":" + entry.getValue()));