CodeGym /Java Blog /Random-IT /Java Generics: come utilizzare le parentesi angolari nell...
John Squirrels
Livello 41
San Francisco

Java Generics: come utilizzare le parentesi angolari nella pratica

Pubblicato nel gruppo Random-IT

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 sono il meccanismo speciale di Java per l'implementazione della programmazione generica, un modo per descrivere dati e algoritmi che consente di lavorare con diversi tipi di dati senza modificare la descrizione degli algoritmi. Il sito Web Oracle ha un tutorial separato dedicato ai generici: " Lezione ". Per capire i generici, devi prima capire perché sono necessari e cosa danno. La sezione " Why Use Generics? " del tutorial afferma che un paio di scopi sono un controllo dei tipi più forte in fase di compilazione e l'eliminazione della necessità di cast espliciti. Generici in Java: come utilizzare le parentesi angolari nella pratica - 1Prepariamoci per alcuni test nel nostro amato compilatore java online Tutorialspoint . Supponiamo di avere il seguente codice:

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à perfettamente. Ma cosa succede se il capo viene da noi e dice che "Ciao, mondo!" è una frase abusata e che devi restituire solo "Ciao"? Rimuoveremo il codice che concatena ", world!" Questo sembra abbastanza innocuo, giusto? Ma in realtà otteniamo un errore IN TEMPO DI COMPILAZIONE:

error: incompatible types: Object cannot be converted to String
Il problema è che nella nostra lista memorizza gli oggetti. String è un discendente di Object (poiché tutte le classi Java ereditano implicitamente Object ), il che significa che abbiamo bisogno di un cast esplicito, ma non ne abbiamo aggiunto uno. Durante l'operazione di concatenazione, il metodo statico String.valueOf(obj) verrà chiamato utilizzando l'oggetto. Alla fine, chiamerà il metodo toString della classe Object . In altre parole, la nostra List contiene un Object . Ciò significa che ovunque abbiamo bisogno di un tipo specifico (non Object ), dovremo eseguire noi stessi la conversione 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, poiché List accetta oggetti, può memorizzare non solo String s, ma anche Integer s. Ma la cosa peggiore è che il compilatore non vede nulla di sbagliato qui. E ora avremo un errore IN TEMPO DI ESECUZIONE (noto come "errore di runtime"). L'errore sarà:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Devi essere d'accordo che questo non è molto buono. E tutto questo perché il compilatore non è un'intelligenza artificiale in grado di indovinare sempre correttamente l'intento del programmatore. Java SE 5 ha introdotto i generici per consentirci di comunicare al compilatore le nostre intenzioni, ovvero quali tipi utilizzeremo. Correggiamo il nostro codice 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 puoi vedere, non abbiamo più bisogno di un cast a String . Inoltre, abbiamo parentesi angolari che racchiudono l'argomento type. Ora il compilatore non ci consentirà di compilare la classe fino a quando non rimuoviamo la riga che aggiunge 123 all'elenco, poiché si tratta di un Integer . E ce lo dirà. Molte persone chiamano i generici "zucchero sintattico". E hanno ragione, poiché dopo che i generici sono stati compilati, diventano davvero le stesse conversioni di tipo. Diamo un'occhiata al bytecode delle classi compilate: una che utilizza un cast esplicito e una che utilizza i generici: Generici in Java: come utilizzare le parentesi angolari nella pratica - 2dopo la compilazione, tutti i generici vengono cancellati. Questo si chiama " cancellazione del tipo". La cancellazione dei tipi e i generici sono progettati per essere retrocompatibili con le versioni precedenti di JDK, consentendo allo stesso tempo al compilatore di aiutare con le definizioni dei tipi nelle nuove versioni di Java.

Tipi grezzi

