patriciaclares

Patrícia Clares

Posted on May 14, 2023

Generics com Java

Conteúdos

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(); 
Enter fullscreen mode Exit fullscreen mode

Então para silenciar o compilador, precisamos adicionar um casting:

Integer i = (Integer) list.iterator.next();
Enter fullscreen mode Exit fullscreen mode

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

List list = List.of(0, "a");
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

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());
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

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');

    }

}
Enter fullscreen mode Exit fullscreen mode

Generics Interfaces

Como vimos em classes, as interfaces seguem a mesma regra:

public interface ExampleInterface<T> {

    void doSomething(final T parameter);

}
Enter fullscreen mode Exit fullscreen mode

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 );
    }

}
Enter fullscreen mode Exit fullscreen mode

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 );
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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(){}

    }

}
Enter fullscreen mode Exit fullscreen mode

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(){}

    }

}
Enter fullscreen mode Exit fullscreen mode

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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);
  }

}
Enter fullscreen mode Exit fullscreen mode

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);

    }

}
Enter fullscreen mode Exit fullscreen mode

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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];
  }
}
Enter fullscreen mode Exit fullscreen mode

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];
  }
}
Enter fullscreen mode Exit fullscreen mode

Para saber mais sobre type erasure, segue o link da baeldung

💖 💪 🙅 🚩
patriciaclares
Patrícia Clares

Posted on May 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related