CIAO! Continuiamo la nostra serie di lezioni sui generici. In precedenza abbiamo avuto un'idea generale di cosa sono e perché sono necessari. Oggi impareremo di più su alcune delle caratteristiche dei generici e su come lavorare con loro. Andiamo! Nell'ultima lezione abbiamo parlato della differenza tra tipi generici e tipi grezzi . Un tipo non elaborato è una classe generica il cui tipo è stato rimosso.
La documentazione dice: "T - il tipo di classe modellato da questo oggetto Class". Traducendo questo dal linguaggio della documentazione al discorso semplice, comprendiamo che la classe dell'oggetto
List list = new ArrayList();
Ecco un esempio. Qui non indichiamo che tipo di oggetti verranno inseriti nel nostro List
. Se proviamo a creare un tale List
e ad aggiungervi alcuni oggetti, vedremo un avviso in IDEA:
"Unchecked call to add(E) as a member of raw type of java.util.List".
Ma abbiamo anche parlato del fatto che i generici sono apparsi solo in Java 5. Quando questa versione è stata rilasciata, i programmatori avevano già scritto un mucchio di codice utilizzando tipi non elaborati, quindi questa caratteristica del linguaggio non poteva smettere di funzionare e la capacità di creare tipi non elaborati in Java è stato preservato. Tuttavia, il problema si è rivelato più diffuso. Come sapete, il codice Java viene convertito in uno speciale formato compilato chiamato bytecode, che viene quindi eseguito dalla macchina virtuale Java. Ma se inseriamo informazioni sui parametri di tipo nel bytecode durante il processo di conversione, tutto il codice scritto in precedenza si romperebbe, perché non c'erano parametri di tipo prima di Java 5! Quando lavori con i generici, c'è un concetto molto importante che devi ricordare. Si chiama cancellazione del tipo. Significa che una classe non contiene informazioni su un parametro di tipo. Queste informazioni sono disponibili solo durante la compilazione e vengono cancellate (diventano inaccessibili) prima del runtime. Se provi a inserire il tipo errato di oggetto nel tuo List<String>
, il compilatore genererà un errore. Questo è esattamente ciò che i creatori del linguaggio vogliono ottenere quando hanno creato i generici: controlli in fase di compilazione. Ma quando tutto il tuo codice Java si trasforma in bytecode, non contiene più informazioni sui parametri di tipo. In bytecode, la tua List<Cat>
lista di gatti non è diversa dalle List<String>
stringhe. In bytecode, nulla dice che cats
è un elenco di Cat
oggetti. Tali informazioni vengono cancellate durante la compilazione: solo il fatto che tu abbia un List<Object> cats
elenco finirà nel bytecode del programma. Vediamo come funziona:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
Abbiamo creato la nostra TestClass
classe generica. È abbastanza semplice: in realtà è una piccola "collezione" di 2 oggetti, che vengono memorizzati immediatamente quando l'oggetto viene creato. Ha 2 T
campi. Quando il createAndAdd2Values()
metodo viene eseguito, i due oggetti passati ( Object a
e Object b
devono essere convertiti al T
tipo e poi aggiunti all'oggetto TestClass
. Nel main()
metodo, creiamo a TestClass<Integer>
, cioè l' Integer
argomento di tipo sostituisce il Integer
parametro di tipo. Stiamo anche passando a Double
e a String
a il createAndAdd2Values()
metodo.Pensi che il nostro programma funzionerà?Dopotutto, abbiamo specificato Integer
come argomento di tipo, ma String
sicuramente non è possibile eseguire il cast su an Integer
!Eseguiamo il metodomain()
metodo e controllo. Uscita console:
22.111
Test String
È stato inaspettato! Perché è successo? È il risultato della cancellazione del tipo. Le informazioni sull'argomento Integer
di tipo utilizzato per istanziare il nostro TestClass<Integer> test
oggetto sono state cancellate quando il codice è stato compilato. Il campo diventa TestClass<Object> test
. I nostri argomenti Double
e String
sono stati facilmente convertiti in Object
oggetti (non vengono convertiti in Integer
oggetti come ci aspettavamo!) e tranquillamente aggiunti a TestClass
. Ecco un altro esempio semplice ma molto rivelatore di cancellazione del tipo:
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
Uscita console:
true
true
Sembra che abbiamo creato raccolte con tre diversi tipi di argomenti: String
, Integer
e la nostra stessa Cat
classe. Ma durante la conversione in bytecode, tutte e tre le liste diventano List<Object>
, quindi quando il programma viene eseguito ci dice che stiamo usando la stessa classe in tutti e tre i casi.
Cancellazione del tipo quando si lavora con matrici e generici
C'è un punto molto importante che deve essere compreso chiaramente quando si lavora con array e classi generiche (comeList
). Dovresti anche tenerne conto quando scegli le strutture dati per il tuo programma. I generici sono soggetti alla cancellazione del tipo. Le informazioni sui parametri di tipo non sono disponibili in fase di esecuzione. Al contrario, gli array conoscono e possono utilizzare le informazioni sul loro tipo di dati quando il programma è in esecuzione. Il tentativo di inserire un tipo non valido in un array causerà la generazione di un'eccezione:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Uscita console:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Poiché esiste una differenza così grande tra array e generici, potrebbero avere problemi di compatibilità. Soprattutto non è possibile creare un array di oggetti generici o anche solo un array parametrizzato. Ti sembra un po' confuso? Diamo un'occhiata. Ad esempio, non puoi fare nulla di tutto ciò in Java:
new List<T>[]
new List<String>[]
new T[]
Se proviamo a creare un array di List<String>
oggetti, otteniamo un errore di compilazione che si lamenta della creazione di un array generico:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
Ma perché si fa? Perché la creazione di tali array non è consentita? Questo è tutto per fornire la sicurezza del tipo. Se il compilatore ci permettesse di creare tali array di oggetti generici, potremmo creare un sacco di problemi per noi stessi. Ecco un semplice esempio tratto dal libro di Joshua Bloch "Effective Java":
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
Immaginiamo che la creazione di un array come List<String>[] stringLists
sia consentita e non genererà un errore di compilazione. Se questo fosse vero, ecco alcune cose che potremmo fare: Nella riga 1, creiamo un array di liste: List<String>[] stringLists
. Il nostro array ne contiene uno List<String>
. Nella riga 2, creiamo un elenco di numeri: List<Integer>
. Nella riga 3, assegniamo our List<String>[]
a una Object[] objects
variabile. Il linguaggio Java lo consente: un array di X
oggetti può memorizzare X
oggetti e oggetti di tutte le sottoclassi X
. Di conseguenza, puoi inserire qualsiasi cosa in un Object
array. Nella riga 4 sostituiamo l'unico elemento dell'array objects()
(a List<String>
) con a List<Integer>
. Pertanto, inseriamo a List<Integer>
in un array destinato esclusivamente alla memorizzazioneList<String>
oggetti! Incontreremo un errore solo quando eseguiamo la riga 5. A ClassCastException
verrà lanciato in fase di esecuzione. Di conseguenza, a Java è stato aggiunto un divieto sulla creazione di tali array. Questo ci permette di evitare tali situazioni.
Come posso aggirare la cancellazione del tipo?
Bene, abbiamo imparato a conoscere la cancellazione dei caratteri. Proviamo a ingannare il sistema! :) Compito: abbiamo unaTestClass<T>
classe generica. Vogliamo scrivere un createNewT()
metodo per questa classe che creerà e restituirà un nuovo T
oggetto. Ma questo è impossibile, giusto? Tutte le informazioni sul T
tipo vengono cancellate durante la compilazione e in fase di esecuzione non è possibile determinare quale tipo di oggetto è necessario creare. In realtà c'è un modo complicato per farlo. Probabilmente ti ricordi che Java ha una Class
classe. Possiamo usarlo per determinare la classe di uno qualsiasi dei nostri oggetti:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
Uscita console:
class java.lang.Integer
class java.lang.String
Ma ecco un aspetto di cui non abbiamo parlato. Nella documentazione di Oracle, vedrai che la classe Class è generica!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
non è solo Class
, ma piuttosto Class<Integer>
. Il tipo dell'oggetto String.class
non è solo Class
, ma piuttosto Class<String>
, ecc. Se non è ancora chiaro, prova ad aggiungere un parametro di tipo all'esempio precedente:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
E ora, usando questa conoscenza, possiamo aggirare la cancellazione dei caratteri e portare a termine il nostro compito! Proviamo a ottenere informazioni su un parametro di tipo. Il nostro argomento di tipo sarà MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
Ed ecco come usiamo la nostra soluzione in pratica:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
Uscita console:
A MySecretClass object was created successfully!
Abbiamo appena passato l'argomento di classe richiesto al costruttore della nostra classe generica:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Questo ci ha permesso di salvare le informazioni sull'argomento di tipo, impedendo che venisse completamente cancellato. Di conseguenza, siamo stati in grado di creare un fileT
oggetto! :) Con questo, la lezione di oggi giunge al termine. Devi sempre ricordare la cancellazione del tipo quando lavori con i generici. Questa soluzione alternativa non sembra molto conveniente, ma dovresti capire che i generici non facevano parte del linguaggio Java quando è stato creato. Questa funzionalità, che ci aiuta a creare raccolte parametrizzate e rilevare errori durante la compilazione, è stata aggiunta in seguito. In alcuni altri linguaggi che includevano generici dalla prima versione, non è prevista la cancellazione del tipo (ad esempio, in C#). A proposito, non abbiamo ancora finito di studiare i generici! Nella prossima lezione imparerai alcune altre caratteristiche dei generici. Per ora, sarebbe bene risolvere un paio di compiti! :)
GO TO FULL VERSION