JavaRush /Java Blog /Random-IT /Generici per gatti
Viacheslav
Livello 3

Generici per gatti

Pubblicato nel gruppo Random-IT
Generici per gatti - 1

introduzione

Oggi è un grande giorno per ricordare ciò che sappiamo di Java. Secondo il documento più importante, cioè Java Language Specifiaction (JLS - Java Language Specifiaction), Java è un linguaggio fortemente tipizzato, come descritto nel capitolo " Capitolo 4. Tipi, valori e variabili ". Cosa significa questo? Diciamo che abbiamo un metodo principale:
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
La tipizzazione forte garantisce che quando questo codice viene compilato, il compilatore controllerà che se abbiamo specificato il tipo della variabile di testo come String, non stiamo tentando di usarlo da nessuna parte come variabile di un altro tipo (ad esempio, come Integer) . Ad esempio, se proviamo a salvare un valore invece del testo 2L(cioè long invece di String), otterremo un errore in fase di compilazione:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Quelli. La tipizzazione forte consente di garantire che le operazioni sugli oggetti vengano eseguite solo quando tali operazioni sono legali per tali oggetti. Questo è anche chiamato indipendenza dai tipi. Come affermato in JLS, esistono due categorie di tipi in Java: tipi primitivi e tipi di riferimento. Puoi ricordare i tipi primitivi dall'articolo di revisione: " Tipi primitivi in ​​Java: non sono così primitivi ". I tipi di riferimento possono essere rappresentati da una classe, un'interfaccia o un array. E oggi saremo interessati ai tipi di riferimento. E cominciamo con gli array:
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Questo codice viene eseguito senza errori. Come sappiamo (ad esempio, da " Oracle Java Tutorial: Arrays "), un array è un contenitore che memorizza dati di un solo tipo. In questo caso, solo linee. Proviamo ad aggiungere long all'array invece di String:
text[1] = 4L;
Eseguiamo questo codice (ad esempio, in Repl.it Online Java Compiler ) e otteniamo un errore:
error: incompatible types: long cannot be converted to String
L'array e l'indipendenza dai tipi del linguaggio non ci permettevano di salvare in un array ciò che non rientrava nel tipo. Questa è una manifestazione dell'indipendenza dai tipi. Ci è stato detto: "Correggi l'errore, ma fino ad allora non compilerò il codice". E la cosa più importante è che ciò avvenga al momento della compilazione e non all'avvio del programma. Cioè, vediamo gli errori immediatamente e non “un giorno”. E poiché ci siamo ricordati degli array, ricordiamoci anche del Java Collections Framework . Lì avevamo strutture diverse. Ad esempio, elenchi. Riscriviamo l'esempio:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Durante la compilazione, riceveremo testun errore sulla riga di inizializzazione della variabile:
incompatible types: Object cannot be converted to String
Nel nostro caso, List può memorizzare qualsiasi oggetto (cioè un oggetto di tipo Object). Pertanto, il compilatore afferma che non può assumersi un tale onere di responsabilità. Dobbiamo quindi specificare esplicitamente il tipo che otterremo dalla lista:
String test = (String) text.get(0);
Questa indicazione è chiamata conversione di tipo o casting di tipo. E tutto funzionerà bene ora finché non proveremo a ottenere l'elemento all'indice 1, perché è di tipo Long. E otterremo un errore discreto, ma già mentre il programma è in esecuzione (in Runtime):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Come possiamo vedere, ci sono diversi importanti svantaggi qui. Innanzitutto siamo costretti a “castare” il valore ottenuto dalla lista alla classe String. D'accordo, questo è brutto. In secondo luogo, in caso di errore, lo vedremo solo quando il programma viene eseguito. Se il nostro codice fosse più complesso, potremmo non rilevare immediatamente un errore del genere. E gli sviluppatori hanno iniziato a pensare a come rendere il lavoro in tali situazioni più semplice e il codice più chiaro. E sono nati: Generics.
Generici per gatti - 2

Generici

Quindi, generici. Che cos'è? Un generico è un modo speciale di descrivere i tipi utilizzati, che il compilatore di codice può utilizzare nel suo lavoro per garantire la sicurezza dei tipi. Sembra qualcosa del genere:
Generici per gatti - 3
Ecco un breve esempio e spiegazione:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
In questo esempio, diciamo che non abbiamo solo List, ma List, che funziona SOLO con oggetti di tipo String. E nessun altro. Ciò che è appena indicato tra parentesi, possiamo memorizzarlo. Tali "parentesi" sono chiamate "parentesi angolari", ad es. parentesi angolari. Il compilatore controllerà per noi se abbiamo commesso errori lavorando con un elenco di stringhe (l'elenco si chiama testo). Il compilatore vedrà che stiamo tentando sfacciatamente di inserire Long nella lista String. E al momento della compilazione darà un errore:
error: no suitable method found for add(long)
Potresti aver ricordato che String è un discendente di CharSequence. E decidi di fare qualcosa del tipo:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Ma questo non è possibile e riceveremo l'errore: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> Sembra strano, perché. la riga CharSequence sec = "test";non contiene errori. Scopriamolo. Dicono di questo comportamento: “I generici sono invarianti”. Cos'è un "invariante"? Mi piace come si dice a riguardo su Wikipedia nell'articolo “ Covarianza e controvarianza ”:
Generici per gatti - 4
Pertanto, l'invarianza è l'assenza di ereditarietà tra tipi derivati. Se Cat è un sottotipo di Animali, allora Set<Cats> non è un sottotipo di Set<Animals> e Set<Animals> non è un sottotipo di Set<Cats>. A proposito, vale la pena dire che a partire da Java SE 7 è apparso il cosiddetto “ Diamond Operator ”. Perché le due parentesi angolari <> sono come un diamante. Questo ci consente di utilizzare generici come questo:
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Sulla base di questo codice, il compilatore capisce che se abbiamo indicato sul lato sinistro che conterrà Listoggetti di tipo String, sul lato destro intendiamo che vogliamo salvare linesun nuovo ArrayList in una variabile, che memorizzerà anche un oggetto del tipo specificato sul lato sinistro. Quindi il compilatore del lato sinistro comprende o deduce il tipo per il lato destro. Questo è il motivo per cui questo comportamento è chiamato inferenza del tipo o "Type Inference" in inglese. Un'altra cosa interessante da notare sono i tipi RAW o “tipi grezzi”. Perché I generici non sono sempre esistiti e Java cerca di mantenere la compatibilità con le versioni precedenti quando possibile, quindi i generici sono costretti a lavorare in qualche modo con il codice in cui non è specificato alcun generico. Vediamo un esempio:
List<CharSequence> lines = new ArrayList<String>();
Come ricordiamo, tale riga non verrà compilata a causa dell'invarianza dei farmaci generici.
List<Object> lines = new ArrayList<String>();
E neanche questo verrà compilato, per lo stesso motivo.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Tali righe verranno compilate e funzioneranno. È in essi che vengono utilizzati i tipi grezzi, ad es. tipi non specificati. Ancora una volta, vale la pena sottolineare che i Raw Type NON DOVREBBERO essere utilizzati nel codice moderno.
Generici per gatti - 5

Classi digitate

Quindi, lezioni digitate. Vediamo come possiamo scrivere la nostra classe tipizzata. Ad esempio, abbiamo una gerarchia di classi:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Vogliamo creare una classe che implementi un contenitore per animali. Sarebbe possibile scrivere una classe che contenga qualsiasi file Animal. Questo è semplice, comprensibile, MA... mischiare cani e gatti è brutto, non sono amici tra loro. Inoltre, se qualcuno riceve un contenitore del genere, potrebbe erroneamente gettare i gatti dal contenitore in un branco di cani... e questo non porterà a nulla di buono. E qui i generici ci aiuteranno. Ad esempio, scriviamo l'implementazione in questo modo:
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
La nostra classe funzionerà con oggetti del tipo specificato da un generico denominato T. Questo è una sorta di alias. Perché Il generico è specificato nel nome della classe, quindi lo riceveremo al momento della dichiarazione della classe:
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Come possiamo vedere, abbiamo indicato di avere Box, che funziona solo con Cat. Il compilatore si è reso conto che catBoxinvece di un generico, Tè necessario sostituire il tipo Catovunque sia specificato il nome del generico T:
Generici per gatti - 6
Quelli. è grazie al Box<Cat>compilatore che capisce cosa slotsdovrebbe essere effettivamente List<Cat>. Perché Box<Dog>dentro ci sarà slots, contenente List<Dog>. Possono essere presenti diversi generici in una dichiarazione di tipo, ad esempio:
public static class Box<T, V> {
Il nome generico può essere qualsiasi cosa, anche se si consiglia di aderire ad alcune regole non dette - "Convenzioni di denominazione dei parametri di tipo": tipo di elemento - E, tipo di chiave - K, tipo di numero - N, T - per tipo, V - per tipo di valore . A proposito, ricorda che abbiamo detto che i generici sono invarianti, cioè non preservare la gerarchia di ereditarietà. In effetti, possiamo influenzarlo. Cioè, abbiamo l'opportunità di creare farmaci generici COvariant, ad es. mantenere le eredità nello stesso ordine. Questo comportamento è chiamato "Bounded Type", cioè tipi limitati. Ad esempio, la nostra classe Boxpotrebbe contenere tutti gli animali, quindi dichiareremo un generico come questo:
public static class Box<T extends Animal> {
Cioè, impostiamo il limite superiore alla classe Animal. Possiamo anche specificare diversi tipi dopo la parola chiave extends. Ciò significa che il tipo con cui lavoreremo deve essere un discendente di qualche classe e allo stesso tempo implementare qualche interfaccia. Per esempio:
public static class Box<T extends Animal & Comparable> {
In questo caso, se proviamo a inserire Boxqualcosa in tale che non sia un erede Animale non implementi Comparable, durante la compilazione riceveremo un errore:
error: type argument Cat is not within bounds of type-variable T
Generici per gatti - 7

Metodo di digitazione

I generici vengono utilizzati non solo nei tipi, ma anche nei metodi individuali. L'applicazione dei metodi può essere vista nel tutorial ufficiale: " Metodi generici ".

Sfondo:

Generici per gatti - 8
Diamo un'occhiata a questa immagine. Come puoi vedere, il compilatore esamina la firma del metodo e vede che stiamo prendendo come input una classe non definita. Non determina mediante la firma che stiamo restituendo un qualche tipo di oggetto, ad es. Oggetto. Pertanto, se vogliamo creare, ad esempio, un ArrayList, allora dobbiamo fare questo:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Devi scrivere esplicitamente che l'output sarà un ArrayList, il che è brutto e aggiunge la possibilità di commettere un errore. Ad esempio, possiamo scrivere queste sciocchezze e compileranno:
ArrayList object = (ArrayList) createObject(LinkedList.class);
Possiamo aiutare il compilatore? Sì, i generici ci permettono di farlo. Diamo un'occhiata allo stesso esempio:
Generici per gatti - 9
Quindi, possiamo creare un oggetto semplicemente in questo modo:
ArrayList<String> object = createObject(ArrayList.class);
Generici per gatti - 10

Carta jolly

Secondo il Tutorial on Generics di Oracle, in particolare la sezione " Wildcards ", possiamo descrivere un "tipo sconosciuto" con un punto interrogativo, chiamato punto interrogativo. Il carattere jolly è uno strumento utile per mitigare alcune delle limitazioni dei farmaci generici. Ad esempio, come abbiamo discusso in precedenza, i farmaci generici sono invarianti. Ciò significa che sebbene tutte le classi siano discendenti (sottotipi) del tipo Object, List<любой тип>non è un sottotipo List<Object>. MA List<любой тип>è un sottotipo List<?>. Quindi possiamo scrivere il seguente codice:
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Come i farmaci generici regolari (ovvero senza l'uso di caratteri jolly), i farmaci generici con caratteri jolly possono essere limitati. Il carattere jolly Limitato superiore sembra familiare:
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Ma puoi anche limitarlo con il carattere jolly Lower bound:
public static void printCatList(List<? super Cat> list) {
Pertanto, il metodo inizierà ad accettare tutti i gatti, nonché quelli più in alto nella gerarchia (fino a Oggetto).
Generici per gatti - 11

Digitare Cancellazione

Parlando di farmaci generici, vale la pena conoscere la “Cancellazione dei caratteri”. In effetti, la cancellazione del tipo riguarda il fatto che i generici sono informazioni per il compilatore. Durante l'esecuzione del programma non ci sono più informazioni sui farmaci generici, questo si chiama "cancellazione". Questa cancellazione ha l'effetto che il tipo generico viene sostituito dal tipo specifico. Se il generico non aveva un confine, verrà sostituito il tipo Oggetto. Se è stato specificato il bordo (ad esempio <T extends Comparable>), verrà sostituito. Ecco un esempio tratto dal tutorial di Oracle: " Erasure of Generic Types ":
Generici per gatti - 12
Come detto sopra, in questo esempio il generico Tviene cancellato fino al bordo, cioè Prima Comparable.
Generici per gatti - 13

Conclusione

I generici sono un argomento molto interessante. Spero che questo argomento sia di tuo interesse. Per riassumere, possiamo dire che i generici sono un ottimo strumento che gli sviluppatori hanno ricevuto per fornire al compilatore informazioni aggiuntive per garantire la sicurezza dei tipi da un lato e la flessibilità dall'altro. E se sei interessato, ti suggerisco di dare un'occhiata alle risorse che mi sono piaciute: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION