Ricerca…


introduzione

I generici sono una funzionalità di programmazione generica che estende il sistema di tipi di Java per consentire a un tipo o un metodo di operare su oggetti di vario tipo fornendo nel contempo la sicurezza del tipo in fase di compilazione. In particolare, il framework delle collezioni Java supporta i generici per specificare il tipo di oggetti memorizzati in un'istanza di raccolta.

Sintassi

  • class ArrayList <E> {} // una classe generica con parametro di tipo E
  • class HashMap <K, V> {} // una classe generica con due parametri di tipo K e V
  • <E> void print (elemento E) {} // un metodo generico con parametro tipo E
  • ArrayList <String> nomi; // dichiarazione di una classe generica
  • ArrayList <?> Oggetti; // dichiarazione di una classe generica con un parametro di tipo sconosciuto
  • new ArrayList <String> () // istanza di una classe generica
  • new ArrayList <> () // istanza con tipo di inferenza "diamond" (Java 7 o successivo)

Osservazioni

I generici sono implementati in Java tramite la cancellazione dei tipi, il che significa che durante il runtime le informazioni sul tipo specificate nell'istanza di una classe generica non sono disponibili. Ad esempio, l'istruzione List<String> names = new ArrayList<>(); produce un oggetto lista da cui il tipo di elemento String non può essere recuperato in fase di runtime. Tuttavia, se l'elenco è memorizzato in un campo di tipo List<String> o passato a un parametro method / constructor di questo stesso tipo o restituito da un metodo di quel tipo restituito, le informazioni di tipo completo possono essere ripristinate in fase di runtime attraverso l'API Java Reflection.

Ciò significa anche che quando si esegue il casting su un tipo generico (es .: (List<String>) list ), il cast è un cast non controllato . Poiché il parametro <String> viene cancellato, la JVM non può controllare se un cast da un List<?> un List<String> è corretto; la JVM vede solo un cast per List da List in fase di runtime.

Creazione di una classe generica

Generics abilita classi, interfacce e metodi per prendere altre classi e interfacce come parametri di tipo.

Questo esempio utilizza la classe generica Param per prendere un singolo parametro di tipo T , delimitato da parentesi angolari ( <> ):

public class Param<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Per creare un'istanza di questa classe, fornire un argomento di tipo al posto di T Ad esempio, Integer :

Param<Integer> integerParam = new Param<Integer>();

L'argomento type può essere qualsiasi tipo di riferimento, inclusi gli array e altri tipi generici:

Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;

In Java SE 7 e versioni successive, l'argomento type può essere sostituito con un set vuoto di argomenti type ( <> ) chiamato diamond :

Java SE 7
Param<Integer> integerParam = new Param<>();

A differenza di altri identificatori, i parametri di tipo non hanno vincoli di denominazione. Tuttavia i loro nomi sono comunemente la prima lettera del loro scopo in maiuscolo. (Questo è vero anche in tutti i JavaDocs ufficiali.)
Gli esempi includono T per "tipo" , E per "elemento" e K / V per "chiave" / "valore" .


Estendere una classe generica

public abstract class AbstractParam<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

AbstractParam è una classe astratta dichiarata con un parametro di tipo T Quando si estende questa classe, quel parametro di tipo può essere sostituito da un argomento di tipo scritto all'interno di <> , oppure il parametro type può rimanere invariato. Nel primo e nel secondo esempio di seguito, String e Integer sostituiscono il parametro type. Nel terzo esempio, il parametro type rimane invariato. Il quarto esempio non usa affatto i generici, quindi è simile a se la classe avesse un parametro Object . Il compilatore avverte che AbstractParam è un tipo non ObjectParam , ma compilerà la classe ObjectParam . Il quinto esempio ha 2 parametri di tipo (vedi "parametri di tipo multiplo" sotto), scegliendo il secondo parametro come parametro di tipo passato alla superclasse.

public class Email extends AbstractParam<String> {
    // ...
}

public class Age extends AbstractParam<Integer> {
    // ...
}

public class Height<T> extends AbstractParam<T> {
    // ...
}

public class ObjectParam extends AbstractParam {
    // ...
}

public class MultiParam<T, E> extends AbstractParam<E> {
    // ...
}

Quanto segue è l'uso:

Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();

Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();

Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);

Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);

MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);

Si noti che nella classe Email , il metodo T getValue() agisce come se avesse una firma di String getValue() , e il void setValue(T) agisce come se fosse stato dichiarato void setValue(String) .

È anche possibile creare un'istanza con classe interna anonima con parentesi graffe vuote ( {} ):

AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);

Nota che l' uso del diamante con classi interne anonime non è permesso.


Parametri di tipo multiplo

Java offre la possibilità di utilizzare più di un parametro di tipo in una classe o interfaccia generica. È possibile utilizzare più parametri di tipo in una classe o in un'interfaccia inserendo un elenco di tipi separati da virgole tra parentesi angolari. Esempio:

public class MultiGenericParam<T, S> {
    private T firstParam;
    private S secondParam;
   
    public MultiGenericParam(T firstParam, S secondParam) {
        this.firstParam = firstParam;
        this.secondParam = secondParam;
    }
    
    public T getFirstParam() {
        return firstParam;
    }
    
    public void setFirstParam(T firstParam) {
        this.firstParam = firstParam;
    }
    
    public S getSecondParam() {
        return secondParam;
    }
    
    public void setSecondParam(S secondParam) {
        this.secondParam = secondParam;
    }
}

L'utilizzo può essere eseguito come di seguito:

MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);

Dichiarazione di un metodo generico

I metodi possono anche avere parametri di tipo generico .

public class Example {

    // The type parameter T is scoped to the method
    // and is independent of type parameters of other methods.
    public <T> List<T> makeList(T t1, T t2) {
        List<T> result = new ArrayList<T>();
        result.add(t1);
        result.add(t2);
        return result;
    }

    public void usage() {
        List<String> listString = makeList("Jeff", "Atwood");
        List<Integer> listInteger = makeList(1, 2);
    }
}

Si noti che non è necessario passare un argomento di tipo effettivo a un metodo generico. Il compilatore deduce l'argomento tipo per noi, in base al tipo di destinazione (ad esempio, la variabile a cui assegniamo il risultato) o ai tipi degli argomenti effettivi. In genere inferirà l'argomento di tipo più specifico che renderà corretta la chiamata.

A volte, anche se raramente, può essere necessario sovrascrivere questo tipo di inferenza con argomenti di tipo esplicito:

void usage() {
    consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}

void consumeObjects(Stream<Object> stream) { ... }

È necessario in questo esempio perché il compilatore non può "guardare avanti" per vedere che l' Object è desiderato per T dopo aver chiamato stream() e altrimenti inferirebbe String basato sugli argomenti makeList . Si noti che il linguaggio Java non supporta l'omissione della classe o dell'oggetto su cui viene chiamato il metodo ( this nell'esempio precedente) quando vengono forniti esplicitamente argomenti tipo.

Il diamante

Java SE 7

Java 7 ha introdotto il Diamond 1 per rimuovere alcuni piatti della caldaia attorno all'istanziazione di classe generica. Con Java 7+ puoi scrivere:

List<String> list = new LinkedList<>();

Dove dovevi scrivere nelle versioni precedenti, questo:

List<String> list = new LinkedList<String>();

Una limitazione è per le classi anonime , in cui è comunque necessario fornire il parametro type nell'istanza:

// This will compile:

Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};

// But this will not:

Comparator<String> caseInsensitiveComparator = new Comparator<>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};
Java SE 8

Sebbene l'uso del diamante con le classi interne anonime non sia supportato in Java 7 e 8, verrà incluso come una nuova funzionalità in Java 9 .


Nota:

1 - Alcune persone chiamano <> uso di " operatore diamante". Questo non è corretto Il diamante non si comporta come un operatore e non è descritto o elencato in nessuna parte del JLS o dei tutorial Java (ufficiali) come operatore. Infatti, <> non è nemmeno un token Java distinto. Piuttosto si tratta di un < pedina seguito da un > Token, ed è legale (anche se cattivo stile) di avere spazi bianchi o commenti tra i due. Il JLS e i Tutorials si riferiscono costantemente a <> come "il diamante", e questo è quindi il termine corretto per esso.

Richiesta di più limiti superiori ("estende A & B")

È possibile richiedere un tipo generico per estendere più limiti superiori.

Esempio: vogliamo ordinare una lista di numeri ma Number non implementa Comparable .

public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
  Collections.sort( n );
}

In questo esempio T deve estendere il Number e implementare il Comparable<T> che dovrebbe adattarsi a tutte le implementazioni del numero incorporato "normale" come Integer o BigDecimal ma non si adatta a quelle più esotiche come Striped64 .

Poiché l'ereditarietà multipla non è consentita, è possibile utilizzare al massimo una classe come limite e deve essere la prima elencata. Ad esempio, <T extends Comparable<T> & Number> non è consentito perché Comparable è un'interfaccia e non una classe.

Creazione di una classe generica limitata

È possibile limitare i tipi validi utilizzati in una classe generica delimitando quel tipo nella definizione della classe. Data la seguente gerarchia di tipi semplici:

public abstract class Animal {
    public abstract String getSound();
}

public class Cat extends Animal {
    public String getSound() {
        return "Meow";
    }
}

public class Dog extends Animal {
    public String getSound() {
        return "Woof";
    }
}

Senza generici limitati , non possiamo creare una classe contenitore che sia sia generica che sa che ogni elemento è un animale:

public class AnimalContainer<T> {

    private Collection<T> col;

    public AnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Illegal, type T doesn't have makeSound()
            // it is used as an java.lang.Object here
            System.out.println(t.makeSound()); 
        }
    }
}

Con il limite generico nella definizione della classe, questo è ora possibile.

public class BoundedAnimalContainer<T extends Animal> { // Note bound here.

    private Collection<T> col;

    public BoundedAnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Now works because T is extending Animal
            System.out.println(t.makeSound()); 
        }
    }
}

Ciò limita anche le istanze valide del tipo generico:

// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();

// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();

// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();

Decidere tra `T`,`? super T`, e `? estende T`

La sintassi per i caratteri jolly limitati generici Java, che rappresentano il tipo sconosciuto da ? è:

  • ? extends T rappresenta un carattere jolly con limite superiore. Il tipo sconosciuto rappresenta un tipo che deve essere un sottotipo di T o di tipo T stesso.

  • ? super T rappresenta un carattere jolly con limite inferiore. Il tipo sconosciuto rappresenta un tipo che deve essere un supertipo di T o di tipo T stesso.

Come regola generale, dovresti usare

  • ? extends T se hai solo bisogno dell'accesso "lettura" ("input")
  • ? super T se hai bisogno dell'accesso "write" ("output")
  • T se hai bisogno di entrambi ("modifica")

Usando extends o super è solitamente meglio perché rende il tuo codice più flessibile (come in: consentire l'uso di sottotipi e supertipi), come vedrai di seguito.

class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}

   public class FruitHelper {

        public void eatAll(Collection<? extends Fruit> fruits) {}

        public void addApple(Collection<? super Apple> apples) {}
}

Il compilatore ora sarà in grado di rilevare alcuni usi errati:

 public class GenericsTest {
      public static void main(String[] args){
  FruitHelper fruitHelper = new FruitHelper() ;
    List<Fruit> fruits = new ArrayList<Fruit>();
    fruits.add(new Apple()); // Allowed, as Apple is a Fruit
    fruits.add(new Banana()); // Allowed, as Banana is a Fruit
    fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
    fruitHelper.eatAll(fruits); // Allowed

    Collection<Banana> bananas = new ArrayList<>();
    bananas.add(new Banana()); // Allowed
    //fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
    fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits

    Collection<Apple> apples = new ArrayList<>();
    fruitHelper.addApple(apples); // Allowed
    apples.add(new GrannySmith()); // Allowed, as this is an Apple
    fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
    
    Collection<GrannySmith> grannySmithApples = new ArrayList<>();
    fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
                                   // GrannySmith is not a supertype of Apple
    apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
    fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit

    Collection<Object> objects = new ArrayList<>();
    fruitHelper.addApple(objects); // Allowed, as Object super Apple
    objects.add(new Shoe()); // Not a fruit
    objects.add(new IPhone()); // Not a fruit
    //fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}

Scegli la T giusta ? super T o ? extends T è necessario per consentire l'uso con sottotipi. Il compilatore può quindi garantire la sicurezza del tipo; non dovrebbe essere necessario eseguire il cast (che non è sicuro per il tipo e potrebbe causare errori di programmazione) se vengono utilizzati correttamente.

Se non è facile da capire, ricorda la regola PECS :

Il roditore usa " E xtends" e C onsumer usa " S uper".

(Il produttore ha solo accesso in scrittura e Consumer ha solo accesso in lettura)

Vantaggi della classe generica e dell'interfaccia

Il codice che utilizza i generici ha molti vantaggi rispetto al codice non generico. Di seguito sono riportati i principali vantaggi


Controlli di tipo più potenti al momento della compilazione

Un compilatore Java applica un controllo di tipo forte al codice generico e genera errori se il codice viola la sicurezza del tipo. Correggere gli errori in fase di compilazione è più semplice che correggere errori di runtime, che possono essere difficili da trovare.


Eliminazione di calchi

Il seguente snippet di codice senza generici richiede il casting:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

Quando viene riscritto per utilizzare i generici , il codice non richiede il casting:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);   // no cast

Abilitare i programmatori ad implementare algoritmi generici

Usando i generici, i programmatori possono implementare algoritmi generici che funzionano su raccolte di tipi diversi, possono essere personalizzati e sono sicuri e facili da leggere.

Associazione di parametri generici a più di 1 tipo

I parametri generici possono anche essere associati a più di un tipo usando la sintassi T extends Type1 & Type2 & ...

Diciamo che vuoi creare una classe il cui tipo generico dovrebbe implementare sia Flushable che Closeable , puoi scrivere

class ExampleClass<T extends Flushable & Closeable> {
}

Ora, ExampleClass accetta solo come parametri generici, tipi che implementano sia Flushable che Closeable .

ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable

ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable

ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.

I metodi di classe possono scegliere di dedurre argomenti di tipo generico come Closeable o Flushable .

class ExampleClass<T extends Flushable & Closeable> {
    /* Assign it to a valid type as you want. */
    public void test (T param) {
        Flushable arg1 = param; // Works
        Closeable arg2 = param; // Works too.
    }

    /* You can even invoke the methods of any valid type directly. */
    public void test2 (T param) {
        param.flush(); // Method of Flushable called on T and works fine.
        param.close(); // Method of Closeable called on T and works fine too.
    }
}

Nota:

Non è possibile associare il parametro generico a uno dei tipi utilizzando la clausola OR ( | ). È supportata solo la clausola AND ( & ). Il tipo generico può estendere solo una classe e molte interfacce. La classe deve essere posizionata all'inizio della lista.

Istanziare un tipo generico

A causa della cancellazione del tipo, quanto segue non funzionerà:

public <T> void genericMethod() {
    T t = new T(); // Can not instantiate the type T.
}

Il tipo T viene cancellato. Dato che, in fase di esecuzione, la JVM non sa cosa fosse originariamente T , non sa quale costruttore chiamare.


soluzioni alternative

  1. Passando alla classe di T quando si chiama genericMethod :

    public <T> void genericMethod(Class<T> cls) {
        try {
            T t = cls.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
             System.err.println("Could not instantiate: " + cls.getName());
        }
    }
    
    genericMethod(String.class);
    

    Che genera eccezioni, poiché non c'è modo di sapere se la classe passata ha un costruttore predefinito accessibile.

Java SE 8
  1. Passando un riferimento al costruttore di T :

    public <T> void genericMethod(Supplier<T> cons) {
        T t = cons.get();
    }
    
    genericMethod(String::new);
    

Riferendosi al tipo generico dichiarato all'interno della propria dichiarazione

Come si fa a utilizzare un'istanza di un (eventualmente) ulteriore tipo generico ereditato all'interno di una dichiarazione di metodo nel tipo generico stesso che viene dichiarato? Questo è uno dei problemi che affronterai quando approfondirai un po 'più in profondità i generici, ma ancora piuttosto comune.

Supponiamo di avere un DataSeries<T> (interfaccia qui), che definisce una serie di dati generica contenente valori di tipo T È complicato lavorare con questo tipo direttamente quando vogliamo eseguire molte operazioni, ad esempio con valori doppi, quindi definiamo DoubleSeries extends DataSeries<Double> . Ora supponiamo che il tipo DataSeries<T> abbia un metodo add(values) che aggiunge un'altra serie della stessa lunghezza e ne restituisce una nuova. Come si impone il tipo di values e il tipo di ritorno da DoubleSeries anziché DataSeries<Double> nella nostra classe derivata?

Il problema può essere risolto aggiungendo un parametro di tipo generico che fa riferimento a ed estende il tipo da dichiarare (applicato a un'interfaccia qui, ma lo stesso è valido per le classi):

public interface DataSeries<T, DS extends DataSeries<T, DS>> {
    DS add(DS values);
    List<T> data();
}

Qui T rappresenta il tipo di dati che la serie detiene, ad es. Double e DS la serie stessa. Un tipo (o tipo) ereditato può ora essere facilmente implementato sostituendo il parametro sopra citato con un corrispondente tipo derivato, ottenendo così una definizione concreta basata sulla Double base della forma:

public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
    static DoubleSeries instance(Collection<Double> data) {
        return new DoubleSeriesImpl(data);
    }
}

In questo momento anche un IDE implementerà l'interfaccia di cui sopra con i tipi corretti in posizione, che, dopo un po 'di riempimento del contenuto potrebbe assomigliare a questo:

class DoubleSeriesImpl implements DoubleSeries {
    private final List<Double> data;

    DoubleSeriesImpl(Collection<Double> data) {
        this.data = new ArrayList<>(data);
    }

    @Override
    public DoubleSeries add(DoubleSeries values) {
        List<Double> incoming = values != null ? values.data() : null;
        if (incoming == null || incoming.size() != data.size()) {
            throw new IllegalArgumentException("bad series");
        }
        List<Double> newdata = new ArrayList<>(data.size());
        for (int i = 0; i < data.size(); i++) {
            newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
        }
        return DoubleSeries.instance(newdata);
    }

    @Override
    public List<Double> data() {
        return Collections.unmodifiableList(data);
    }
}

Come puoi vedere il metodo add viene dichiarato come DoubleSeries add(DoubleSeries values) e il compilatore è soddisfatto.

Il modello può essere ulteriormente nidificato, se necessario.

Uso di instanceof con Generics

Usare i generici per definire il tipo in instanceof

Si consideri la seguente classe generica Example dichiarato con il parametro formale <T> :

class Example<T> {
    public boolean isTypeAString(String s) {
        return s instanceof T; // Compilation error, cannot use T as class type here
    }
}

Questo darà sempre un errore di compilazione perché non appena il compilatore compila il codice sorgente Java nel bytecode Java applica un processo noto come cancellazione del tipo , che converte tutto il codice generico in codice non generico, rendendo impossibile distinguere tra i tipi T in fase di esecuzione. Il tipo utilizzato con instanceof deve essere reifiable , il che significa che tutte le informazioni sul tipo devono essere disponibili in fase di esecuzione, e questo di solito non è il caso per i tipi generici.

La seguente classe rappresenta ciò che due diverse classi di Example , Example<String> ed Example<Number> , assomigliano dopo che i generici sono stati cancellati dalla cancellazione del tipo :

class Example { // formal parameter is gone
    public boolean isTypeAString(String s) {
        return s instanceof Object; // Both <String> and <Number> are now Object
    }
}

Poiché i tipi sono andati, non è possibile per JVM sapere quale tipo è T


Eccezione alla regola precedente

È sempre possibile utilizzare un carattere jolly illimitato (?) Per specificare un tipo instanceof come segue:

    public boolean isAList(Object obj) {
        return obj instanceof List<?>;
    }

Questo può essere utile per valutare se un'istanza obj è una List o no:

System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true

In effetti, il carattere jolly illimitato è considerato un tipo reifiable.


Utilizzando un'istanza generica con instanceof

L'altro lato della medaglia è che l'uso di un'istanza t di T con instanceof è legale, come mostrato nell'esempio seguente:

class Example<T> {
    public boolean isTypeAString(T t) {
        return t instanceof String; // No compilation error this time
    }
}

perché dopo la cancellazione del tipo la classe sarà simile alla seguente:

class Example { // formal parameter is gone
    public boolean isTypeAString(Object t) {
        return t instanceof String; // No compilation error this time
    }
}

Dato che, anche se la cancellazione del tipo avviene comunque, ora la JVM può distinguere tra diversi tipi di memoria, anche se usano lo stesso tipo di riferimento ( Object ), come mostra il seguente frammento:

Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String

Diversi modi per implementare un'interfaccia generica (o estendere una classe generica)

Supponiamo che la seguente interfaccia generica sia stata dichiarata:

public interface MyGenericInterface<T> {
    public void foo(T t);
}

Di seguito sono elencati i possibili modi per implementarlo.


Implementazione di classi non generiche con un tipo specifico

Scegliere un tipo specifico per sostituire il parametro di tipo formale <T> di MyGenericClass e implementarlo, come nell'esempio seguente:

public class NonGenericClass implements MyGenericInterface<String> {
    public void foo(String t) { } // type T has been replaced by String
}

Questa classe si occupa solo di String , e questo significa che l'utilizzo di MyGenericInterface con parametri diversi (ad esempio Integer , Object ecc.) Non verrà compilato, come mostra il seguente frammento:

NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile

Implementazione di classe generica

Dichiarare un'altra interfaccia generica con il parametro di tipo formale <T> che implementa MyGenericInterface , come segue:

public class MyGenericSubclass<T> implements MyGenericInterface<T> {
    public void foo(T t) { } // type T is still the same
    // other methods...
}

Si noti che un parametro di tipo formale diverso potrebbe essere stato utilizzato, come segue:

public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
    public void foo(U t) { }
    // other methods...
}

Implementazione della classe del tipo non elaborata

Dichiarare una classe non generica che implementa MyGenericInteface come tipo non MyGenericInteface (senza utilizzare generici), come segue:

public class MyGenericSubclass implements MyGenericInterface {
    public void foo(Object t) { } // type T has been replaced by Object
    // other possible methods
}

In questo modo non è consigliabile, poiché non è sicuro al 100% in fase di esecuzione perché mischia il tipo non elaborato (della sottoclasse) con generici (dell'interfaccia) ed è anche fonte di confusione. I moderni compilatori Java solleveranno un avvertimento con questo tipo di implementazione, tuttavia il codice - per ragioni di compatibilità con JVM precedente (1.4 o precedenti) - verrà compilato.


Tutti i modi elencati sopra sono consentiti anche quando si utilizza una classe generica come supertipo invece di un'interfaccia generica.

Uso di Generics per il cast automatico

Con i generici, è possibile restituire ciò che il chiamante si aspetta:

private Map<String, Object> data;
public <T> T get(String key) {
    return (T) data.get(key);
}

Il metodo verrà compilato con un avviso. Il codice è in realtà più sicuro di quanto sembri perché il runtime Java eseguirà un cast quando lo si utilizza:

Bar bar = foo.get("bar");

È meno sicuro quando si utilizzano tipi generici:

List<Bar> bars = foo.get("bars");

Qui, il cast funzionerà quando il tipo restituito è un qualsiasi tipo di List (ad esempio, la restituzione di List<String> non innescherebbe un ClassCastException , ma alla fine lo si otterrà quando si eliminano elementi dall'elenco).

Per aggirare questo problema, puoi creare un'API che utilizza le chiavi digitate:

public final static Key<List<Bar>> BARS = new Key<>("BARS");

insieme a questo metodo put() :

public <T> T put(Key<T> key, T value);

Con questo approccio, non è possibile inserire il tipo sbagliato nella mappa, quindi il risultato sarà sempre corretto (a meno che non si creino accidentalmente due chiavi con lo stesso nome ma tipi diversi).

Relazionato:

Ottieni una classe che soddisfi i parametri generici in fase di esecuzione

Molti parametri generici non associati, come quelli utilizzati in un metodo statico, non possono essere ripristinati in fase di esecuzione (vedere Altri thread su Cancellazione ). Tuttavia esiste una strategia comune utilizzata per accedere al tipo che soddisfa un parametro generico su una classe in fase di esecuzione. Ciò consente il codice generico che dipende dall'accesso al tipo senza dover inserire informazioni sul tipo tramite ogni chiamata.

sfondo

La parametrizzazione generica su una classe può essere ispezionata creando una classe interna anonima. Questa classe acquisirà le informazioni sul tipo. In generale questo meccanismo è indicato come token di tipo super , che sono dettagliati nel post del blog di Neal Gafter .

implementazioni

Tre implementazioni comuni in Java sono:

Esempio di utilizzo

public class DataService<MODEL_TYPE> {
     private final DataDao dataDao = new DataDao();
     private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
                                                                (getClass()){}.getRawType();
     public List<MODEL_TYPE> getAll() {
         return dataDao.getAllOfType(type);
    }
}

// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be 
// recovered at runtime
public class UserService extends DataService<User> {}

public class Main {
    public static void main(String[] args) {
          UserService service = new UserService();
          List<User> users = service.getAll();
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow