DEV Community

Cover image for Generics com Java
Patrícia Clares
Patrícia Clares

Posted on • Edited on

6 3 3 3

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

Top comments (1)

Collapse
 
maaferreira_meli profile image
Matheus Ferreira De Araujo

Muito bom o artigo !! Me esclareceu muitas questões que simplesmente não faziam sentido pra mim 👌✌️

The best way to debug slow web pages cover image

The best way to debug slow web pages

Tools like Page Speed Insights and Google Lighthouse are great for providing advice for front end performance issues. But what these tools can’t do, is evaluate performance across your entire stack of distributed services and applications.

Watch video

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay