O objetivo deste artigo é introduzir genéricos da Java, para que todos possam ter uma compreensão final, clara e precisa de todos os aspectos dos genéricos da Java e também estabelecer a base para o próximo artigo "Recuperando a reflexão Java".
Introdução
Os genéricos são um ponto de conhecimento muito importante em Java. Os genéricos são amplamente utilizados nas estruturas de coleção Java. Neste artigo, examinaremos o design dos genéricos da Java do zero, o que envolverá o processamento de curinga e o apagamento do tipo angustiante.
Básico genérico
Classes genéricas
Vamos primeiro definir uma aula de caixa simples:
Caixa de classe pública {objeto private string; public void set (objeto string) {this.object = object; } public string get () {return objeto; }}Esta é a prática mais comum. Uma das desvantagens disso é que apenas os elementos do tipo string podem ser carregados na caixa. No futuro, se precisarmos carregar outros tipos de elementos, como o número inteiro, também devemos reescrever outra caixa. O código não pode ser reutilizado e o uso de genéricos pode resolver bem esse problema.
classe pública Box <t> {// t significa "tipo" privado t t; public void set (t t) {this.t = t; } public t get () {return t; }}Dessa forma, nossa aula de caixa pode ser reutilizada e podemos substituir T por qualquer tipo que desejar:
Caixa <Teger> Integerbox = new Box <Teger> (); caixa <boun> duploBox = new Box <OWLOUE> (); caixa <String> StringBox = new Box <String> ();
Métodos genéricos
Depois de ler a aula genérica, vamos aprender sobre métodos genéricos. Declarar um método genérico é simples, basta adicionar um formulário semelhante a <k, v> ao tipo de retorno:
classe pública util {public static <k, v> boolean compare (par <k, v> p1, par <k, v> p2) {return p1.getKey (). igual (p2.getKey ()) && p1.getValue (). equals (p2.getValue ()); }} Public Class Par <k, v> {private k key; Valor V Privado; Public par (K -Key, V Value) {this.key = key; this.value = value; } public void setKey (K -Key) {this.key = key; } public void SetValue (Value V) {this.value = value; } public k getKey () {return Key; } public v getValue () {return value; }}Podemos chamar métodos genéricos como este:
Par <Inteiro, String> P1 = novo par <> (1, "Apple"); par <Inteiro, String> P2 = novo par <> (2, "Pear"); booleano mesmo = util. <Inteiro, String> Compare (p1, p2);
Ou use a inferência de tipo em java 1.7/1.8 para permitir que o Java deduza automaticamente os parâmetros de tipo correspondente:
Par <Inteiro, String> P1 = novo par <> (1, "Apple"); par <Integer, String> P2 = novo par <> (2, "Pear"); booleano mesmo = util.comPare (P1, P2);
Símbolo de limite
Agora, queremos implementar essa função para encontrar o número de elementos em uma matriz genérica maior que um elemento específico. Podemos implementá -lo assim:
public static <t> int countGreaterThan (t [] anarray, t elem) {int count = 0; para (t e: anarray) if (e> elem) // erro de compilador ++ contagem; contagem de retorno;}Mas isso é obviamente errado, porque, exceto por tipos primitivos, como curta, int, dupla, longa, flutuante, byte, char etc., outras classes podem não necessariamente usar operadores>, portanto o compilador relata um erro. Como resolver esse problema? A resposta é usar o símbolo de limite.
interface pública comparável <t> {public int compareto (t o);}Faça uma declaração semelhante ao seguinte, o que equivale a dizer ao compilador que o parâmetro de tipo t representa classes que implementam a interface comparável, o que é equivalente a dizer ao compilador que todos implementam pelo menos o método compareto.
public static <t estende comparável <t>> int contingGreaterThan (t [] anarray, t elem) {int contou = 0; para (t e: anarray) if (e.compareto (elem)> 0) ++ contagem; contagem de retorno;}Curinga
Antes de entender os curingas, devemos primeiro esclarecer um conceito ou emprestar a aula de caixa que definimos acima, suponha que adicionemos um método como este:
public void boxtest (caixa <número> n) { / * ... * /}Então, que tipo de parâmetros a caixa <número> n permite aceitar? Podemos passar na caixa <TEGER> ou caixa <Pouble>? A resposta é não. Embora o número inteiro e o duplo sejam subclasses de número, não há relação entre a caixa <TEGER> ou a caixa <Pouse> e a caixa <Numm> em genéricos. Isso é muito importante, e usaremos um exemplo completo para aprofundar nosso entendimento.
Primeiro, definimos algumas aulas simples e as usaremos abaixo:
classe fruta {} classe Apple estende a fruta {} classe Orange estende a fruta {}No exemplo a seguir, criamos um leitor de classe genérico e, em seguida, em F1 (), quando tentamos fruit f = fruitReader.readExact (maçãs); O compilador reportará um erro porque não há relação entre a lista <brut> e a lista <pple>.
classe pública GenicReading {STATIC LIST <APLE> APPLES = ARRAYS.ASLIST (new Apple ()); Lista estática <bruit> fruit = Arrays.asList (new Fruit ()); Classe estática leitor <t> {t readExact (list <t> list) {return list.get (0); }} void static f1 () {leitor <bruit> fruitReader = novo leitor <fruit> (); // Erros: List <Frupt> não pode ser aplicado na listagem <pple>. // fruit f = fruitReader.readExact (maçãs); } public static void main (string [] args) {f1 (); }}Mas, de acordo com nossos hábitos de pensamento habituais, deve haver uma conexão entre maçã e frutas, mas o compilador não pode reconhecê -lo. Então, como posso resolver esse problema no código genérico? Podemos resolver esse problema usando curingas:
classe estática covariantreader <t> {t readcovariant (list <? Extends t> list) {return list.get (0); }} void estático f2 () {covariantreader <brut> fruitReader = new covariantreader <bruit> (); Frutas f = fruitader.readcovariant (frutas); Frutas a = fruitReader.readcovariant (maçãs);} public static void main (string [] args) {f2 ();}Isso é bastante semelhante a dizer ao compilador que os parâmetros aceitos pelo método readcovariante do Fruit Reader é desde que a subclasse que satisfaça a fruta (incluindo a própria fruta), de modo que a relação entre a subclasse e a classe pai também esteja associada.
PECS PRINCÍPIOS
Vimos um uso semelhante a <? estende t> acima. Usando -o, podemos obter elementos da lista, então podemos adicionar elementos na lista? Vamos tentar:
classe pública GenicaNeDCoVariance {public static void main (string [] args) {// Wildcards permite covariância: list <? estende fruto> flist = new ArrayList <Apple> (); // Erro de compilação: não é possível adicionar nenhum tipo de objeto: // flist.add (new Apple ()) // flist.add (new Orange ()) // flist.add (new Fruit ()) // flist.add (new Object ()) flist.add (null); // legal, mas desinteressante // sabemos que ele retorna pelo menos frutas: frutas f = flist.get (0); }}A resposta é não, o compilador Java não nos permite fazer isso, por quê? Podemos também considerar esse problema da perspectiva do compilador. Porque lista <? Estende a fruta> Flist pode ter muitos significados:
Lista <? estende fruto> flist = new ArrayList <bruit> (); lista <?? estende a fruta> flist = new ArrayList <Apple> (); lista <?? estende fruto> flist = novo Arraylist <Orange> ();
Portanto, para aulas de coleta que implementam <? Estende T>, eles só podem ser considerados como um produtor que fornece (Get) elemento para o exterior e não pode ser usado como consumidor para obter (adicionar) elementos ao exterior.
O que devemos fazer se quisermos adicionar o elemento? Você pode usar <? super t>:
public class GenicWriting {Lista Static <pple> Apples = new ArrayList <Apple> (); Lista estática <bruit> fruit = novo Arraylist <FRUT> (); static <t> void writeExact (lista <t> list, t item) {list.add (item); } void static f1 () {writeExact (maçãs, new Apple ()); writeExact (frutas, novo Apple ()); } static <t> void writewithwildcard (list <? super t> list, t item) {list.add (item)} estático void f2 () {writewithwildcard (maçãs, new Apple ()); writewithwildcard (frutas, novo Apple ()); } public static void main (string [] args) {f1 (); f2 (); }}Dessa forma, podemos adicionar elementos ao contêiner, mas a desvantagem de usar super é que não podemos obter elementos no contêiner no futuro. O motivo é muito simples. Continuamos a considerar essa questão da perspectiva do compilador. Para lista <? Lista de Super Apple>, pode ter os seguintes significados:
Lista <? Super Apple> List = new ArrayList <pple> (); lista <?? Super Apple> List = new ArrayList <brut> (); lista <?? Super Apple> List = new ArrayList <ject> ();
Quando tentamos obter uma lista de maçã, podemos obter uma fruta, que pode ser outros tipos de frutas, como laranja.
Com base no exemplo acima, podemos resumir uma regra, "Produtor se estende, Super Consumer Super":
Depois de ler alguns código -fonte do Java Collections, podemos descobrir que geralmente usamos os dois juntos, como o seguinte:
public class Coleções {public static <t> void cópia (list <? super t> dest, list <? estende t> src) {for (int i = 0; i <src.size (); i ++) dest.Set (i, src.get (i)); }}Tipo Apagar
Talvez a coisa mais angustiante sobre os genéricos do Java seja o apagamento do tipo, especialmente para programadores com experiência em C ++. O apagamento do tipo significa que os genéricos do Java só podem ser usados para verificação do tipo estático durante a compilação e, em seguida, o código gerado pelo compilador apagará as informações de tipo correspondente. Dessa forma, durante a corrida, a JVM realmente conhece o tipo específico representado pelo genérico. O objetivo disso é porque os genéricos do Java foram introduzidos após 1,5. Para manter a compatibilidade descendente, você só pode digitar apagar para ser compatível com o código não genérico anterior. Para este ponto, se você ler o código -fonte da estrutura de coleção Java, poderá descobrir que algumas classes não suportam genéricas.
Tendo dito muito, o que significa apagamento genérico? Vamos primeiro olhar para o seguinte exemplo simples:
classe pública Node <t> {private t dados; Nó privado <T> Em seguida; nó público (dados t, nó <T> a seguir)} this.data = dados; this.Next = Next; } public t getData () {retornar dados; } // ...}Depois que o compilador concluir a verificação do tipo correspondente, o código acima será realmente convertido para:
classe pública Nó {Dados do objeto privado; nó privado a seguir; public node (dados do objeto, nó próximo) {this.data = data; this.Next = Next; } public Object getData () {return data; } // ...}Isso significa que, independentemente de declararmos o nó <String> ou o nó <TEGER>, a JVM é considerada um nó <Becjat> durante o tempo de execução. Existe alguma maneira de resolver esse problema? Isso exige que redefinimos os limites e modifique o código acima para o seguinte:
classe pública nó <t estende comparável <t>> {private t dados; Nó privado <T> Em seguida; nó público (dados t, nó <T> a seguir) {this.data = data; this.Next = Next; } public t getData () {retornar dados; } // ...}Dessa forma, o compilador substituirá o local onde t aparece com comparável em vez do objeto padrão:
classe pública nó {dados comparáveis privados; nó privado a seguir; public node (dados comparáveis, nó em seguida) {this.data = data; this.Next = Next; } public comparável getData () {retornar dados; } // ...}O conceito acima pode ser mais fácil de entender, mas, de fato, o apagamento genérico traz muito mais problemas. Em seguida, vamos dar uma olhada sistemática em alguns dos problemas trazidos por apagamento do tipo. Alguns problemas podem não ser encontrados nos genéricos do C ++, mas você precisa ter um cuidado extra em Java.
Pergunta 1
Matrizes genéricas não são permitidas em Java. Se o compilador fizer algo como o seguinte, ele relatará um erro:
Lista <Teger> [] Arrayoflists = New List <Integer> [2]; // Erro no tempo de compilação
Por que o compilador não suporta a prática acima? Continue a usar o pensamento reverso, consideramos esse problema da perspectiva do compilador.
Vamos primeiro olhar para o seguinte exemplo:
Objeto [] strings = new string [2]; strings [0] = "oi"; // okStrings [1] = 100; // Uma ArrayStoreException é jogada.
O código acima é fácil de entender. As matrizes de string não podem armazenar elementos inteiros, e esses erros geralmente precisam ser descobertos até que o código seja executado e o compilador não possa reconhecê -los. Em seguida, vamos dar uma olhada no que acontecerá se Java apoiar a criação de matrizes genéricas:
Objeto [] stringLists = new List <String> []; // Erro do compilador, mas finja que é permitido stringLists [0] = new ArrayList <String> (); // ok // Uma ArrayStoreException deve ser lançada, mas o tempo de execução não pode detectá -lo.StringLists [1] = new ArrayList <Teger> ();
Suponha que apoiemos a criação de matrizes genéricas. Como as informações de tipo durante o tempo de execução foram apagadas, a JVM realmente não sabe a diferença entre o novo ArrayList <String> () e o novo ArrayList <Teger> (). Se esses erros ocorrerem em cenários práticos de aplicação, eles serão muito difíceis de detectar.
Se você ainda está cético em relação a isso, pode tentar executar o seguinte código:
classe pública ERESEDTYPEEQUIVALENCE {public static void main (string [] args) {classe C1 = new ArrayList <String> (). getClass (); Classe C2 = novo ArrayList <Teger> (). GetClass (); System.out.println (c1 == c2); // verdadeiro }}Pergunta 2
Continue a reutilizar nossa classe de nó acima. Para um código genérico, o compilador Java realmente nos ajudará secretamente a implementar um método Bridge.
classe pública nó <T> {public t Data; nó público (dados t) {this.data = data; } public void setData (t data) {System.out.println ("node.setData"); this.data = dados; }} classe pública myNode estende o nó <TEGER> {public myNode (dados inteiros) {super (dados); } public void setData (dados inteiros) {System.out.println ("mynode.setData"); super.setData (dados); }}Depois de ler a análise acima, você pode pensar que, após o tipo apagamento, o compilador transformará o nó e o mynode no seguinte:
public class Node {public Object Data; public node (dados do objeto) {this.data = data; } public void setData (dados do objeto) {System.out.println ("node.setData"); this.data = dados; }} classe pública myNode estende o nó {public myNode (dados inteiros) {super (dados); } public void setData (dados inteiros) {System.out.println ("mynode.setData"); super.setData (dados); }}Na verdade, esse não é o caso. Vamos primeiro olhar para o código a seguir. Quando esse código é executado, uma ClassCastException será lançada, solicitando que a string não possa ser convertida em número inteiro:
MyNode mn = novo myNode (5); nó n = mn; // um tipo bruto - compilador lança um aviso desmarcado. // faz com que uma ClassCastException seja jogada.// número inteiro x = mn.data;
Se seguirmos o código que geramos acima, não devemos relatar um erro ao executar na linha 3 (observe que eu comentei a linha 4), porque o método setData (dados da string) não existe no myNode, para que possamos chamar apenas o método setData (dados do objeto) do nó da classe pai. Como dessa maneira, o código da linha 3 acima não deve relatar um erro, porque, é claro, a string pode ser convertida em objeto; portanto, como o ClassCastException é lançado?
De fato, o compilador Java lida automaticamente no código acima:
classe MyNode estende o nó {// Método da ponte gerado pelo compilador public void SetData (dados do objeto) {setData ((integer) dados); } public void setData (dados inteiros) {System.out.println ("mynode.setData"); super.setData (dados); } // ...}É por isso que o erro acima é relatado. Quando os dados do setData ((número inteiro)); A string não pode ser convertida em número inteiro. Portanto, quando o compilador solicitar um aviso desmarcado na linha 2 acima, não podemos optar por ignorá -lo; caso contrário, teremos que esperar até o tempo de execução para encontrar a exceção. Seria ótimo se adicionássemos o nó <Integer> n = Mn no início, para que o compilador possa nos ajudar a encontrar erros com antecedência.
Pergunta 3
Como mencionamos acima, os genéricos da Java só podem fornecer uma verificação estática em grande parte e, em grande parte, as informações de tipo serão apagadas; portanto, o compilador não passará o seguinte método de usar parâmetros de tipo para criar instâncias:
public static <e> void Append (List <E> List) {e elem = new E (); // Lista de erros do tempo de compilação.add (elem);}Mas o que devemos fazer se quisermos criar instâncias usando parâmetros de tipo em determinados cenários? A reflexão pode ser usada para resolver este problema:
public static <e> void Append (List <E> List, classe <e> cls) lança exceção {e elem = cls.newInstance (); // ok list.add (elem);}Podemos chamá -lo assim:
List <string> ls = new ArrayList <> (); append (ls, string.class);
De fato, para o problema acima, você também pode usar padrões de design de fábrica e modelo para resolvê -lo. Os amigos interessados podem querer dar uma olhada na explicação da criação de instância de tipos no Capítulo 15, pensando em Java. Não entraremos nisso aqui.
Pergunta 4
Não podemos usar a instância da palavra -chave diretamente para o código genérico, porque o compilador Java apagará todas as informações relevantes do tipo genérico ao gerar o código, assim como a JVM que verificamos acima não pode reconhecer a diferença entre o ArrayList <Integer> e o ArrayList <String> durante o tempo de execução:
public static <e> void rtti (list <e> list) {if (list a instância do ArrayList <Teger>) {// Erro de tempo de compilação // ...}} => {ArrayList <Teger>, ArrayList <string>, LinkedList <SATIAGEM>, ...}Como acima, podemos usar curingas para redefinir os limites para resolver esse problema:
public static void rtti (list <?> list) {if (list instanceof ArrayList <?>) {// ok; Instância de requer um tipo reifiável // ...}}Resumir
O exposto acima tem tudo a ver com assistir a genéricos da Java neste artigo, espero que seja útil para todos. Amigos interessados podem continuar se referindo a este site:
Explicação detalhada do básico da matriz Java
O básico da programação Java: imitando o compartilhamento de código de login de usuários
O básico da programação da rede Java: comunicação unidirecional
Se houver alguma falha, deixe uma mensagem para apontá -la. Obrigado amigos pelo seu apoio para este site!