Genéricos Java explicados resumidamente com exemplos

Consulte estas perguntas frequentes para obter mais informações:

http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html#SECTION1

Aqui resumimos alguns pontos importantes deste FAQ.

Exemplo (de tipo genérico):

classe Par<X,Y> { 
  privado X primeiro;
  privado Y segundo;   public par (X a1, Y a2) {
    primeiro = a1;
    segundo = a2;
  }
  public X getFirst() { return first; }
  public Y getSegundo() { return segundo; }
  public void setFirst(X arg)  { primeiro = arg; }
  public void setSecond(Y arg) { segundo = arg; }
}

A aula Par tem dois parâmetros de tipo X e S .  São substituídos por argumentos de tipo quando o tipo genérico Par é instanciado. Por exemplo, na declaração Par<String, Data> O parâmetro de tipo X é substituído pelo argumento de tipo Corda e S é substituído por  Data .
O amido dos identificadores X e S é toda a definição da classe.  Neste âmbito os dois parâmetros de tipo X e S são utilizados como se fossem tipos (com algumas restrições). No exemplo acima, os parâmetros do tipo são utilizados como argumento e o tipo de retorno são dois métodos da instância e dois tipos de campos da instância.
Os parâmetros de tipo podem ser declarados com limites. Os limites dão acesso a métodos de tipo desconhecido que o parâmetro de tipo representa. No nosso exemplo, não invocamos nenhum método dos tipos desconhecidos X e S . Por este motivo, os dois parâmetros de tipo são ilimitados.

Exemplo (de um tipo parametrizado concreto):

