Generics com Java
Tags: webdev, java, programming, softwareengineering
Published: 2023-05-14 22:57:39

## Conteúdos

- [Motivação](#motiva--o)
- [Generic Methods](#generic-methods)
- [Generic Classes](#generic-classes)
- [Generics Interfaces](#generics-interfaces)
- [Bounded Generics](#bounded-generics)
- [Multiple Bounds](#multiple-bounds)
- [Wildcards](#wildcards)
- [Type erasure](#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

```java
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:

```java
Integer i = (Integer) list.iterator.next();
```

Não é garantido que a lista contenha apenas inteiros, pois é possível adicionar outros tipos de objetos nela:

```java
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.

```java
// 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:

```java
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<T>* 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<T, G>* pois ele trabalha em cima do objeto ***T*** para retornar um objeto diferente que estamos chamando de ***G.***

Segue o exemplo completo:

```java
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:

```java
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*:**

```java
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.

```java
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:

```java
public interface ExampleInterface<T> {

    void doSomething(final T parameter);

}
```

Utilização da interface com o tipo inteiro.

```java
// 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.

```java
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:

```java
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[]?*

```java
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:

```java
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?

```java
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:

```java
public class Example<T extends Number> {

    public long sum(List<T> numbers) {
        return numbers.stream().mapToLong(Number::longValue).sum();
    }

}
```

Utilizaremos ele dessa forma:

```java
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.

```java
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:

```java
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:

```java
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.

```java
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:

```java
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](https://www.baeldung.com/java-type-erasure)