Genéricos em Java - Generics in Java

Os genéricos são um recurso de programação genérica que foi adicionado à linguagem de programação Java em 2004 na versão J2SE 5.0. Eles foram projetados para estender o sistema de tipos do Java para permitir "um tipo ou método operar em objetos de vários tipos enquanto fornece segurança de tipo em tempo de compilação". O aspecto segurança do tipo tempo de compilação não foi totalmente alcançado, pois foi demonstrado em 2016 que não é garantido em todos os casos.

A estrutura de coleções Java oferece suporte a genéricos para especificar o tipo de objetos armazenados em uma instância de coleção.

Em 1998, Gilad Bracha , Martin Odersky , David Stoutamire e Philip Wadler criaram o Java Genérico, uma extensão da linguagem Java para suportar tipos genéricos. Java genérico foi incorporado ao Java com a adição de curingas.

Hierarquia e classificação

De acordo com a especificação da linguagem Java :

  • Uma variável de tipo é um identificador não qualificado. Variáveis ​​de tipo são introduzidas por declarações de classes genéricas, declarações de interfaces genéricas, declarações de métodos genéricos e por declarações de construtores genéricos.
  • Uma classe é genérica se declara uma ou mais variáveis ​​de tipo. Essas variáveis ​​de tipo são conhecidas como parâmetros de tipo da classe. Ele define uma ou mais variáveis ​​de tipo que atuam como parâmetros. Uma declaração de classe genérica define um conjunto de tipos parametrizados, um para cada chamada possível da seção de parâmetro de tipo. Todos esses tipos parametrizados compartilham a mesma classe em tempo de execução.
  • Uma interface é genérica se declara uma ou mais variáveis ​​de tipo. Essas variáveis ​​de tipo são conhecidas como parâmetros de tipo da interface. Ele define uma ou mais variáveis ​​de tipo que atuam como parâmetros. Uma declaração de interface genérica define um conjunto de tipos, um para cada chamada possível da seção de parâmetro de tipo. Todos os tipos parametrizados compartilham a mesma interface em tempo de execução.
  • Um método é genérico se declara uma ou mais variáveis ​​de tipo. Essas variáveis ​​de tipo são conhecidas como parâmetros de tipo formal do método. A forma da lista de parâmetros de tipo formal é idêntica a uma lista de parâmetros de tipo de uma classe ou interface.
  • Um construtor pode ser declarado como genérico, independentemente de a classe em que o construtor é declarado ser genérica. Um construtor é genérico se declara uma ou mais variáveis ​​de tipo. Essas variáveis ​​de tipo são conhecidas como parâmetros de tipo formal do construtor. A forma da lista de parâmetros de tipo formal é idêntica a uma lista de parâmetros de tipo de uma classe ou interface genérica.

Motivação

O seguinte bloco de código Java ilustra um problema que existe quando não se usa genéricos. Primeiro, ele declara um ArrayListdo tipo Object. Em seguida, adiciona um Stringao ArrayList. Finalmente, ele tenta recuperar o adicionado Stringe convertê-lo em um Integer- um erro na lógica, pois geralmente não é possível converter uma string arbitrária em um inteiro.

List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
Integer i = (Integer)v.get(0); // Run time error

Embora o código seja compilado sem erros, ele lança uma exceção de tempo de execução ( java.lang.ClassCastException) ao executar a terceira linha do código. Esse tipo de erro lógico pode ser detectado durante o tempo de compilação usando genéricos e é a principal motivação para usá-los.

O fragmento de código acima pode ser reescrito usando genéricos da seguinte maneira:

List<String> v = new ArrayList<String>();
v.add("test");
Integer i = (Integer)v.get(0); // (type error)  compilation-time error

O parâmetro de tipo Stringdentro dos colchetes declara que o ArrayListé constituído de String(um descendente dos constituintes ArrayListgenéricos de Object). Com os genéricos, não é mais necessário converter a terceira linha para qualquer tipo particular, porque o resultado de v.get(0)é definido como Stringpelo código gerado pelo compilador.

A falha lógica na terceira linha deste fragmento será detectada como um erro em tempo de compilação (com J2SE 5.0 ou posterior) porque o compilador detectará esse v.get(0)retorno em Stringvez de Integer. Para um exemplo mais elaborado, consulte a referência.

Aqui está um pequeno trecho da definição das interfaces Liste Iteratorno pacote java.util:

public interface List<E> { 
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> { 
    E next();
    boolean hasNext();
}

Digite curingas

Um argumento de tipo para um tipo parametrizado não está limitado a uma classe ou interface concreta. Java permite o uso de curingas de tipo para servir como argumentos de tipo para tipos parametrizados. Os curingas são argumentos de tipo no formato " <?>"; opcionalmente com um superior ou inferior ligado . Dado que o tipo exato representado por um curinga é desconhecido, restrições são colocadas no tipo de métodos que podem ser chamados em um objeto que usa tipos parametrizados.

Aqui está um exemplo em que o tipo de elemento de a Collection<E>é parametrizado por um caractere curinga:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // compile-time error
c.add(null); // allowed

Como não sabemos o que o tipo de elemento de crepresenta, não podemos adicionar objetos a ele. O add()método aceita argumentos do tipo E, o tipo de elemento da Collection<E>interface genérica. Quando o argumento do tipo real é ?, ele representa algum tipo desconhecido. Qualquer valor de argumento de método que passamos para o add()método deve ser um subtipo desse tipo desconhecido. Como não sabemos que tipo é, não podemos passar nada. A única exceção é nula ; que é um membro de todo tipo.

Para especificar o limite superior de um tipo curinga, a extendspalavra-chave é usada para indicar que o argumento de tipo é um subtipo da classe delimitadora. Isso List<? extends Number>significa que a lista fornecida contém objetos de algum tipo desconhecido que estende a Numberclasse. Por exemplo, a lista pode ser List<Float>ou List<Number>. Ler um elemento da lista retornará a Number. Adicionar elementos nulos também é permitido.

O uso de curingas acima adiciona flexibilidade, uma vez que não há qualquer relação de herança entre quaisquer dois tipos parametrizados com tipo concreto como argumento de tipo. Nem List<Number>nem List<Integer>é um subtipo do outro; embora Integerseja um subtipo de Number. Portanto, qualquer método que tome List<Number>como parâmetro não aceita um argumento de List<Integer>. Se assim fosse, seria possível inserir um Numberque não é um Integernele; que viola a segurança de tipo. Aqui está um exemplo que demonstra como a segurança de tipo seria violada se List<Integer>fosse um subtipo de List<Number>:

List<Integer> ints = new ArrayList<Integer>();
ints.add(2);
List<Number> nums = ints;  // valid if List<Integer> were a subtype of List<Number> according to substitution rule. 
nums.add(3.14);  
Integer x = ints.get(1); // now 3.14 is assigned to an Integer variable!

A solução com curingas funciona porque não permite operações que violariam a segurança de tipo:

List<? extends Number> nums = ints;  // OK
nums.add(3.14); // compile-time error
nums.add(null); // allowed

Para especificar a classe de limite inferior de um tipo curinga, a superpalavra-chave é usada. Esta palavra-chave indica que o argumento de tipo é um supertipo da classe delimitadora. Então, List<? super Number>poderia representar List<Number>ou List<Object>. Leitura de uma lista definida como List<? super Number>elementos de retorno do tipo Object. Adicionar a tal lista requer elementos de tipo Number, qualquer subtipo de Numberou nulo (que é um membro de cada tipo).

O mnemônico PECS (Producer Extends, Consumer Super) do livro Effective Java de Joshua Bloch oferece uma maneira fácil de lembrar quando usar curingas (correspondendo a covariância e contravariância ) em Java.

Definições genéricas de classe

Aqui está um exemplo de uma classe Java genérica, que pode ser usada para representar entradas individuais (chave para mapeamentos de valor) em um mapa :

public class Entry<KeyType, ValueType> {
  
    private final KeyType key;
    private final ValueType value;

    public Entry(KeyType key, ValueType value) {  
        this.key = key;
        this.value = value;
    }

    public KeyType getKey() {
        return key;
    }

    public ValueType getValue() {
        return value;
    }

    public String toString() { 
        return "(" + key + ", " + value + ")";  
    }

}

Essa classe genérica pode ser usada das seguintes maneiras, por exemplo:

Entry<String, String> grade = new Entry<String, String>("Mike", "A");
Entry<String, Integer> mark = new Entry<String, Integer>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<Integer, Boolean>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Ele produz:

grade: (Mike, A)
mark: (Mike, 100)
13 is prime.

Operador diamante

Graças à inferência de tipo , Java SE 7 e superior permitem que o programador substitua um par vazio de colchetes angulares ( <>, chamado de operador de diamante ) por um par de colchetes angulares contendo um ou mais parâmetros de tipo que um contexto suficientemente próximo implica . Assim, o exemplo de código acima usando Entrypode ser reescrito como:

Entry<String, String> grade = new Entry<>("Mike", "A");
Entry<String, Integer> mark = new Entry<>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Definições de métodos genéricos

Aqui está um exemplo de um método genérico usando a classe genérica acima:

public static <Type> Entry<Type, Type> twice(Type value) {
    return new Entry<Type, Type>(value, value);
}

Nota: Se removermos o primeiro <Type>no método acima, obteremos um erro de compilação (não é possível encontrar o símbolo 'Tipo'), pois representa a declaração do símbolo.

Em muitos casos, o usuário do método não precisa indicar os parâmetros de tipo, pois eles podem ser inferidos:

Entry<String, String> pair = Entry.twice("Hello");

Os parâmetros podem ser adicionados explicitamente, se necessário:

Entry<String, String> pair = Entry.<String>twice("Hello");

O uso de tipos primitivos não é permitido, e versões em caixa devem ser usadas em seu lugar:

Entry<int, int> pair; // Fails compilation. Use Integer instead.

Também existe a possibilidade de criar métodos genéricos com base em parâmetros dados.

public <Type> Type[] toArray(Type... elements) {
    return elements;
}

Nesses casos, você também não pode usar tipos primitivos, por exemplo:

Integer[] array = toArray(1, 2, 3, 4, 5, 6);

Genéricos na cláusula de lançamento

Embora as exceções em si não possam ser genéricas, os parâmetros genéricos podem aparecer em uma cláusula throws:

public <T extends Throwable> void throwMeConditional(boolean conditional, T exception) throws T {
    if (conditional) {
        throw exception;
    }
}

Problemas com apagamento de tipo

Os genéricos são verificados em tempo de compilação para correção de tipo. As informações de tipo genérico são então removidas em um processo chamado eliminação de tipo . Por exemplo, List<Integer>será convertido para o tipo não genérico List, que normalmente contém objetos arbitrários. A verificação em tempo de compilação garante que o código resultante tenha o tipo correto.

Devido ao apagamento de tipo, os parâmetros de tipo não podem ser determinados em tempo de execução. Por exemplo, quando um ArrayListé examinado em tempo de execução, não há uma maneira geral de determinar se, antes da eliminação do tipo, era um ArrayList<Integer>ou um ArrayList<Float>. Muitas pessoas estão insatisfeitas com essa restrição. Existem abordagens parciais. Por exemplo, elementos individuais podem ser examinados para determinar o tipo a que pertencem; por exemplo, se um ArrayListcontém um Integer, que ArrayList pode ter sido parametrizado com Integer(no entanto, ele pode ter sido parametrizado com qualquer pai de Integer, como Numberou Object).

Demonstrando esse ponto, o código a seguir resulta em "Igual":

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // evaluates to true
    System.out.println("Equal");
}

Outro efeito do apagamento de tipo é que uma classe genérica não pode estender a classe Throwable de nenhuma forma, direta ou indiretamente:

public class GenericException<T> extends Exception

A razão pela qual isso não é suportado é devido ao apagamento de tipo:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Devido ao apagamento do tipo, o tempo de execução não saberá qual bloco catch executar, portanto, isso é proibido pelo compilador.

Os genéricos Java diferem dos modelos C ++ . Os genéricos Java geram apenas uma versão compilada de uma classe ou função genérica, independentemente do número de tipos de parametrização usados. Além disso, o ambiente de tempo de execução Java não precisa saber qual tipo parametrizado é usado porque as informações de tipo são validadas em tempo de compilação e não estão incluídas no código compilado. Consequentemente, instanciar uma classe Java de um tipo parametrizado é impossível porque a instanciação requer uma chamada a um construtor, que não estará disponível se o tipo for desconhecido.

Por exemplo, o código a seguir não pode ser compilado:

<T> T instantiateElementType(List<T> arg) {
     return new T(); //causes a compile error
}

Como há apenas uma cópia por classe genérica em tempo de execução, as variáveis ​​estáticas são compartilhadas entre todas as instâncias da classe, independentemente de seu parâmetro de tipo. Conseqüentemente, o parâmetro type não pode ser usado na declaração de variáveis ​​estáticas ou em métodos estáticos.

Projeto sobre genéricos

O Projeto Valhalla é um projeto experimental para incubar recursos genéricos e de linguagem Java aprimorados, para versões futuras potencialmente do Java 10 em diante. As melhorias potenciais incluem:

Veja também

Referências

  1. ^ Linguagem de programação Java
  2. ^ A ClassCastException pode ser lançada mesmo na ausência de conversões ou nulos. "Java and Scala's Type Systems are Unsound" (PDF) .
  3. ^ GJ: Java genérico
  4. ^ Especificação da linguagem Java, terceira edição por James Gosling, Bill Joy, Guy Steele, Gilad Bracha - Prentice Hall PTR 2005
  5. ^ Gilad Bracha (5 de julho de 2004). "Genéricos na linguagem de programação Java" (PDF) . www.oracle.com .
  6. ^ Gilad Bracha (5 de julho de 2004). "Genéricos na linguagem de programação Java" (PDF) . www.oracle.com . p. 5
  7. ^ Bracha, Gilad . "Curingas> Bônus> Genéricos" . Os tutoriais Java ™ . Oráculo. ... A única exceção é nula, que é membro de todos os tipos ...
  8. ^ http://docs.oracle.com/javase/7/docs/technotes/guides/language/type-inference-generic-instance-creation.html
  9. ^ Gafter, Neal (2006-11-05). "Reified Generics for Java" . Página visitada em 2010-04-20 .
  10. ^ "Especificação da linguagem Java, seção 8.1.2" . Oracle . Retirado em 24 de outubro de 2015 .
  11. ^ Goetz, Brian. "Bem-vindo ao Valhalla!" . Arquivo de correio OpenJDK . OpenJDK . Retirado em 12 de agosto de 2014 .