JavaRush /Java Blog /Random-IT /La teoria dei generici in Java o come mettere in pratica ...
Viacheslav
Livello 3

La teoria dei generici in Java o come mettere in pratica le parentesi

Pubblicato nel gruppo Random-IT

introduzione

A partire da JSE 5.0, i generici sono stati aggiunti all'arsenale del linguaggio Java.
La teoria dei generici in Java o come mettere in pratica le parentesi - 1

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 ”.

Innanzitutto, per comprendere i farmaci generici, è necessario capire perché sono necessari e cosa forniscono. Nel tutorial nella sezione " Perché usare i generici ?" Si dice che uno degli scopi sia un controllo più forte del tipo in fase di compilazione e l'eliminazione della necessità di casting esplicito.
La teoria dei generici in Java o come mettere in pratica le parentesi - 2
Prepariamo il nostro compilatore Java online tutorialspoint preferito per gli esperimenti . Immaginiamo questo 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à 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:
La teoria dei generici in Java o come mettere in pratica le parentesi - 3
Dopo la compilazione, qualsiasi informazione sui farmaci generici viene cancellata. Questo si chiama "Cancellazione del tipo" o " Cancellazione del tipo ". La cancellazione dei tipi e i generici sono progettati per fornire compatibilità con le versioni precedenti di JDK, pur consentendo al compilatore di assistere con l'inferenza del tipo nelle versioni più recenti di Java.
La teoria dei generici in Java o come mettere in pratica le parentesi - 4

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 teoria dei generici in Java o come mettere in pratica le parentesi - 5
I tipi tipizzati sono opposti, con l'indicazione di "chiarimento":
La teoria dei generici in Java o come mettere in pratica le parentesi - 6
Come possiamo vedere, abbiamo utilizzato un design insolito, contrassegnato da una freccia nello screenshot. Questa è una sintassi speciale aggiunta in Java SE 7 e si chiama " the Diamond ", che significa diamante. Perché? Puoi tracciare un'analogia tra la forma di un diamante e la forma delle parentesi graffe: <> 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.
La teoria dei generici in Java o come mettere in pratica le parentesi - 7

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.
Diamo un'occhiata ad 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, 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.
La teoria dei generici in Java o come mettere in pratica le parentesi - 8

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: Per i jolly si applica il cosiddetto principio Get Put . Possono essere espressi nella seguente forma:
La teoria dei generici in Java o come mettere in pratica le parentesi - 9
Questo principio è anche chiamato principio PECS (Producer Extends Consumer Super). Potete leggere di più su Habré nell'articolo “ Utilizzo di caratteri jolly generici per migliorare l'usabilità dell'API Java ”, nonché nell'eccellente discussione su StackOverflow: “ Utilizzo di caratteri jolly in Generics Java ”. Ecco un piccolo esempio dal sorgente Java: il metodo Collections.copy:
La teoria dei generici in Java o come mettere in pratica le parentesi - 10
Bene, un piccolo esempio di come 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, 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:
La teoria dei generici in Java o come mettere in pratica le parentesi - 11
Ed ecco un altro buon esempio che fallirà con un errore:
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: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION