introduzione
A partire da JSE 5.0, i generici sono stati aggiunti all'arsenale del linguaggio Java.Cosa sono i generici in Java?
I generici (generalizzazioni) sono mezzi speciali del linguaggio Java per implementare la programmazione generalizzata: un approccio speciale alla descrizione di dati e algoritmi che consente di lavorare con diversi tipi di dati senza modificarne la descrizione. Sul sito web di Oracle, un tutorial separato è dedicato ai generici: “ Lesson: Generics ”.
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello");
String text = list.get(0) + ", world!";
System.out.print(text);
}
}
Questo codice funzionerà correttamente. Ma cosa succederebbe se venissero da noi e dicessero che la frase “Ciao mondo!” picchiato e puoi solo rispondere Ciao? Rimuoviamo la concatenazione con la stringa dal codice ", world!"
. Sembrerebbe che cosa potrebbe esserci di più innocuo? Ma in realtà riceveremo un errore DURANTE LA COMPILAZIONE : error: incompatible types: Object cannot be converted to String
il fatto è che nel nostro caso List memorizza un elenco di oggetti di tipo Object. Poiché String è un discendente di Object (poiché tutte le classi sono implicitamente ereditate da Object in Java), richiede un cast esplicito, cosa che non abbiamo fatto. E durante la concatenazione, verrà chiamato il metodo statico String.valueOf(obj) sull'oggetto, che alla fine chiamerà il metodo toString sull'oggetto. Cioè, la nostra lista contiene Object. Si scopre che dove abbiamo bisogno di un tipo specifico, e non di Object, dovremo eseguire noi stessi il casting del tipo:
import java.util.*;
public class HelloWorld{
public static void main(String []args){
List list = new ArrayList();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println((String)str);
}
}
}
Tuttavia, in questo caso, perché List accetta un elenco di oggetti, memorizza non solo String, ma anche Integer. Ma la cosa peggiore è che in questo caso il compilatore non vedrà nulla di sbagliato. Ed ecco che riceveremo un errore DURANTE L'ESECUZIONE (dicono anche che l'errore è stato ricevuto “a Runtime”). L'errore sarà: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
d'accordo, non il più piacevole. E tutto questo perché il compilatore non è un'intelligenza artificiale e non può indovinare tutto ciò che intende il programmatore. Per dire al compilatore di più su quali tipi utilizzeremo, Java SE 5 ha introdotto generics . Correggiamo la nostra versione dicendo al compilatore cosa vogliamo:
import java.util.*;
public class HelloWorld {
public static void main(String []args){
List<String> list = new ArrayList<>();
list.add("Hello!");
list.add(123);
for (Object str : list) {
System.out.println(str);
}
}
}
Come possiamo vedere, non abbiamo più bisogno del cast di String. Inoltre, ora abbiamo parentesi angolari che incorniciano i farmaci generici. Ora il compilatore non consentirà la compilazione della classe finché non rimuoveremo l'aggiunta di 123 all'elenco, perché questo è intero. Ce lo dirà lui. Molte persone chiamano i generici "zucchero sintattico". E hanno ragione, dal momento che i generici diventeranno effettivamente quelle stesse caste una volta compilati. Diamo un'occhiata al bytecode delle classi compilate: con casting manuale e utilizzando generici:
Tipi grezzi o tipi grezzi
Quando parliamo di generici, abbiamo sempre due categorie: tipi tipizzati (Generic Types) e tipi “raw” (Raw Types). I tipi grezzi sono tipi senza specificare la "qualificazione" tra parentesi angolari:<>
la sintassi del diamante è anche associata al concetto di " Type Inference ", o inferenza del tipo. Dopotutto, il compilatore, vedendo <> a destra, guarda il lato sinistro, dove si trova la dichiarazione del tipo della variabile a cui è assegnato il valore. E da questa parte capisce di che tipo è digitato il valore a destra. Infatti, se un generico viene specificato sul lato sinistro e non specificato sul lato destro, il compilatore sarà in grado di dedurne il tipo:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = new ArrayList();
list.add("Hello World");
String data = list.get(0);
System.out.println(data);
}
}
Tuttavia, si tratterebbe di una miscela del nuovo stile con i generici e del vecchio stile senza di essi. E questo è estremamente indesiderabile. Durante la compilazione del codice sopra riceveremo il messaggio: Note: HelloWorld.java uses unchecked or unsafe operations
. In effetti, non sembra chiaro il motivo per cui sia necessario aggiungere il diamante qui. Ma ecco un esempio:
import java.util.*;
public class HelloWorld{
public static void main(String []args) {
List<String> list = Arrays.asList("Hello", "World");
List<Integer> data = new ArrayList(list);
Integer intNumber = data.get(0);
System.out.println(data);
}
}
Come ricordiamo, ArrayList ha anche un secondo costruttore che accetta una raccolta come input. Ed è qui che sta l'inganno. Senza la sintassi diamante, il compilatore non capisce di essere ingannato, ma con diamante sì. Pertanto, regola n. 1 : usa sempre la sintassi diamante se usiamo tipi tipizzati. Altrimenti rischiamo di perdere il punto in cui utilizziamo il tipo grezzo. Per evitare avvisi nel log che “utilizza operazioni non controllate o non sicure” è possibile specificare un'annotazione speciale sul metodo o sulla classe utilizzata: @SuppressWarnings("unchecked")
Suppress è tradotto come sopprimi, cioè letteralmente sopprimere gli avvisi. Ma pensa al motivo per cui hai deciso di indicarlo? Ricorda la regola numero uno e forse è necessario aggiungere la digitazione.
Metodi generici
I generici ti consentono di digitare metodi. C'è una sezione separata dedicata a questa funzionalità nel tutorial di Oracle: " Metodi generici ". Di questo tutorial, è importante ricordare la sintassi:- include un elenco di parametri digitati all'interno di parentesi angolari;
- l'elenco dei parametri digitati precede il metodo restituito.
import java.util.*;
public class HelloWorld{
public static class Util {
public static <T> T getValue(Object obj, Class<T> clazz) {
return (T) obj;
}
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList("Author", "Book");
for (Object element : list) {
String data = Util.getValue(element, String.class);
System.out.println(data);
System.out.println(Util.<String>getValue(element));
}
}
}
Se guardi la classe Util, vediamo due metodi tipizzati al suo interno. Con l'inferenza del tipo, possiamo fornire la definizione del tipo direttamente al compilatore oppure possiamo specificarla noi stessi. Entrambe le opzioni sono presentate nell'esempio. A proposito, la sintassi è abbastanza logica se ci pensi. Quando si digita un metodo, specifichiamo il generico PRIMA del metodo perché se utilizziamo il generico dopo il metodo, Java non sarà in grado di capire quale tipo utilizzare. Pertanto, prima annunciamo che utilizzeremo il generico T e poi diciamo che restituiremo questo generico. Naturalmente Util.<Integer>getValue(element, String.class)
fallirà con un errore incompatible types: Class<String> cannot be converted to Class<Integer>
. Quando usi metodi digitati, dovresti sempre ricordare la cancellazione del tipo. Diamo un'occhiata ad un esempio:
import java.util.*;
public class HelloWorld {
public static class Util {
public static <T> T getValue(Object obj) {
return (T) obj;
}
}
public static void main(String []args) {
List list = Arrays.asList(2, 3);
for (Object element : list) {
System.out.println(Util.<Integer>getValue(element) + 1);
}
}
}
Funzionerà alla grande. Ma solo finché il compilatore capisce che il metodo chiamato è di tipo Integer. Sostituiamo l'output della console con la seguente riga: System.out.println(Util.getValue(element) + 1);
E otteniamo l'errore: tipi di operandi errati per l'operatore binario '+', primo tipo: Object , secondo tipo: int Cioè, i tipi sono stati cancellati. Il compilatore vede che nessuno ha specificato il tipo, il tipo è specificato come Object e l'esecuzione del codice fallisce con un errore.
Tipi generici
Puoi digitare non solo i metodi, ma anche le classi stesse. Oracle ha una sezione " Tipi generici " dedicata a questo nella sua guida. Diamo un'occhiata ad un esempio:public static class SomeType<T> {
public <E> void test(Collection<E> collection) {
for (E element : collection) {
System.out.println(element);
}
}
public void test(List<Integer> collection) {
for (Integer element : collection) {
System.out.println(element);
}
}
}
Tutto è semplice qui. Se utilizziamo una classe, il generico è elencato dopo il nome della classe. Creiamo ora un'istanza di questa classe nel metodo main:
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Funzionerà bene. Il compilatore vede che esiste un elenco di numeri e una raccolta di tipo String. Ma cosa succede se cancelliamo i generici e facciamo questo:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Otterremo l'errore: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
digitare nuovamente la cancellazione. Poiché la classe non ha più un generico, il compilatore decide che, poiché abbiamo passato una List, un metodo con List<Integer> è più appropriato. E cadiamo con un errore. Pertanto, regola n. 2: se una classe viene digitata, specificare sempre il tipo nel generico .
Restrizioni
Possiamo applicare una restrizione ai tipi specificati nei generici. Ad esempio, vogliamo che il contenitore accetti solo Number come input. Questa funzionalità è descritta nel tutorial Oracle nella sezione Parametri del tipo delimitato . Diamo un'occhiata ad un esempio:import java.util.*;
public class HelloWorld{
public static class NumberContainer<T extends Number> {
private T number;
public NumberContainer(T number) { this.number = number; }
public void print() {
System.out.println(number);
}
}
public static void main(String []args) {
NumberContainer number1 = new NumberContainer(2L);
NumberContainer number2 = new NumberContainer(1);
NumberContainer number3 = new NumberContainer("f");
}
}
Come puoi vedere, abbiamo limitato il tipo generico alla classe/interfaccia Number e ai suoi discendenti. È interessante notare che puoi specificare non solo una classe, ma anche le interfacce. Ad esempio: public static class NumberContainer<T extends Number & Comparable> {
I generici hanno anche il concetto di Wildcard https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Essi, a loro volta, si dividono in tre tipologie:
- Caratteri jolly con limite superiore - < ? estende Numero >
- Caratteri jolly illimitati - < ? >
- Caratteri jolly con limite inferiore - < ? super intero >
public static class TestClass {
public static void print(List<? extends String> list) {
list.add("Hello World!");
System.out.println(list.get(0));
}
}
public static void main(String []args) {
List<String> list = new ArrayList<>();
TestClass.print(list);
}
Ma se sostituisci extends con super, tutto andrà bene. Poiché riempiamo l'elenco con un valore prima di emetterlo, per noi è un consumatore, cioè un consumatore. Pertanto usiamo super.
Eredità
C'è un'altra caratteristica insolita dei farmaci generici: la loro ereditarietà. L'ereditarietà dei generici è descritta nel tutorial di Oracle nella sezione " Generici, ereditarietà e sottotipi ". La cosa principale è ricordare e realizzare quanto segue. Non possiamo farlo:List<CharSequence> list1 = new ArrayList<String>();
Poiché l'ereditarietà funziona diversamente con i generici:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Anche qui tutto è semplice. List<String> non è un discendente di List<Object>, sebbene String sia un discendente di Object.
Finale
Quindi abbiamo rinfrescato la nostra memoria sui farmaci generici. Se raramente vengono utilizzati in tutta la loro potenza, alcuni dettagli cadono dalla memoria. Spero che questa breve recensione vi aiuti a rinfrescarvi la memoria. E per risultati maggiori, ti consiglio vivamente di leggere i seguenti materiali:- Yuri Tkach: Tipi grezzi - Generici n. 1 - Java avanzato
- Ereditarietà ed estensori generici - Generics #2 - Java avanzato
- Estensione di tipo ricorsivo - Generici n. 3 - Java avanzato
- Alexander Matorin - Generici non evidenti
- Introduzione a Java. Generici. Caratteri jolly | Technostream
- O'Reilly: generici e raccolte Java
GO TO FULL VERSION