Parlando di generici, abbiamo sempre due categorie: tipi parametrizzati e tipi grezzi. I tipi grezzi sono tipi che omettono il "chiarimento del tipo" tra parentesi angolari: Generici in Java: come utilizzare in pratica le parentesi angolari - 3i tipi parametrizzati, invece, includono un "chiarimento": Generici in Java: come utilizzare in pratica le parentesi angolari - 4come puoi vedere, abbiamo utilizzato un costrutto insolito, contrassegnato da una freccia nello screenshot. Questa è una sintassi speciale che è stata aggiunta a Java SE 7. Si chiama " diamante ". Perché? Le parentesi angolari formano un rombo: <> . Dovresti anche sapere che la sintassi del diamante è associata al concetto di " inferenza di tipo ". Dopo tutto, il compilatore, vedendo <>a destra, esamina il lato sinistro dell'operatore di assegnazione, dove trova il tipo di variabile di cui si sta assegnando il valore. In base a quanto trova in questa parte, capisce il tipo del valore a destra. Infatti, se un tipo generico è dato a sinistra, ma non a destra, il compilatore può dedurre 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);
	}
}
Ma questo mescola il nuovo stile con i generici e il vecchio stile senza di essi. E questo è altamente indesiderabile. Durante la compilazione del codice sopra, otteniamo il seguente messaggio:

Note: HelloWorld.java uses unchecked or unsafe operations
In effetti, il motivo per cui devi anche aggiungere un diamante qui sembra incomprensibile. 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);
	}
}
Ricorderete che ArrayList ha un secondo costruttore che prende una collezione come argomento. Ed è qui che si nasconde qualcosa di sinistro. Senza la sintassi del diamante, il compilatore non capisce che viene ingannato. Con la sintassi del diamante, lo fa. Quindi, la regola n. 1 è: usa sempre la sintassi del diamante con i tipi parametrizzati. Altrimenti, rischiamo di perdere dove stiamo usando i tipi non elaborati. Per eliminare gli avvisi "usa operazioni non controllate o non sicure", possiamo usare l' annotazione @SuppressWarnings("unchecked") su un metodo o una classe. Ma pensa al motivo per cui hai deciso di usarlo. Ricorda la regola numero uno. Forse è necessario aggiungere un argomento di tipo.

Metodi generici Java

I generici consentono di creare metodi i cui tipi di parametro e tipo restituito sono parametrizzati. Una sezione separata è dedicata a questa funzionalità nel tutorial Oracle: " Metodi generici ". È importante ricordare la sintassi insegnata in questo tutorial:
  • include un elenco di parametri di tipo all'interno di parentesi angolari;
  • l'elenco dei parametri di tipo va prima del tipo restituito del metodo.
Diamo un'occhiata a un esempio:

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 , vedrai che ha due metodi generici. Grazie alla possibilità di inferenza del tipo, possiamo indicare il tipo direttamente al compilatore oppure possiamo specificarlo noi stessi. Entrambe le opzioni sono presentate nell'esempio. A proposito, la sintassi ha molto senso se ci pensi. Quando dichiariamo un metodo generico, specifichiamo il parametro di tipo PRIMA del metodo, perché se dichiariamo il parametro di tipo dopo il metodo, la JVM non sarebbe in grado di capire quale tipo usare. Di conseguenza, prima dichiariamo che utilizzeremo il parametro di tipo T , quindi diciamo che restituiremo questo tipo. Naturalmente, Util.<Integer>getValue(element, String.class) fallirà con un errore:tipi incompatibili: Class<String> non può essere convertito in Class<Integer> . Quando si utilizzano metodi generici, è necessario ricordare sempre la cancellazione del tipo. Diamo un'occhiata a 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);
		}
    }
}
Questo funzionerà bene. Ma solo finché il compilatore comprende che il tipo restituito del metodo chiamato è Integer . Sostituisci l'istruzione di output della console con la seguente riga:

System.out.println(Util.getValue(element) + 1);
Otteniamo un errore:

bad operand types for binary operator '+', first type: Object, second type: int.
In altre parole, si è verificata la cancellazione del tipo. Il compilatore vede che nessuno ha specificato il tipo, quindi il tipo viene indicato come Object e il metodo fallisce con un errore.

Classi generiche

Non solo i metodi possono essere parametrizzati. Anche le classi possono. La sezione "Tipi generici" del tutorial di Oracle è dedicata a questo. Consideriamo 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 usiamo la classe generica, il parametro type è indicato dopo il nome della classe. Ora creiamo un'istanza di questa classe nel metodo principale :

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Questo codice funzionerà bene. Il compilatore vede che c'è un List of numbers e una Collection of Strings . Ma cosa succede se eliminiamo il parametro type e facciamo questo:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Otteniamo un errore:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Ancora una volta, questa è la cancellazione del tipo. Poiché la classe non utilizza più un parametro di tipo, il compilatore decide che, poiché è stato passato un List , il metodo con List<Integer> è più appropriato. E falliamo con un errore. Pertanto, abbiamo la regola n. 2: se hai una classe generica, specifica sempre i parametri del tipo.

Restrizioni

Possiamo limitare i tipi specificati in metodi generici e classi. Ad esempio, supponiamo di volere che un contenitore accetti solo un numero come argomento di tipo. Questa funzionalità è descritta nella sezione Parametri di tipo limitato dell'esercitazione di Oracle. Diamo un'occhiata a 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 ristretto il parametro type alla classe/interfaccia Number o ai suoi discendenti. Si noti che è possibile specificare non solo una classe, ma anche interfacce. Per esempio:

public static class NumberContainer<T extends Number & Comparable> {
I generici supportano anche i caratteri jolly. Sono divisi in tre tipi:
  • Caratteri jolly con limite superiore — < ? estende Numero >
  • Caratteri jolly illimitati — < ? >
  • Caratteri jolly con limite inferiore — < ? super intero >
L'uso dei caratteri jolly deve rispettare il principio Get-Put . Può essere espresso come segue:
  • Utilizzare un carattere jolly esteso quando si ottengono solo valori da una struttura.
  • Usa un super jolly quando inserisci solo valori in una struttura.
  • E non usare un carattere jolly quando entrambi volete ottenere e inserire da/a una struttura.
Questo principio è anche chiamato il principio Producer Extends Consumer Super (PECS). Ecco un piccolo esempio dal codice sorgente per il metodo Collections.copy di Java : Generici in Java: come utilizzare in pratica le parentesi angolari - 5Ed ecco un piccolo esempio di cosa NON funzionerà:

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 , allora va tutto bene. Poiché popoliamo l' elenco con un valore prima di visualizzarne il contenuto, è un consumer . Di conseguenza, usiamo super.

Eredità

I generici hanno un'altra caratteristica interessante: l'ereditarietà. Il modo in cui funziona l'ereditarietà per i generici è descritto in " Generici, ereditarietà e sottotipi " nel tutorial di Oracle. L'importante è ricordare e riconoscere quanto segue. Non possiamo farlo:

List<CharSequence> list1 = new ArrayList<String>();
Perché l'ereditarietà funziona in modo diverso con i generici: Generici in Java: come utilizzare in pratica le parentesi angolari - 6ed ecco un altro buon esempio che fallirà con un errore:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Ancora una volta, tutto è semplice qui. List<String> non è un discendente di List<Object> , anche se String è un discendente di Object . Per rafforzare ciò che hai imparato, ti suggeriamo di guardare una lezione video dal nostro corso Java

Conclusione

Quindi abbiamo rinfrescato la nostra memoria riguardo ai generici. Se raramente sfrutti appieno le loro capacità, alcuni dettagli diventano sfocati. Spero che questa breve recensione ti abbia aiutato a rinfrescare la tua memoria. Per risultati ancora migliori, ti consiglio vivamente di familiarizzare con il seguente materiale:
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION