Perché dovrei preoccuparmi che Java non abbia generici reificati?


102

Questa è emersa come una domanda che ho posto di recente in un'intervista come qualcosa che il candidato desiderava vedere aggiunto al linguaggio Java. È comunemente identificato come un dolore il fatto che Java non abbia reificato i generici ma, se spinto, il candidato non poteva effettivamente dirmi il tipo di cose che avrebbe potuto ottenere se fossero lì.

Ovviamente poiché i tipi grezzi sono consentiti in Java (e i controlli non sicuri), è possibile sovvertire i generici e finire con un List<Integer>che (ad esempio) contiene effettivamente Strings. Questo chiaramente potrebbe essere reso impossibile se le informazioni sul tipo fossero reificate; ma dev'esserci di più !

Le persone potrebbero pubblicare esempi di cose che vorrebbero davvero fare , se fossero disponibili farmaci generici reificati? Voglio dire, ovviamente potresti ottenere il tipo di a Listin fase di esecuzione, ma cosa faresti con esso?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDIT : Un rapido aggiornamento a questo dato che le risposte sembrano principalmente essere preoccupate per la necessità di passare a Classcome parametro (ad esempio EnumSet.noneOf(TimeUnit.class)). Stavo cercando qualcosa di simile a dove questo non è possibile . Per esempio:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Ciò significherebbe che potresti ottenere la classe di un tipo generico in fase di esecuzione? (se è così, ho un esempio!)
James B

Penso che la maggior parte del desiderio con i generici rigenerabili provenga da persone che usano i generici principalmente con le raccolte e desiderano che quelle raccolte si comportino più come array.
kdgregory

2
La domanda più interessante (per me): cosa ci vorrebbe per implementare i generici in stile C ++ in Java? Sembra certamente fattibile in runtime, ma interromperebbe tutti i classloader esistenti (perché findClass()dovrebbe ignorare la parametrizzazione, ma defineClass()non potrebbe). E come sappiamo, The Powers That Be mantengono la compatibilità con le versioni precedenti di primaria importanza.
kdgregory

1
In realtà, Java fornisce generici reificati in modo molto limitato . Fornisco maggiori dettagli in questo thread SO: stackoverflow.com/questions/879855/…
Richard Gomes

La nota chiave JavaOne indica che Java 9 supporterà la reificazione.
Rangi Keen

Risposte:


81

Dalle poche volte in cui mi sono imbattuto in questo "bisogno", alla fine si riduce a questo costrutto:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Funziona in C # presupponendo che Tabbia un costruttore predefinito . Puoi anche ottenere il tipo di runtime da typeof(T)e ottenere i costruttori da Type.GetConstructor().

La soluzione Java comune sarebbe passare l' Class<T>argomento as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(non deve necessariamente essere passato come argomento del costruttore, poiché va bene anche un argomento del metodo, quanto sopra è solo un esempio, anche il try-catch viene omesso per brevità)

Per tutti gli altri costrutti di tipo generico, il tipo effettivo può essere facilmente risolto con un po 'di riflessione. Le seguenti domande e risposte illustrano i casi d'uso e le possibilità:


5
Funziona (ad esempio) in C #? Come fai a sapere che T ha un costruttore predefinito?
Thomas Jung,

4
Sì, lo fa. E questo è solo un esempio di base (supponiamo di lavorare con javabeans). Il punto è che con Java non è possibile ottenere la classe durante il runtime da T.classo T.getClass(), in modo da poter accedere a tutti i suoi campi, costruttori e metodi. Rende anche impossibile la costruzione.
BalusC

3
Questo mi sembra davvero debole come "grande problema", in particolare perché è probabile che sia utile solo in combinazione con alcune riflessioni molto fragili su costruttori / parametri ecc.
oxbow_lakes

33
Viene compilato in C # a condizione che si dichiari il tipo come: public class Foo <T> dove T: new (). Ciò limiterà i tipi di T validi a quelli che contengono un costruttore senza parametri.
Martin Harris,

3
@Colin In realtà puoi dire a java che un dato tipo generico è necessario per estendere più di una classe o interfaccia. Vedere la sezione "Multiple Bounds" di docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Ovviamente, non puoi dirgli che l'oggetto sarà uno di un insieme specifico che non condivide metodi che potrebbero essere astratti in un'interfaccia, che potrebbe essere in qualche modo implementata in C ++ usando la specializzazione dei modelli.)
JAB

99

La cosa che più comunemente mi morde è l'incapacità di sfruttare l'invio multiplo su più tipi generici. Quanto segue non è possibile e ci sono molti casi in cui sarebbe la soluzione migliore:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
Non c'è assolutamente bisogno della reificazione per poterlo fare. La selezione del metodo viene eseguita in fase di compilazione, quando sono disponibili le informazioni sul tipo in fase di compilazione.
Tom Hawtin - tackline

26
@ Tom: questo non viene nemmeno compilato a causa della cancellazione del tipo. Entrambi vengono compilati come public void my_method(List input) {}. Non mi sono però mai imbattuto in questa esigenza, semplicemente perché non avrebbero lo stesso nome. Se hanno lo stesso nome, mi chiedo se public <T extends Object> void my_method(List<T> input) {}non sia un'idea migliore.
BalusC

1
Hm, tenderei ad evitare del tutto il sovraccarico con lo stesso numero di parametri e preferirei qualcosa di simile myStringsMethod(List<String> input)e myIntegersMethod(List<Integer> input)anche se il sovraccarico per un caso del genere fosse possibile in Java.
Fabian Steeg

4
@ Fabian: il che significa che devi avere un codice separato e prevenire il tipo di vantaggi che ottieni <algorithm>in C ++.
David Thornley,

Sono confuso, questo è essenzialmente lo stesso del mio secondo punto, ma ho ricevuto 2 voti negativi su di esso? Qualcuno si preoccupa di illuminarmi?
rsp

34

Mi viene in mente la sicurezza dei tipi . Il downcasting a un tipo parametrizzato sarà sempre pericoloso senza generici reificati:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Inoltre, le astrazioni perderebbero meno , almeno quelle che potrebbero essere interessate alle informazioni di runtime sui loro parametri di tipo. Oggi, se hai bisogno di qualsiasi tipo di informazione di runtime sul tipo di uno dei parametri generici, devi trasmetterla anche tu Class. In questo modo, la tua interfaccia esterna dipende dalla tua implementazione (se usi RTTI per i tuoi parametri o meno).


1
Sì, ho un modo per aggirare questo in quanto creo un ParametrizedListche copia i dati nei tipi di controllo della raccolta di origine. È un po 'come, Collections.checkedListma può essere seminato con una raccolta per iniziare.
oxbow_lakes

@tackline - beh, alcune astrazioni perderebbero meno. Se hai bisogno dell'accesso per digitare i metadati nella tua implementazione, l'interfaccia esterna te lo dirà perché i client devono inviarti un oggetto classe.
gustafc

... il che significa che con i generici reificati, potresti aggiungere cose come T.class.getAnnotation(MyAnnotation.class)(dove Tè un tipo generico) senza cambiare l'interfaccia esterna.
gustafc

@gustafc: se pensi che i modelli C ++ ti diano la completa sicurezza dei tipi, leggi questo: kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory: non ho mai detto che C ++ fosse sicuro al 100%, solo che la cancellazione danneggia la sicurezza dei tipi. Come dici tu stesso, "C ++, risulta, ha una propria forma di cancellazione del tipo, nota come cast del puntatore in stile C." Ma Java esegue solo cast dinamici (non reinterpretando), quindi la reificazione collegherebbe tutto questo al sistema di tipi.
gustafc

26

Saresti in grado di creare array generici nel tuo codice.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

cosa c'è di sbagliato in Object? Un array di oggetti è comunque un array di riferimenti. Non è che i dati dell'oggetto siano in pila: sono tutti nell'heap.
Ran Biron

19
Tipo di sicurezza. Posso mettere quello che voglio in Object [], ma solo Strings in String [].
Turnor

3
Ran: Senza essere sarcastico: ti piacerebbe usare un linguaggio di scripting invece di Java, quindi hai la flessibilità di variabili non tipizzate ovunque!
pecora volante

2
Gli array sono covarianti (e quindi non tipizzati) in entrambe le lingue. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Compila bene in entrambe le lingue. Non funziona bene.
Martijn

16

Questa è una vecchia domanda, ci sono un sacco di risposte, ma penso che le risposte esistenti non siano nel segno.

"reificato" significa solo reale e di solito significa solo l'opposto del tipo di cancellazione.

Il grosso problema relativo a Java Generics:

  • Questo orribile requisito di boxe e disconnessione tra primitive e tipi di riferimento. Questo non è direttamente correlato alla reificazione o alla cancellazione del tipo. C # / Scala risolve questo problema.
  • Nessun tipo di auto. JavaFX 8 ha dovuto rimuovere i "builder" per questo motivo. Assolutamente niente a che fare con la cancellazione del tipo. Scala risolve questo problema, non sono sicuro di C #.
  • Nessuna varianza del tipo lato dichiarazione. C # 4.0 / Scala hanno questo. Assolutamente niente a che fare con la cancellazione del tipo.
  • Non posso sovraccaricare void method(List<A> l)e method(List<B> l). Ciò è dovuto alla cancellazione del tipo, ma è estremamente meschino.
  • Nessun supporto per la riflessione del tipo di runtime. Questo è il cuore della cancellazione dei caratteri. Se ti piacciono i compilatori super avanzati che verificano e dimostrano la maggior parte della logica del tuo programma in fase di compilazione, dovresti usare la riflessione il meno possibile e questo tipo di cancellazione del tipo non dovrebbe disturbarti. Se ti piace una programmazione di tipo più irregolare, scripty e dinamica e non ti interessa tanto che un compilatore dimostri quanto più possibile la tua logica corretta, allora vuoi una migliore riflessione e correggere la cancellazione del tipo è importante.

1
Per lo più lo trovo difficile con i casi di serializzazione. Spesso vorresti essere in grado di annusare i tipi di classe di cose generiche che vengono serializzate, ma sei bloccato a causa della cancellazione del tipo. Rende difficile fare qualcosa del generedeserialize(thingy, List<Integer>.class)
Cogman

3
Penso che questa sia la risposta migliore. Soprattutto parti che descrivono quali problemi sono fondamentalmente dovuti alla cancellazione del tipo e quali sono solo problemi di progettazione del linguaggio Java. La prima cosa che dico alle persone che iniziano a lamentarsi della cancellazione è che Scala e Haskell stanno lavorando anche per tipo di cancellazione.
aemxdp

13

La serializzazione sarebbe più semplice con la reificazione. Quello che vorremmo è

deserialize(thingy, List<Integer>.class);

Quello che dobbiamo fare è

deserialize(thing, new TypeReference<List<Integer>>(){});

sembra brutto e funziona in modo strano.

Ci sono anche casi in cui sarebbe davvero utile dire qualcosa di simile

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Queste cose non mordono spesso, ma mordono quando accadono.


Questo. Mille volte questo. Ogni volta che provo a scrivere codice di deserializzazione in Java, passo la giornata lamentandomi che non sto lavorando a qualcos'altro
Basic

11

La mia esposizione a Java Geneircs è piuttosto limitata e, a parte i punti già menzionati da altre risposte, c'è uno scenario spiegato nel libro Java Generics and Collections , di Maurice Naftalin e Philip Walder, dove i generici reificati sono utili.

Poiché i tipi non sono modificabili, non è possibile avere eccezioni parametrizzate.

Ad esempio la dichiarazione del modulo sottostante non è valida.

class ParametericException<T> extends Exception // compile error

Questo perché la clausola catch controlla se l'eccezione generata corrisponde a un determinato tipo. Questo controllo è uguale al controllo eseguito dal test di istanza e poiché il tipo non è modificabile, la forma di dichiarazione sopra riportata non è valida.

Se il codice precedente fosse valido, sarebbe stata possibile gestire le eccezioni nel modo seguente:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Il libro menziona anche che se i generici Java sono definiti in modo simile al modo in cui sono definiti i modelli C ++ (espansione), può portare a un'implementazione più efficiente poiché offre maggiori opportunità di ottimizzazione. Ma non offre alcuna spiegazione più di questo, quindi qualsiasi spiegazione (suggerimento) da parte di persone informate sarebbe utile.


1
È un punto valido, ma non sono abbastanza sicuro del motivo per cui una classe di eccezioni così parametrizzata sarebbe utile. Potresti modificare la tua risposta per contenere un breve esempio di quando questo potrebbe essere utile?
oxbow_lakes

@oxbow_lakes: Mi dispiace La mia conoscenza di Java Generics è piuttosto limitata e sto tentando di migliorarla. Quindi ora non sono in grado di pensare a nessun esempio in cui l'eccezione parametrizzata potrebbe essere utile. Proverò a pensarci. Grazie.
sateesh

Potrebbe fungere da sostituto per ereditarietà multipla di tipi di eccezione.
meriton

Il miglioramento delle prestazioni è che attualmente un parametro di tipo deve ereditare da Object, richiedendo il boxing di tipi primitivi, che impone un'esecuzione e un sovraccarico di memoria.
meriton

8

Gli array probabilmente funzionerebbero molto meglio con i generici se fossero reificati.


2
Certo, ma avrebbero ancora problemi. List<String>non è un List<Object>.
Tom Hawtin - tackline

D'accordo, ma solo per le primitive (Integer, Long, ecc.). Per Object "normale", questo è lo stesso. Poiché le primitive non possono essere un tipo parametrizzato (un problema molto più serio, almeno IMHO), non lo vedo come un vero dolore.
Ran Biron

3
Il problema con gli array è la loro covarianza, niente a che fare con la reificazione.
Ricorso il

5

Ho un wrapper che presenta un set di risultati jdbc come iteratore, (significa che posso testare le operazioni originate dal database molto più facilmente attraverso l'inserimento delle dipendenze).

L'API assomiglia Iterator<T> dove T è un tipo che può essere costruito utilizzando solo stringhe nel costruttore. L'iteratore quindi esamina le stringhe restituite dalla query sql e quindi cerca di abbinarle a un costruttore di tipo T.

Nell'attuale modo in cui vengono implementati i generici, devo anche passare alla classe degli oggetti che creerò dal mio gruppo di risultati. Se ho capito bene, se i generici fossero reificati, potrei semplicemente chiamare T.getClass () per ottenere i suoi costruttori, e quindi non dover eseguire il cast del risultato di Class.newInstance (), che sarebbe molto più ordinato.

Fondamentalmente, penso che renda più semplice la scrittura di API (al contrario della semplice scrittura di un'applicazione), perché puoi dedurre molto di più dagli oggetti, e quindi sarà necessaria meno configurazione ... Non ho apprezzato le implicazioni delle annotazioni fino a quando non ho li ho visti usati in cose come spring o xstream invece che in risme di configurazione.


Ma passare in classe mi sembra più sicuro. In ogni caso, la creazione riflessiva di istanze da query di database è estremamente fragile a modifiche come il refactoring (sia nel codice che nel database). Immagino che stavo cercando cose per cui non è possibile fornire la classe
oxbow_lakes

5

Una cosa carina sarebbe evitare la boxe per i tipi primitivi (valore). Ciò è in qualche modo correlato al reclamo sull'array sollevato da altri e nei casi in cui l'uso della memoria è limitato, potrebbe effettivamente fare una differenza significativa.

Esistono anche diversi tipi di problemi durante la scrittura di un framework in cui è importante essere in grado di riflettere sul tipo parametrizzato. Ovviamente questo può essere aggirato passando un oggetto di classe in runtime, ma questo oscura l'API e pone un ulteriore fardello sull'utente del framework.


2
Questo essenzialmente si riduce alla capacità di fare new T[]dove T è di tipo primitivo!
oxbow_lakes

2

Non è che otterrai qualcosa di straordinario. Sarà solo più semplice da capire. La cancellazione del tipo sembra un momento difficile per i principianti e alla fine richiede la comprensione del modo in cui funziona il compilatore.

La mia opinione è che i generici siano semplicemente un extra che consente di risparmiare un sacco di casting ridondanti.


Questo è certamente ciò che sono i generici in Java . In C # e in altri linguaggi sono uno strumento potente
Basic

1

Qualcosa che tutte le risposte qui hanno perso che è costantemente un mal di testa per me è che poiché i tipi vengono cancellati, non puoi ereditare due volte un'interfaccia generica. Questo può essere un problema quando si desidera creare interfacce a grana fine.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

Eccone uno che mi ha catturato oggi: senza reificazione, se scrivi un metodo che accetta un elenco varargs di elementi generici ... i chiamanti possono PENSARE di non essere tipizzati, ma passano accidentalmente qualsiasi vecchia rozza e fanno saltare in aria il tuo metodo.

Sembra improbabile che ciò accada? ... Certo, finché ... non usi Class come tipo di dati. A questo punto, il chiamante ti invierà volentieri molti oggetti Class, ma un semplice errore di battitura ti invierà oggetti Class che non aderiscono a T e si verificherà un disastro.

(NB: potrei aver commesso un errore qui, ma cercando su Google "generics varargs", quanto sopra sembra essere proprio quello che ti aspetteresti. La cosa che rende questo un problema pratico è l'uso di Class, credo - i chiamanti sembrano stare meno attenti :()


Ad esempio, sto usando un paradigma che utilizza gli oggetti Class come chiave nelle mappe (è più complesso di una semplice mappa, ma concettualmente è quello che sta succedendo).

ad esempio, funziona alla grande in Java Generics (esempio banale):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

es. senza reificazione in Java Generics, questo accetta QUALSIASI oggetto "Classe". Ed è solo una piccola estensione del codice precedente:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

I metodi di cui sopra devono essere scritti migliaia di volte in un singolo progetto, quindi la possibilità di errore umano diventa alta. Il debug degli errori si sta dimostrando "non divertente". Attualmente sto cercando di trovare un'alternativa, ma non ho molte speranze.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.