Article image
William Lisboa
William Lisboa01/02/2023 00:11
Compartilhe

Java - Generics

  • #Java

Introdução

Em qualquer projeto de software não trivial, os bugs são simplesmente um fato da vida. Planejamento, programação e testes cuidadosos podem ajudar a reduzir sua difusão, mas de alguma forma, em algum lugar, eles sempre encontrarão uma maneira de invadir seu código. Isso se torna especialmente aparente à medida que novos recursos são introduzidos e sua base de código cresce em tamanho e complexidade.

Felizmente, alguns bugs são mais fáceis de detectar do que outros. Bugs em tempo de compilação, por exemplo, podem ser detectados logo no início; você pode usar as mensagens de erro do compilador para descobrir qual é o problema e corrigi-lo ali mesmo. Bugs de tempo de execução, no entanto, podem ser muito mais problemáticos; eles nem sempre aparecem imediatamente e, quando aparecem, pode ser em um ponto do programa muito distante da causa real do problema.

Os Generics introduzido no J2SE 5.0, concedeu um aprimoramento há muito aguardado do sistema de tipo, que permite que um tipo ou método opere em objetos de vários tipos enquanto fornece segurança de tipos em tempo de compilação. Ele adiciona segurança de tipos em tempo de compilação ao Collections Framework e elimina o trabalho penoso de casting.

Vantagens dos Generics

1. Type-safety: Suponha que você queira armazenar o nome de alguns livros em um ArrayList e por engano, você adicionou um valor Integer em vez de String. O compilador permite, mas o problema ocorre quando você deseja recuperá-lo. Portanto, o compilador lança um erro em tempo de execução. Com o uso de generic, o compilador exibe os erros em tempo de tempo de compilação não em tempo de execução. Economiza tempo do programador porque é difícil encontrar o erro em tempo de execução. É sempre melhor encontrar os erros em tempo de compilação do que em tempo de execução.

2. Typecasting is not required: Typecasting em cada operação de recuperação é uma grande dor de cabeça. Se temos uma lista que guarde os nomes de animais de um zoológico não precisamos fazer casting dos dados todas as vezes que realizarmos uma operação de recuperação, se já sabemos que nossa lista contém apenas dados de String, não precisamos executar Typecasting todas as vezes.

3. Code reuse: Podemos escrever um método genérico, classe e interface. Para que possamos reutilizar o código. Como você pode ver, o Collection framework fornece muitas classes que são genéricas. Você pode usá-los de acordo com seu tipo de dados.

Convenções de nomenclatura de parâmetros de tipo

Por convenção, os nomes dos parâmetros de tipo são letras únicas e maiúsculas. Isso contrasta fortemente com as convenções de nomenclatura de variáveis que você já conhece, e por um bom motivo: sem essa convenção, seria difícil dizer a diferença entre uma variável de tipo e uma classe comum ou nome de interface.

Os nomes de parâmetro de tipo mais comumente usados são:

• E - Element (usado extensivamente pelo Java Collections Framework)

• K - Key

• N - Number

• T - Type

• V - Value

• S,U,V etc. - 2°, 3º, 4º tipos

Você verá esses nomes usados em toda a API Java SE.

Um Exemplo Rápido

Sem os Generics

Como você pode ver na Classe Box abaixo seus métodos aceitam ou retornam um objeto, você pode passar o que quiser, desde que não seja um dos tipos primitivos. Não há como verificar, em tempo de compilação, como a classe é usada. Uma parte do código pode colocar um inteiro no box e esperar obter inteiros dele, enquanto outra parte do código pode passar por engano em uma string, resultando em um erro de tempo de execução.

image

Com os Generics

image

Como você pode ver, todas as ocorrências de Object são substituídas por T. Uma variável de tipo pode ser qualquer tipo não primitivo que você especificar: qualquer tipo de classe, qualquer tipo de interface, qualquer tipo de array ou até mesmo outro tipo de variável.

Invocando e instanciando uma classe de tipo genérico

Para referenciar a classe Box genérica de dentro do seu código, você deve executar uma invocação de tipo genérico, que substitui T por algum valor concreto, como Integer:

image

Você pode pensar em uma invocação de tipo genérico como sendo semelhante a uma invocação de método comum, mas em vez de passar um argumento para um método, você está passando um argumento de tipo — Integer neste caso — para a própria classe Box.

Como qualquer outra declaração de variável, esse código na verdade não cria um novo objeto Box. Ele simplesmente declara que integerBox manterá uma referência a uma "Box de Integer", que é como Box é lido.

Uma invocação de um tipo genérico é geralmente conhecida como um tipo parametrizado. Para instanciar esta classe, use a palavra-chave new, como de costume, mas coloque <Integer> entre o nome da classe e o parêntese:

image

Diamond.

No Java SE 7 e posterior, você pode substituir os argumentos de tipo necessários para chamar o construtor de uma classe genérica por um conjunto vazio de argumentos de tipo (<>), desde que o compilador possa determinar ou inferir os argumentos de tipo a partir do contexto. Este par de colchetes angulares, <>, é informalmente chamado de diamond. Por exemplo, você pode criar uma instância de Box<Integer> com a seguinte instrução:

image

Métodos com Generics

Métodos genéricos são métodos que introduzem seus próprios parâmetros de tipo. Isso é semelhante a declarar um tipo genérico, mas o escopo do parâmetro de tipo é limitado ao método em que é declarado. Métodos genéricos estáticos e não estáticos são permitidos, assim como construtores de classes genéricas.

A sintaxe de um método genérico inclui uma lista de parâmetros de tipo, dentro de colchetes angulares, que aparecem antes do tipo de retorno do método. Para métodos genéricos estáticos, a seção de parâmetro de tipo deve aparecer antes do tipo de retorno do método.

A classe Util inclui um método genérico, compare, que compara dois objetos Pair:

image

Parâmetros de tipo limitado

Pode haver momentos em que você deseja restringir os tipos que podem ser usados como argumentos de tipo em um tipo parametrizado. Por exemplo, um método que opera em números pode querer aceitar apenas instâncias de Number ou suas subclasses. É para isso que servem os parâmetros de tipo limitado.

Para declarar um parâmetro de tipo limitado, liste o nome do parâmetro de tipo, seguido pela palavra-chave extends, seguida por seu limite superior que neste exemplo é Number. Observe que, nesse contexto, extends é usado em um sentido geral para significar "extends" (como em classes) ou "implements" (como em interfaces).

image

Agora vamos instanciar nossa classe Box e executar nosso método inspect.

image

Ao modificar nosso método genérico para incluir esse parâmetro de tipo limitado, a compilação falhará, pois nossa invocação de inspect inclui uma String e ele só aceita um tipo estendido da classe Number. 

Múltiplos Limites

Uma variável de tipo com vários limites é um subtipo de todos os tipos listados no limite. Se um dos limites for uma classe, ele deve ser especificado primeiro.

image

Métodos Genéricos e Parâmetros de Tipo Limitado

Parâmetros de tipo limitado são fundamentais para a implementação de algoritmos genéricos. Considere o seguinte método que conta o número de elementos em uma matriz T[] que são maiores que um elemento especificado elem.

image

A implementação do método é direta, mas não compila porque o operador maior que (>) se aplica apenas a tipos primitivos, como short, int, double, long, float, byte e char. Você não pode usar o operador > para comparar objetos.

Para corrigir o problema, use um parâmetro de tipo limitado pela interface Comparable:

image

Executando um exemplo

image

No código acima declaramos um array de inteiros e uma variável greaterThan4 para armazenar a quantidade de números maiores que quatro presente no array. Depois chamamos o método estático countGreaterThan para retornar à quantidade de números maiores que 4 no array de inteiros, e em seguida imprimimos no console o valor da variável greaterThan4.

Wildcards

No código genérico, o ponto de interrogação (?), chamado wildcard (curinga em português), representa um tipo desconhecido. O wildcard pode ser usado em diversas situações: como o tipo de um parâmetro, campo ou variável local; às vezes como um tipo de retorno (embora seja melhor prática de programação ser mais específico). O wildcard nunca é usado como um argumento de tipo para uma chamada de método genérico, uma criação de instância de classe genérica ou um supertipo.

Wildcard de Limite Superior

Você pode usar um wildcard de limite superior para relaxar as restrições em uma variável. Por exemplo, digamos que você queira escrever um método que funcione em List<Integer>, List<Double> e List<Number>; você pode conseguir isso usando um wildcard de limite superior.

Para declarar um wildcard de limite superior, use o caractere ('?'), seguido pela palavra-chave extends, seguida por seu limite superior. Observe que, nesse contexto, extends é usado em um sentido geral para significar "extends" (como em classes) ou "implements" (como em interfaces).

Para escrever o método que funciona em listas de Number e os subtipos de Number, como Integer, Double e Float, você especificaria List<? extends Number>. O termo List<Number> é mais restritivo do que List<? extends Number> porque o primeiro corresponde apenas a uma lista do tipo Number, enquanto o último corresponde a uma lista do tipo Number ou qualquer uma de suas subclasses.

considere o seguinte método para listar uma lista de objetos do tipo Number ou qualquer uma de suas subclasses.

image

Agora vamos instanciar a classe Sample e executar o método listNumbers.

image

Wildcards Sem Limites

O tipo Wildcard sem limite é especificado usando o caractere (?), por exemplo, List<?>. Isso é chamado de lista de tipo desconhecido. Há dois cenários em que um Wildcard sem limite é uma abordagem útil:

• Se você estiver escrevendo um método que pode ser implementado usando uma funcionalidade fornecida na classe Object.

• Quando o código está usando métodos na classe genérica que não dependem do parâmetro de tipo. Por exemplo, List.size ou List.clear. Na verdade, Class<?> é usado com tanta frequência porque a maioria dos métodos em Class<T> não depende de T.

Considere o seguinte método, printList:

image

O objetivo de printList é imprimir uma lista de qualquer tipo, mas não consegue atingir esse objetivo, imprime apenas uma lista de instâncias de Object; ele não pode imprimir List<Integer>, List<String>, List<Double> e assim por diante, porque eles não são subtipos de List<Object>. Para escrever um método printList genérico, use List<?>:

image

Como para qualquer tipo concreto A, List<A> é um subtipo de List<?>, você pode usar printList para imprimir uma lista de qualquer tipo.

Wildcards de Limite Inferior

A seção Wildcard de Limite Superior mostra que um wildcard de limite superior restringe o tipo desconhecido para ser um tipo específico ou um subtipo desse tipo e é representado usando a palavra-chave extends. De maneira semelhante, um wildcard de limite inferior restringe o tipo desconhecido a ser um tipo específico ou um supertipo desse tipo.

Um wildcard de limite inferior é definido usando o caractere ('?'), seguido pela palavra-chave super, seguida por seu limite inferior:<? super A> .

Digamos que você queira escrever um método que coloque objetos Integer em uma lista. Para maximizar a flexibilidade, você gostaria que o método funcionasse em List<Integer>, List<Number> e List<Object> — qualquer coisa que possa conter valores Integer.

image

O termo List<Integer> é mais restritivo do que List<? super Integer> porque o primeiro corresponde apenas a uma lista do tipo Integer, enquanto o último corresponde a uma lista de qualquer tipo que seja um supertipo de Integer.

Executando addNumbers com uma lista de Integer.

image

Executando addNumbers com uma lista de Number.

image

Executando addNumbers com uma lista de Object.

image

Considerações Finais

Esse artigo descreve os principais pontos dos Generics em java, mas existe outros pontos bastante importantes, que você devia dar uma olhada com mais atenção:

- Type Erasure

- Restrictions on Generics

- Generics, Inheritance, and Subtypes

- Wildcards and Subtyping

- Wildcard Capture and Helper Methods

Os pontos citados acima você pode conferir nessa documentação da própria Oracle: https://docs.oracle.com/javase/tutorial/java/generics/index.html.

Referências:

https://docs.oracle.com/javase/tutorial/extra/generics/index.html

https://www.javatpoint.com/generics-in-java

https://javagoal.com/advantages-of-generics-in-java/

Compartilhe
Comentários (3)
Joana Pereira
Joana Pereira - 02/02/2023 14:44

Excelente artigo, obrigada por compartilhar!

Alisson Camargo
Alisson Camargo - 01/02/2023 09:48

Show, curti muito o conteúdo 🤩

Willian Farias
Willian Farias - 01/02/2023 06:57

Muito interessante, obrigado por compartilhar!