Conteúdos
- Motivação
- Generic Methods
- Generic Classes
- Generics Interfaces
- Bounded Generics
- Multiple Bounds
- Wildcards
- Type erasure
Motivação
Generics foi introduzido no Java SE 5.0 para poupar o desenvolvedor de usar casting excessivos durante o desenvolvimento e reduzir bugs durante o tempo de execução.
Olhe no exemplo abaixo um código sem Generics
List list = new LinkedList(); list.add(new Integer(1)); // Na linha abaixo o compilador irá reclamar pois ele não sabe qual o tipo de dado do retorno Integer i = list.iterator().next();
Então para silenciar o compilador, precisamos adicionar um casting:
Integer i = (Integer) list.iterator.next();
Não é garantido que a lista contenha apenas inteiros, pois é possível adicionar outros tipos de objetos nela:
List list = List.of(0, "a");
O que pode provocar uma exceção em tempo de execução.
Seria muito mais fácil especificar apenas uma vez o tipo de objeto que estamos trabalhando tornando o código mais fácil de ser lido, evitando os casts excessivos e também potencias problemas em tempo de execução.
// Na linha abaixo estamos especificando o tipo da nossa Lista List<Integer> list = new LinkedList<>(); list.add(1); Integer i = list.iterator().next();
Generic Methods
Vamos começar com as características de um método genérico, observe o código abaixo:
public static <T, G> Set<G> fromArrayToEvenSet(T[] a, Predicate<T> filterFunction, Function<T, G> mapperFunction) { return Arrays.stream(a) .filter(filterFunction) .map(mapperFunction) .collect(Collectors.toSet()); }
A característica predominante de um método genérico é o diamond operator logo antes da tipagem do retorno da função, onde eles informam os tipos genéricos dos parâmetros que estamos recebendo, no nosso caso T e G
No código acima estamos recebendo na nossa função genérica um array a que pode ser de qualquer tipo (string, int e etc…).
Em seguida estamos recebendo uma função que é responsável por filtrar o conteúdo do array, note que a função é do tipo Predicate pois essa função tem uma única responsabilidade que é retornar true ou false.
Por último estamos recebendo uma função responsável por transformar um objeto em outro, nesse caso a função é do tipo Function pois ele trabalha em cima do objeto T para retornar um objeto diferente que estamos chamando de G.
Segue o exemplo completo:
public class Main { public static void main(String[] args) { Integer[] intArray = {1, 2, 3, 4, 5}; final var stringEvens = fromArrayToEvenSet(intArray, Main::toNumeric, Main::isEven); System.out.println(stringEvens); // [number: 4, number: 2] } private static Numeric toNumeric(final int number){ return new Numeric(number); } public static boolean isEven(final int number){ return number % 2 == 0; } public static <T, G> Set<G> fromArrayToEvenSet(T[] a, Function<T, G> mapperFunction, Predicate<T> filterFunction) { return Arrays.stream(a) .filter(filterFunction) .map(mapperFunction) .collect(Collectors.toSet()); } static class Numeric { private final int number; Numeric(int number) { this.number = number; } @Override public String toString() { return "number: " + this.number; } } }
Generic Classes
Ja vimos anteriormente que um método pode ser genérico mas e se a classe fosse genérica o que aconteceria?
Quando usamos uma classe genérica, precisamos identificar com o diamond operator a quantidade de objetos genéricos que vamos trabalhar, Segue abaixo o exemplo:
public class Example<T> { public void doSomething(final T parameter) { System.out.println("parameter: " + parameter); } }
Se tentarmos adicionar um outro objeto como parâmetro no método, o compilador irá reclamar pois não especificamos ele na classe. Podemos adicionar esse objeto genérico no próprio método com o diamond operator como vimos antes, mas vamos seguir pela classe.
Na classe:
public class Example<T, G> { public void doSomething(final T parameter, final G parameter2) { System.out.println("parameter: " + parameter); System.out.println("parameter2: " + parameter2); } }
Agora precisamos instanciar essa classe e utilizar esse método.
public class Main { public static void main(String[] args) { // Note que para cada uma das instâncias estamos passando objeto diferentes final var integerExample = new Example<Integer, String>(); final var listExample = new Example<List, Double>(); final var doubleExample = new Example<Double, Character>(); // E ao utilizar o método, precisamos respeitar o objeto que espeficifamos na instância. integerExample.doSomething(1, "Olá"); listExample.doSomething(List.of("1", 2, "3", 4), 4.88); doubleExample.doSomething(10.99, 'C'); } }
Generics Interfaces
Como vimos em classes, as interfaces seguem a mesma regra:
public interface ExampleInterface<T> { void doSomething(final T parameter); }
Utilização da interface com o tipo inteiro.
// Note que estamos especificando o tipo que a interface irá receber aqui. public class Example implements ExampleInterface<Integer>{ // E então o método que estamos sobrescrevendo se transforma no mesmo tipo @Override public void doSomething(Integer parameter) { System.out.println("parameter: " + parameter ); } }
Utilização da interface com o tipo Lista.
public class Example implements ExampleInterface<List<String>>{ @Override public void doSomething(List<String> parameter) { System.out.println("parameter: " + parameter ); } }
Bounded Generics
Bounded significa restrito/limitado, podemos limitar os tipos que o método aceita, por exemplo, podemos especificar que o método aceita todas as subclasses ou a superclasse de um tipo, o que também faz com que nesse exemplo, o nosso tipo genérico herde os comportamentos de Number:
public static <T extends Number> Set<Integer> fromArrayToSet(T[] a) { return Arrays.stream(a) .map(Number::intValue) .collect(Collectors.toSet()); }
No exemplo acima, limitamos o tipo genérico T para aceitar apenas subclasses da superclasse Number, então o que aconteceria se tentarmos passar uma lista de String para o nosso parâmetro T[]?
String[] stringArray = {"a", "b", "c"}; // Na linha abaixo o compilador irá reclamar de instâncias inválidas do tipo String para Number final var stringEvens = fromArrayToSet(stringArray);
Multiple Bounds
Como vimos em Bounded Generics podemos limitar quem pode utilizar nosso método genérico, e podemos limitar mais ainda usando interfaces, observe o código abaixo:
public class Main { public static void main(String[] args) { final Person wizard = new Wizard(); final Person muggle = new Muggle(); startWalkAndEat(wizard); startWalkAndEat(muggle); } public static <T extends Person> void startWalkAndEat(T a) { a.walk(); a.eat(); } static class Muggle extends Person {} static class Wizard extends Person implements Comparable{ @Override public int compareTo(Object o) { return 0; } } static class Person { public void walk(){} public void eat(){} } }
Tanto um Trouxa como um Bruxo podem comer e andar porque ambos são pessoas, que foi a restrição que adicionamos, apenas subclasses(classes filhas) de Person e a mesma(classe pai) podem utilizar o método startWalkAndEat() mas e se adicionarmos mais uma restrição em cima da atual, onde apenas as classes que implementam a interface Comparable seriam permitidas, o que aconteceria?
public class Main { public static void main(String[] args) { final Person wizard = new Wizard(); final Muggle muggle = new Muggle(); startWalkAndEat(wizard); // A linha abaixo começa a dar erro de compilação pois a classe muggle não implementa a interface Comparable startWalkAndEat(muggle); } // Adição da nova restrição public static <T extends Person & Comparable> void startWalkAndEat(T a) { a.walk(); a.eat(); } static class Muggle extends Person {} static class Wizard extends Person implements Comparable{ @Override public int compareTo(Object o) { return 0; } } static class Person { public void walk(){} public void eat(){} } }
Como comentado no código, agora não é possível passar a classe Muggle para o método startWalkAndEat(), porque esta classe não implementa a interface Comparable.
Wildcards
Observe o código abaixo:
public class Example<T extends Number> { public long sum(List<T> numbers) { return numbers.stream().mapToLong(Number::longValue).sum(); } }
Utilizaremos ele dessa forma:
public class Main { public static void main(String[] args) { final var example = new Example<>(); List<Number> numbers = new ArrayList<>(); numbers.add(5); numbers.add(10L); numbers.add(15f); numbers.add(20.0); example.sum(numbers); } }
Estamos passando vários tipos de numbers para a lista, mas e se criarmos uma lista de inteiros?
Se inteiros pertence a Number então, não daria problema, certo?
Errado! Por isso nasceu a necessidade de termos o wildcard, segue o código a seguir.
public class Main { public static void main(String[] args) { final var example = new Example(); List<Number> numbers = new ArrayList<>(); numbers.add(5); numbers.add(10L); numbers.add(15f); numbers.add(20.0); // Aqui funciona example.sum(numbers); List<Integer> numbersInteger = new ArrayList<>(); numbersInteger.add(5); numbersInteger.add(10); numbersInteger.add(15); numbersInteger.add(20); // Mas aqui temos um erro de compilação example.sum(numbersInteger); } }
O List<Integer>
e o List<Number>
não estão relacionados como Integer e Number, eles apenas compartilham o pai comum (List<?>).
Então para resolver esse problema podemos utilizar o Wildcard:
public class Example { public long sum(List<? extends Number> numbers) { return numbers.stream().mapToLong(Number::longValue).sum(); } }
Dessa forma ambas as listas irão funcionar.
É importante observar que, apenas com wildcards podemos limitar os parâmetros dos métodos, não é possível fazer isso com generics.
Também seria possível utilizar sem wildcards, da forma a seguir:
public class Example { public <T extends Number> long sum(List<T> numbers) { return numbers.stream().mapToLong(Number::longValue).sum(); } }
Mas dessa forma limitamos T apenas para Numbers. Nesse caso o wildcard seria mais flexível.
Type erasure
Esse mecanismo possibilitou suporte para Generics em tempo de compilação mas não em tempo de execução.
Na prática isso significa que o compilador Java usa o tipo genérico em tempo de compilação para verificar a tipagem dos dados, mas em tempo de execução todos os tipos genéricos são substituídos pelo tipo raw correspondente.
public class MinhaLista<T> { private T[] array; public MinhaLista() { this.array = (T[]) new Object[10]; } public void add(T item) { array[0] = item; } public T get(int index) { return array[index]; } }
Depois da compilação:
public class MinhaLista { private Object[] array; public MinhaLista() { this.array = (Object[]) new Object[10]; } public void add(Object item) { // ... } public Object get(int index) { return array[index]; } }
Para saber mais sobre type erasure, segue o link da baeldung
Top comments (1)
Muito bom o artigo !! Me esclareceu muitas questões que simplesmente não faziam sentido pra mim 👌✌️