public void imprimePair( Par<String,Long> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<String,Long> limite = novo Par<String,Long> ("máximo",1024L);
printPair(limite);

Exemplo (de um tipo wildcard parametrizado):

public void imprimePair( Par<?,?> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<?,?> limite = novo Par<String,Long> ("máximo",1024L);
printPair(limite);

Todas as instanciações de um tipo genérico partilham a mesma representação do tipo de tempo de execução, ou seja, a representação do tipo bruto. Por exemplo, as instanciações de um tipo genérico Lista , como Lista<Data> , Lista<String> , Lista<Long> , etc. têm tipos estáticos diferentes em tempo de compilação, mas o mesmo tipo dinâmico Lista em tempo de execução.

Uma lista é constitutiva por duas partes:

  • uma verificação estática de tipo realizada pelo compilador em tempo de compilação e
  • uma verificação dinâmica de tipo realizada pela máquina virtual em tempo de execução.

A parte estática classifica as conjurações sem sentido, que não podem ter sucesso, como a conjuração de Corda que Data ou de Lista<String> que Lista<Data> .

A parte dinâmica utiliza informação do tipo de tempo de execução e executa uma verificação do tipo de tempo de execução.  Isto levanta um ClassCastException se o tipo dinâmico do objeto não for o tipo de destino (ou um subtipo do tipo de destino) da conversão. Exemplos de moldes com parte dinâmica são os moldes de Objeto que Corda ou de Objeto que Lista<String> .  São os chamodos abatidos, desde um supertipo até um subtipo.

Exemplos de tipos parametrizados concretos são Lista<String> , Mapa<String,Data> , mas não Lista<? estende o número> ou Mapa<String,?> .

Por vezes espera-se que um Lista<Object> seria um supertipo de um Lista<String> , porque Objeto é um supertipo de Corda .  Esta expectativa decorre do facto de existir tal tipo de relação para arrays:  Objeto[] é um supertipo de Corda[] , porque Objeto é um supertipo de Corda . (Este tipo de relacionamento é conhecido como covariância .) A relação de supersubtipo dos tipos de componentes estende-se aos tipos de array correspondentes. Não existe tal relação de tipo para instanciações de tipos genéricos. (Tipos parametrizados são não covariável.)

A falta de uma relação de supersubtipo entre instanciações do mesmo tipo genérico tem várias consunções.  Aqui está um exemplo.

Exemplo:

void printAll(ArrayList<Object> c) {
  para (Objecto o: c) 
    System.out.println(o);
} ArrayList<String> lista = new ArrayList<String>();
... preencher lista ...
imprimirTodos(lista);   //erro

UM ArrayList<String> objeto não pode ser passado como argumento para um método que solicita um ArrayList<Object> porque os dois tipos são instanciações do mesmo tipo genérico, mas para diferente argumentos de tipo e, por esse motivo, não são compatíveis entre si.

Os tipos parametrizados específicos são instâncias concretas de um tipo genérico.  São quase como tipos; existem apenas algumas restrições.  NÃO podem ser utilizados para os seguintes fins:

  • para criação de matrizes
  • no tratamento de exceções
  • numa classe literal
  • num instância de expressão

As matrizes que contêm elementos cujo tipo é um tipo parametrizado concreto são ilegais.

Exemplo (o tipo de array é ilegal):

teste estático de vazio() {
  Par<Inteiro,Inteiro>[] intPairArr = novo Par<Inteiro,Inteiro>[10] ; //erro
  addElements(intPairArr); 
  Pair<Integer,Integer> par = intPairArr[1];
  Inteiro i = par.getFirst();
  par.setSecond(i);
}
static void addElements( Objeto[] objArr) {
  objArr[0] = new Par<Integer,Integer>(0,0);
  objArr[1] = new Par<String,String>("","");      // deve falhar com ArrayStoreException
}

O compilador proíbe a criação de arrays cujo tipo de componente seja um tipo parametrizado concreto, como Par<Inteiro,Inteiro> no nosso exemplo.  Discutimos na entrada anterior porque é razoável que o compilador qualifique um Par<Inteiro,Inteiro>[] como ilegal.  O principal problema é que o compilador e o sistema de tempo de execução devem garantir que um array é um homogêneo sequência de elementos do mesmo tipo.  Uma das verificações de tipo, ou seja, a verificação de armazenamento de arrays realizada pela máquina virtual em tempo de execução, não consegue detetar a inserção ofensiva de um elemento estranho. No exemplo, a segunda inserção no adicionarElementos O método deveria falhar, porque estaman a adder um par de strings a um array de valores integrados, mas não falha como esperado. Os motivos foram discutidos na entrada anterior.

Se não pudermos utilizar matrizes contendo elementos cujo tipo é um tipo parametrizado concreto, ou que utilizaremos como solução alternativa?

Considere três soluções alternativas concebíveis:

  • matriz de tipos brutos
  • matriz de tipo parametrizado wildcard ilimitado
  • coleção em vez de array

Embora as matrizes de tipos parametrizados concretos sejam ilegais, são permitidas coleções de tipos parametrizados concretos.

Um tipo de cura parametrizado é uma instanciação de um tipo genérico em que pelo menos um argumento de tipo é um cura.  Exemplos de tipos curinga parametrizados são Coleção<?> , Lista<? estende o número> , Comparador<? super String> e Par<String,?> . Um tipo parametrizado wildcard denota uma família de tipos que compreende instanciações concretas de um tipo genérico.  O tipo de joker utilizado determina quais os tipos parametrizados concretos que pertencem à família.  Por exemplo, o tipo parametrizado wildcard Coleção<?> denota a família de todas as instanciações do Coleção interface independentemente do argumento de tipo.  O tipo parametrizado curinga Lista<? estende o número> denota a família de todos os tipos de lista em que o tipo de elemento é um subtipo de  Número .  O tipo parametrizado curinga Comparador<? super String> é a família de todas as instanciações do Comparador interface para tipos de argumentos de tipo que são supertipos de Corda.

Um tipo parametrizado wildcard não é um tipo no sentido regular (diferente de uma classe/interface não parametrizada ou de um tipo bruto).

Os tipos curinga parametrizados podem ser utilizados para digitação (como classes e interfaces não parametrizadas):

  • como argumentos e tipos de métodos de retorno
  • como tipo de campo ou variável de referência local
  • como tipo de componente de um argumento de tipo arrayas de outros tipos parametrizados
  • como tipo de alvo em lançamentos

Não só os tipos podem ser genéricos, como os métodos também podem ser genéricos. Os métodos estáticos e não estáticos, assim como os construtores, podem ter parâmetros de tipo. A sintaxe para a declaração dos parâmetros do tipo é semelhante à sintaxe dos tipos genéricos. A secção do parâmetro de tipo é delimitada por parênteses angulares e aparece antes do tipo de retorno do método. A sua sintaxe e significado são idênticos a uma lista de parâmetros de tipo genérico.

Eis o exemplo de um genérico máx. método que calcula o maior valor numa coleção de elementos de tipo desconhecido UM .

Exemplo (de um método genérico):

Coleções de turma {
  estática pública <A estende Comparável<A>> A max(Coleção< UM > xs) {
    Iterador< UM > xi = xs.iterator();
    UM w = xi. seguinte();
    while (xi.hasNext()) {
      UM x = xi. seguinte();
      if (w.compareTo(x) < 0) w = x;
    }
    retornar w;
  }
} 

O máx. método tem um parâmetro de tipo, denominado UM .  É um espaço reservado para o tipo de elemento da coleção em que o método funciona. O parâmetro type tem um limite; deve ser um tipo que seja um subtipo de Comparável<A>

Exemplo (do mesmo tipo genérico, desta vez com limites):

interface pública comparável<T> {
  public int compararTo(T arg);
}
classe pública TreeMap<Key estende Comparável<Key> ,Dados>{
  private static class Entry<K,V> {
     Tecla K;
     Valor V;
     Entrada<K,V> esquerda = nulo;
     Entrada<K,V> direita = nulo;
     Entrada<K,V> pai;
  }
  private transient Entry<Key,Data> root = null;
  ...
  private Entry<Key,Data> getEntry(chave-chave) {
     Entrada<Key,Dados> p = raiz;
     Chave k = chave;
     while (p! = nulo) {
       int cmp = k. compareTo(p.key) ; 
       if (cmp == 0)
         devolver p;
       senão se (cmp < 0)
         p = p. esquerda;
       outro 
         p = p.direito; 
     }
     retornar nulo;
  }
  public boolean containsKey(Chave-chave) {
     return getEntry(chave)! = nulo;
  }
  ...
}

No exemplo acima, o parâmetro de tipo Chave tem o limite Comparável<Key> .  A especificação de um limite tem dois efeitos:

  • Dá acesso aos métodos que o limite especifica .  No exemplo, o limite Comparável<Key> dá acesso ao comparar com método que queremos invocar na implementação do nosso ÁrvoreMapa aula.
  • Apenas os tipos “dentro dos limites” podem ser utilizados para a instanciação do tipo genérico.   No exemplo, um tipo parametrizado como TreeMap<Number,String> seria rejeitado, porque o tipo Número não é um subtipo de Comparável<Número> .  Um tipo parametrizado como TreeMap<String,String> seria aceite, porque o tipo String está dentro dos limites, ou seja, é um subtipo de Comparável<String> .

Todas as classes, interfaces e tipos de enum podem ser utilizados como limite de parâmetros de tipo, incluindo tipos aninhados e internos.  Nem os tipos primitivos nem os tipos array podem ser utilizados como limite de parâmetros de tipo.

Exemplos (de limites de parâmetros de tipo):

classe X0 <T estende interno > {...}      //erro
classe X1 <T estende Objeto[] > {...} //erro
classe X2 <T estende Número > {...}
classe X3 <T estende Corda > {...}
classe X4 <T estende Executável > {...}
classe X5 <T estende Thread.Estado > {...}
classe X6 <T estende Lista > {...}
classe X7 <T estende Lista<String> > {...}
classe X8 <T estende Lista<? estende o número> > {...}
classe X9 <T estende Comparável<? super Número> > {...}
classe X10<T estende Mapa.Entry<?,?> > {...}

O exemplo de código mostra que tipos primitivos como interno e tipos de array como Objeto[] não são permitidos como limite de parâmetro de tipo.

Os tipos de aula, como Número ou Corda e tipos de interface, como Executável , são permitidos como limite de parâmetro de tipo.

Os tipos de enumeração, como Thread.Estado também são permitidos como limite de parâmetro de tipo. Thread.Estado é um exemplo de um tipo aninado utilizado como limite de parâmetro de tipo.  Os tipos internos não estáticos também são permitidos.

Os tipos brutos são permitidos como parâmetros de tipos negligentes; Lista é um exemplo

Os tipos parametrizados são permitidos como limite de parâmetros de tipo, incluindo tipos parametrizados concretos, como Lista<String> , tipos parametrizados de joker limitados, como Lista<? estende o número> e Comparável<? superlongo> e tipos parametrizados de wildcard ilimitados, como Mapa.Entry<?,?> .  Um limite que é um tipo curinga parametrizado permite como argumento de tipo todos os tipos que pertencem à família de tipos que o joker denota.  O limite de tipo parametrizado wildcard apenas fornece acesso restrito aos campos e métodos; as restrições dependem do tipo de joker.

Exemplo (de tipo wildcard parametrizado como limite de parâmetro de tipo):

classe X< T estende List<? estende o número> >{
  public void algumMetodo(T t) {
    t.add(novo Longo(0L));    //erro
    Número n = t.remove(0);
  }
}
classe Teste {
  public static void main(String[] args) {
     X<ArrayList< Longo >> x1 = new X<ArrayList<Long>>(); 
     X<ArrayList< Corda >> x2 = new X<ArrayList<String>>(); //erro
  }
}

Variáveis ​​de referência do tipo T (o parâmetro de tipo) são tratados como variáveis ​​de referência de um tipo cura (o parâmetro de tipo ligado).  No nosso exemplo, a consequência é que o compilador rejeitou a invocação de métodos que recebem um argumento do tipo “unknown” que o parâmetro de tipo representa, como Lista.adicionar , porque o limite é um tipo wildcard parametrizado com um limite superior.
Ao mesmo tempo ou limite Lista<? estende o número> determina os tipos que podem ser utilizados como argumentos de tipo. O compilador aceita todos os argumentos de tipo que pertencem à família de tipos Lista<? estende o número> , isto é, todos os subtipos de Lista com um argumento de tipo que é um subtipo de Número .

Um parâmetro de tipo pode ser utilizado como limite de outro parâmetro de tipo.

Exemplo (de um parâmetro de tipo utilizado como limite de parâmetro de tipo):

classe Triplo <T> {
  privado T fst, snd, trd;
  público < Você estende T , V estende T , C estende T > Triplo (U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

Neste exemplo o parâmetro de tipo T da classe parametrizada é utilizado como limite dos parâmetros do tipo Você ,  V e C de um método de instância parametrizado dessa classe.

Outras oportunidades de utilizar parâmetros de tipo como limites de outros parâmetros de tipo incluem situações em que um tipo aninhado é definido num tipo genérico ou uma classe local é definida num método genérico.  É ainda permitido utilizar um parâmetro de tipo como limite de outro parâmetro de tipo na mesma secção do parâmetro de tipo.

Um limite que é uma classe dá acesso a todos os seus membros públicos, ou seja, campos públicos, métodos e tipos aninháveis. Apenas os construtores não são acessíveis, porque não há garantia de que uma subclasse do limite tenha os mesmos construtores que o limite.

Exemplo (de uma classe utilizada como limite de um parâmetro de tipo):

aula pública SuperClasse {     // membros estáticos
    enum public EnumType {ISSO, ISSO}
    objeto estático público staticField;
    public static void staticMethod() { ... }

    // membros não estáticos
    public class InnerClass {...}
    public object nãoStaticField;
    public void nãoStaticMethod() { ... }

    // construtores
    public SuperClass() { ... }

    // membros privados
    objeto privado campoprivado; 

    ...
}

public final class SomeClass<T extends SuperClasse >{
    objeto T privado;
    public AlgumaClasse(T t) { objeto = t; } 

    public string paraString() {
        retornar
         "tipo aninhado estático    : "+T.EnumType.class+"\n" 
        +"campo estático          : "+T.staticField+"\n"
        +"método estático         : "+T.staticMethod()+"\n"
        +"tipo aninhado não estático: "+T.InnerClass.class+"\n"
        +"campo não estático      : "+object.nonStaticField+"\n"
        +"método não estático     : "+object.nonStaticMethod()+"\n"
        +"construtor           : "+(novo T())+"\n"                    // erro
        +"membro privado        : "+object.privateField+"\n"          // erro 
        ;
    }
}

O limite SuperClasse dá acesso aos seus tipos aninhados, campos e métodos estáticos e campos e métodos não estáticos.  Apenas o construtor não está acessível.  Isto ocorre porque os construtores não são herdados. Cada subclasse define os seus próprios construtores e não tem de suportar os construtores da sua superclasse.  Assim, não há garantia de que uma subclasse de SuperClasse tera o mesmo construtor que a sua superclasse.

O contexto em que Enum<E estende Enum<E>> “aparece é uma declaração do Enum classe no pacote java.lang :

classe abstrata pública Enum<E estende Enum<E>> {
  ...
}

O tipo Enum é a classe base comum de todos os tipos de enumeração.  Em Java, um tipo de enumeração como Cor é traduzido numa classe Cor que se estende Enum<Color> . O objectivo da superclasse Enum é fornecer funcionalidade comum a todos os tipos de enumeração.

Aqui está um esboço da aula Enum :

classe abstrata pública Enum< E estende Enum<E>> implementa Comparable< E >, serializável {
  nome da string final privada;
  public final string nome() { ... }   privado final int ordinal;
  public final int ordinal() { ... }

  Enum protected (string nome, int ordinal) { ... }

  Public String           toString() { ... }
  public final boolean equals(Objecto outro) {...}
  public final int hashCode() { ... }
  protected end object clone() throws CloneNotSupportedException { ... }
  public final int compareTo( E o) {...}

  Classe final pública< E > getDeclaringClass() { ... }
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}

Uma característica surpreendente da afirmação “ Enum<E estende Enum<E>> ” é o facto de uma classe nété-definida Enum e o seu parâmetro de tipo recém-definido E aparecem no limite desse mesmo parâmetro de tipo.  Significa que o Enum type deve ser instanciado para um dos seus subtipos.  Para perceber porque é que isto faz sentido, considere-se que todo o tipo enum é traduzido num subtipo de Enum .

Eis o tipo de enum inventado Cor :

enumeração Cor {VERMELHO, AZUL, VERDE}

O compilador traduz-no na seguinte classe:

aula final pública Cor estende Enum<Color> {
  public static final Color[] valores() { return (Color[])$VALUES. clone(); }
  public static color valueOf (string nome) { ... }   private color (String s, int i) { super (s, i); }

  público estático final Cor VERMELHO;
  public static final Cor AZUL;
  público estático final Cor VERDE;

  private static final cor $VALUES[];

  estático {
    VERMELHO = new Cor("VERMELHO", 0);
    AZUL = new Cor("AZUL", 1);
    VERDE = nova Cor("VERDE", 2);
    $VALUES = (nova Cor[] { VERMELHO, AZUL, VERDE });
  }
}

A herança tem o efeito de que o Cor type herda todos os métodos implementados no Enum<Color> .  Entre eles está o comparar com método.  O Cor. comparar com método deveria provavelmente levar um Cor como argumento.  Para que isso aconteça, a aula Enum é genérico e o Enum.compareTo método leva Enum parâmetro de tipo E como argumento.  Como resultado, digite Cor derivado de Enum<Color> herda um comparar com método que leva um Cor como e argumento, exatamente como deberia.
Se dissecarmos uma afirmação “ Enum<E estende Enum<E>> “podemos permitir que este padrão tenha vários aspetos.

Em primeiro lugar, existe o facto de o parâmetro de tipo ligado ser o próprio tipo: “ Enum <E estende Enum <E>> “. Garante que apenas os subtipos do tipo Enum são permitidos como argumentos de tipo. (Teoricamente, digite Enum poderia ser instanciado em si mesmo, como em Enum<Enum> , mas isso não é certamente intencional e é difícil imaginar uma situação em que tal instância seja útil.)

Em segundo lugar, há o facto de o tipo ligado à parametria ser o tipo parametrizado Enum <E> , que utiliza o parâmetro de tipo E como o argumento de tipo do limite. Esta declaração garante que a relação de herança entre um subtipo e uma instanciação de Enum é da forma X estende Enum<X> “. Um subtipo como “ X estende Enum<Y> “não pode ser declarado por causa do argumento de tipo S não estaria dentro dos limites; apenas subtipos de Enum<X> dentro dos limites.

Terceiro, há o facto de Enum é genérico em primeiro lugar.  Isto significa que alguns dos métodos da classe Enum receber um argumento ou devolver um valor de um tipo desconhecido (ou depender de um tipo desconhecido). Como já sabemos, este tipo desconhecido será mais tarde um subtipo X de Enum<X> .  Portanto, no tipo parametrizado Enum<X> , estes métodos envolvem o subtipo X , e são herdados no subtipo X .  O comparar com método é um exemplo de um método deste tipo; é herdado da superclasse para cada subclasse e tem uma assinatura específica da subclasse em cada caso.

Em suma, uma declaração “ Enum<E estende Enum<E>> ” pode ser decifrado como: Enum é um tipo genérico que só pode ser instanciado para os seus subtipos, e estes subtipos herdarão alguns métodos úteis, alguns dos quais aceitam argumentos específicos do subtipo (ou dependem do subtipo).

Parâmetros de tipo de limite inferior de classes

Os parâmetros de tipo podem ter vários limites, como por classe Box<T extends Number> {...} . Mas um parâmetro de tipo não pode ter um limite inferior, ou seja, uma construção como classe Caixa<T ótimo Número> {...} não é permitido.  Por que não?  A resposta é: é inútil porque não lhe compraria nada, se fosse permitido.  Vamos ver porque é que os parâmetros de tipo de limite inferior das classes são confusos, explorando o que significa um limite superior num parâmetro de tipo.

O limite superior de um parâmetro de tipo tem três efeitos:

    1. Instanciação restrita. O limite superior restringe o conjunto de tipos que podem ser utilizados para a instanciação do tipo genérico.  Se declararmos um classe Box<T extends Number> {...} então o compilador garantiria que apenas subtipos de Número pode ser usado como argumento de tipo.  Ou seja, um Caixa<Número> ou um Caixa<Longa> é permitido, mas um Caixa<Object> ou Caixa<String> seria rejeitado

Exemplo (de instanciação restrita devido a um limite superior num parâmetro de tipo):
classe Box<T extends Número> { 
   valor T privado; 
   caixa pública (T t) { valor = t; } 
   ...
}  classe Teste { 
   public static void main(String[] args) { 
       Box<Long> boxOfLong = new Box<Long>(0L);   // multar
       Box<String> boxOfString = new Box<String>(""); // erro: String não está dentro dos limites
   }
}

  1. Acesso a membros não estáticos. O limite superior dá acesso a todos os métodos e campos não estáticos públicos do limite superior.  Na implementação do nosso classe Box<T extends Number> {...} podemos invocar todos os métodos públicos não estáticos definidos na classe Número , como intValor() por exemplo.  Sem o limite superior, o compilador rejeitaria qualquer invocação deste tipo. Exemplo (de acesso a membros não estáticos devido a um limite superior num parâmetro de tipo):

classe Box<T extends Número> { 
   valor T privado; 
   caixa pública (T t) { valor = t; } 
   public int incremento() { return valor.intValue()+1; } // <= seria um erro sem o limite de número
   ...
} 

    1. Introduza Apagamento. O limite superior mais à esquerda é utilizado para a eliminação de tipos e substitui o parâmetro de tipo no código de bytes.  Em nosso classe Box<T extends Number> {...} todas as ocorrências de T seria substituído pelo limite superior Número .  Por exemplo, se a classe Caixa tem um campo privado do tipo T e um método conjunto vazio (conteúdo T) para definir este campo privado, então o campo seria do tipo Número após o apagamento do tipo e o método seria traduzido para um método conjunto vazio (conteúdo do número) .

Exemplo (de utilizado do limite superior num parâmetro de tipo sem desligamento de tipos – antes tipo apagamento):
classe Caixa< T estende o número >{ 
   privado T valor; 
   caixa pública ( T t) { valor = t; } 
   ...
} 

      Exemplo (de utilizado do limite superior num parâmetro de tipo sem desligamento de tipos –

depois

      tipo apagamento):

 

classe Caixa { 
   privado Número valor; 
   caixa pública ( Número t) { valor = t; } 
   ...
}

    Além disso, o limite superior mais à esquerda aparece noutros locais, como moldes inseridos automaticamente e métodos de ponte.

Se fossem permitidos limites inferiores nos parâmetros de tipo, que efeitos secundários teriam ou deveriam ter? Se uma construção como classe Caixa<T ótimo Número> {...} seriam permitidos, o que significaria?  O que significariam os três efeitos colaterais de um limite de parâmetro de tipo superior – instanciação restrita, acesso a membro não estático, desligamento de tipo – para um limite inferior?

Instanciações restritas. O compilador pode restringir o conjunto de tipos que podem ser utilizados para a instanciação do tipo genérico com um parâmetro de tipo de limite inferior.  Por exemplo, o compilador poderia permitir instanciações como Caixa<Número> e Caixa<Object> de um Caixa<T ótimo Número> e rejeitar instanciações como Caixa<Longa> ou Caixa<Curta> .  Este seria um efeito alinhado com o efeito colateral restritivo descrito para limites superiores de parâmetros de tipo.

Acesso a membros não estáticos. Um limite de parâmetro de tipo inferior não dá acesso a nenhum método específico para além dos herdados da classe Objeto . No exemplo de Caixa<T ótimo Número> os supertipos de Número não têm nada em comum, exceto que são tipos de referência e, portanto, subtipos de Objeto .  O compilador não pode assumir que o campo do tipo T é do tipo Número ou um subtipo do mesmo.  Em vez disso, o campo do tipo T pode ser de qualquer supertipo de Número , como Serializável ou Objeto .  A invocação de um método como intValor() já não é seguro para o tipo e o compilador teria de o rejeitar. Como consequência, o limite inferior do parâmetro de tipo não daria acesso a membros não estáticos para além dos definidos na classe Objeto e, portanto, tem o mesmo efeito que “sem limite”.

Tipo de apagamento .  Seguindo esta linha de lógica, não faz sentido substituir as ocorrências do parâmetro tipo pelo seu limite inferior mais à esquerda.  Declarando um método como o construtor Caixa( T t) como construtor Caixa( Número t) não faz sentido, tendo em conta que T é substituído por um supertipo de Número .  Um Objeto pode ser corretamente passado para o construtor numa instanciação Caixa<Object> e o construtor rejeitá-lo-ia.  Isso seria completamente errado.   Assim sendo, o apagamento de tipo substituiria todas as ocorrências da variável de tipo T por tipo Objeto , e não pelo seu limite inferior.  Mas, mais uma vez, o limite inferior teria o mesmo efeito que “sem limite”.

Quer descobrir o que significaria se ambos se reduzissem _e_ limites superiores foram permitidos?   Pessoalmente, nem quero pensar nisso e prefiro arquivá-lo como “não controlável”, se me permitem.

O resultado final é: tudo isto é “ ótimo “O limite comprar-lhe-ia é a restrição de que apenas os supertipos de Número podem ser utilizados como argumentos de tipo.  E mesmo isso é muitas vezes mal compreendido.  NÃO significaria que classe Box<T super Número> {...} contém apenas instâncias de supertipos de Número .  Muito pelo contrário – como demonstra o exemplo abaixo!

Exemplo (de utilizado do limite superior num parâmetro de tipo sem desligamento de tipos – antes tipo apagamento):

classe Caixa< Supernúmero T >{
   privado T valor;
   caixa pública ( T t) { valor = t; }
   ...
}

Exemplo (de utilizado do limite superior num parâmetro de tipo sem desligamento de tipos – depois tipo apagamento):

classe Caixa {
   privado Objeto valor;
   caixa pública ( Objeto t) { valor = t; }
   ...
}

UM classe Caixa<T ótimo Número> {...} seria traduzido por tipo apagamento para um Caixa contendo um Objeto campo e o seu construtor seriam traduzidos para Caixa (Objecto t) . Isto é fundamentalmente diferente de um classe Caixa<T estende Número> {...} , que seria traduzido para um Caixa contendo um Número campo e o seu construtor seriam traduzidos para Caixa (Número t) . Consequentemente, um Caixa<Número> instanciado de um classe Box<T extends Number> {...} seria diferente de um Caixa<Número> instanciação de um classe Box<T super Número> {...} , o que provavelmente causará confusão.  Por esta razão, os limites inferiores não fazem sentido nos parâmetros de tipo das classes.

 


 

Parâmetros de tipo de limite inferior de métodos

Em conjunto com os métodos e os seus tipos de argumentos, um parâmetro de tipo com um limite inferior pode ser ocasionalmente útil.

Exemplo (de um método que beneficia de um parâmetro de tipo com limite inferior):

classe Par<X,Y> {
   privado X primeiro;
   privado Y segundo;
   ...
   público < Um super X , B super Y > B addToMap(Map<A,B> map) { // erro: o parâmetro de tipo não pode ter limite inferior
      return mapa.put(primeiro, segundo);
   }
}
classe Teste { 
   public static void main(String[] args) { 
     Par<String,Long> par = new Par<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Número número = par.addToMap(mapa);
   }
}

O addToMap() método adiciona o conteúdo do par a um mapa.  Qualquer mapa que possa conter supertipos de X e S faria.  O mapa colocar() O método devolve o valor encontrado no mapa para a chave fornecida, se já existir uma entrada de valor-chave para a chave no mapa.  O valor de retorno do mapa colocar() método deve ser devolvido do addToMap() método.  Nestas circunstâncias, gostaríamos de declarar o método como mostrado acima: O mapa é parametrizado com supertipos dos parâmetros de tipo do par e o addToMap() O tipo de retorno do método é o tipo de valor do mapa.

Como o compilador não permite limites inferiores nos parâmetros de tipo, precisamos de uma solução alternativa.

Uma solução alternativa que nos vem à mente é o uso de uma cura, porque as curas podem ter um limite inferior.  Aqui está uma solução alternativa usando um personagem de curadoria.

Exemplo (de uma solução alternativa para o exemplo anterior utilizando wildcards):

classe Par<X,Y> {
   privado X primeiro;
   privado Y segundo;
   ...
   público Objeto addToMap(Map< ? Super X , ? super Y Mapa >) { 
      return mapa.put(primeiro, segundo);
   }
}
classe Teste { 
   public static void main(String[] args) { 
     Par<String,Long> par = new Par<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Número número = (Número) par.addToMap(mapa);
   }
}

Funciona, exceto que não há forma de declarar o tipo de retorno como desejado.  Seria o supertipo de S que o compilador captura do tipo de mapa, mas não existe sintaxe para o especificar.  Não devemos declarar o tipo de retorno como “ ? super Y “, porque “ ? super Y “é um joker e não um tipo e, poranto, não é permitido como tipo de retorno. Não temos escolha e devemos usá-la Objeto vez, como o tipo de retorno do nosso método.  Este tipo de retorno bastante inespecífico, por sua vez, obriga os chamadores do addToMap() método para converter o valor de retorno para baixo de Objeto ao seu tipo real.  Não era bem isso que tínhamos em mente.

Outra solução alternativa é a utilização de métodos estáticos.  Aqui está uma solução alternativa com um método estático em vez de um método não estático.

Exemplo (de uma solução alternativa para o exemplo anterior utilizando um método estático):

classe Par<X,Y> {
   privado X primeiro;
   privado Y segundo;
   ...
   público estático < UM , B , X estende A ,S estende B > B addToMap(Par<X,par Y>, Mapa< UM , B Mapa >) {
      return mapa.put(par.primeiro,par.segundo);
   }
}
classe Teste { 
   public static void main(String[] args) { 
     Par<String,Long> par = new Par<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Número número = Par . addToMap( par ,mapa);
   }
}

O genérico addToMap() método tem quatro parâmetros de tipo: dois espaços reservados X e S para o tipo do par e dois espaços reservados UM e B para o tipo do mapa. UM e B são supertipos de X e S , porque X e S   são declarados com UM e B como os seus limites superiores. (Note que os parâmetros de tipo do método genérico X e S não tem nada a ver com Par aula X e S parâmetros.  Os nomes X e S são reutilizados para o método genérico para os tornar facilmente reconhecíveis como parâmetros de tipo de par. Utilizando quatro parâmetros de tipo, podemos declarar o tipo de retorno preciso conforme desejado: é o mesmo tipo que o tipo de valor do mapa.

Exemplo (de utilização ilegal de parâmetro de tipo em contexto estático de uma classe genérica):

classe SomeClass <T> {
  // inicializador estático, campo estático, método estático
  estático {
    AlgumClass< T Teste > = new SomeClass< T >(); //erro
  }
  privado estático T informações globais;          //erro
  público  estático T getGlobalInfo() {        //erro
    devolver informações globais;
  }
  // inicializador não estático, campo não estático, método não estático
  { 
    AlgumClass< T Teste > = new SomeClass< T >();
  }
  privado        T informações locais;
  público         T getLocalInfo() { 
    devolver informações locais;
  }
  // tipos aninhados estáticos
  público estático aula A falha estende a exceção {
    final privada T informações;           //erro
    falha pública ( T t) { informação = t; }      //erro
    público T getInfo() { devolver informação; }    //erro
   }
  privado interface Copiável {
    T cópia();                       //erro
  }
  privado enumeração Estado {
    VÁLIDO, INVÁLIDO;
    privado T informações; //erro
    public void setInfo( T t) { informação = t; } //erro
    público T getInfo() { devolver informação; }    //erro
  }
  // tipos aninhados não estáticos
  público aula Acessador {
    público T getInfo() { return localInfo; }
  }
}

O exemplo ilustra que o tipo de parâmetro não pode ser utilizado no contexto estático de uma classe genérica.  Mostra também que as interfaces aninhadas e os tipos enum são considerados membros do tipo estático da classe.  Apenas as classes internas, ou seja, as classes aninadas não estáticas, podem utilizar o parâmetro do tipo da classe genérica envolvente.

O amido do parâmetro de tipo de interface é toda a definição da interface, exceto quando campos ou tipos aninados.  Isto acontece porque os campos e tipos definidos na interface são implicitamente estáticos.

Exemplo (de utilização ilegal de um parâmetro de tipo numa interface genérica):

interface SomeInterface <T> {
  // campo
  AlgumClass< T Valor > = new SomeClass< T >();  //erro
  // tipo aninhado
  aula Acessador {
    público T getInfo() {            //erro
      return valor.getGlobalInfo();
    }
  }
  // métodos
  T getValor();
}

O exemplo mostra que os campos de uma interface são implicitamente estáticos, pelo que o parâmetro de tipo não pode ser utilizado em lado nenhum na declaração de um campo de uma interface genérica.  Da mesma forma, a classe aninhada é considerada uma classe aninhada estática, e não uma classe interna, e por esta razão a utilização do parâmetro type em qualquer parte da classe aninhada é ilegal.

Solução alternativa para classes e interfaces estáticas aninhadas:

No caso das classes e interfaces aninadas estáticas, não se trata de uma grande calamidade.  Como solução alternativa, podemos gerar uma classe estática ou uma interface adequada.

Exemplo (solução alternativa – gerar um tipo estático estático):

classe Wrapper <T> {
  final privada T oObjeto;

  invólucro público ( T t) { oObjecto = t; }
  público T getWrapper() { return oObjecto; }   público imutável <T> makeImutável() {
        retornar novo Imutável <T> (oObjecto);
  }
  mutável público <T> makeMutable() {
        retornar novo Mutável <T> (oObjecto);
  }
  private static <A> A makeClone(A theObject) { ... }

  classe final estática pública imutável <A> { // é uma classe genérica agora
        final privada UM oObjeto;

        público Imutável ( UM arg) {
            theObject = makeClone(arg);
        }
        público UM getWrappedItem() {
            return makeClone(oObjecto);
        }
  }
  classe estática pública mutável <A> {
        ... semelhante ...
  }
}

Não existe tal solução alternativa para tipos de enum aninhados porque não podem ser genéricos.

Solução alternativa para classes estáticas aninhadas:

Se o tipo estático aninado para uma classe, uma solução alternativa seria transformar uma classe estática numa classe interna não estática.

Exemplo (solução alternativa – utilizando uma classe interna):

classe Wrapper <T> {
  final privada T oObjeto;

  invólucro público ( T t) { oObjecto = t; }
  público T getWrapper() { return oObjecto; }   public Mutável makeMutable() {
      retornar esse. novo Mutável(oObjecto);
  }
  public Imutável makeImmutable() {
      retornar esse. novo Imutável(oObjecto);
  }
  private static <A> A makeClone(A theObject) { ... }

  classe final pública Imutável {  // já não é uma classe estática
      final privada T oObjeto;

      público Imutável ( T arg) {
          theObject = makeClone(arg);
      }
      público T getWrappedItem() {
          return makeClone(oObjecto);
      }
  }
  classe pública mutável 
        ... semelhante ...
  }
}

Esta alternativa de solução traz uma certa sobrecarga porque todas as classes internas têm uma referência oculta a uma situação do tipo externo, da qual, neste exemplo, não precisa nem tiram partido; a referência oculta é apenas bagagem.  Muitas vezes, as classes internas são combinadas com interfaces para manter a classe interna como um detalhe de implementação privado da classe envolvente.  Podemos fazer o mesmo aqui.

Exemplo (uma solução alternativa anterior refinada):

classe Wrapper <T> {
  final privada T oObjeto;

  invólucro público ( T t) { oObjecto = t; }
  público T getWrapper() { return oObjecto; }   mutável público <T> makeMutable() {
      return this.new Mutable(theObject);
  }
  público imutável <T> makeImutável() {
      return this.new Imutável(oObjecto);
  }
  private static <A> A makeClone(A theObject) { ... }

  interface pública Imutável<S> {
      S getWrappedItem();
  }
  interface pública Mutável<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  privado classe final ImmutableImplementation implementa Imutável<T> { 

      private final T theObject;

      public ImmutableImplementation(T arg) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(oObjecto);
      }
  }
  privado classe MutableImplementation implementa Mutable<T> { 
        ... semelhante ...
  }
}

Como se pode observar, as interfaces públicas aninhadas precisam de ser genéricas, enquanto as classes privadas internas do utilizador o parâmetro de tipo da classe podem envolver.

 

 

 

 

 

Deixe uma resposta