Perché alcuni sostengono che l'implementazione di Java dei generici sia dannosa?


115

Di tanto in tanto ho sentito che con i generici Java non funzionava correttamente. (riferimento più vicino, qui )

Perdonate la mia inesperienza, ma cosa li avrebbe resi migliori?


23
Non vedo motivo per chiudere questo; con tutte le stronzate che circolano sui generici, avere qualche chiarimento tra le differenze tra Java e C # potrebbe aiutare le persone che cercano di schivare il fango mal informato.
Rob il

Risposte:


146

Male:

  • Le informazioni sul tipo vengono perse in fase di compilazione, quindi in fase di esecuzione non è possibile dire quale tipo deve essere "destinato"
  • Non può essere utilizzato per i tipi di valore (questo è un grosso problema - in .NET, ad esempio, List<byte>è supportato da un byte[]e non è richiesta la boxe)
  • La sintassi per chiamare metodi generici fa schifo (IMO)
  • La sintassi dei vincoli può creare confusione
  • I caratteri jolly sono generalmente fonte di confusione
  • Varie restrizioni dovute a quanto sopra - casting ecc

Buona:

  • Il carattere jolly consente di specificare la covarianza / controvarianza sul lato della chiamata, il che è molto chiaro in molte situazioni
  • Meglio di niente!

51
@Paul: Penso che avrei preferito una rottura temporanea ma successivamente un'API decente. Oppure prendi il percorso .NET e introduce nuovi tipi di raccolta. (I generici .NET nella 2.0 non hanno danneggiato le app 1.1.)
Jon Skeet

6
Con una grande base di codice esistente, mi piace il fatto di poter iniziare a cambiare la firma di un metodo per utilizzare i generici senza cambiare chiunque lo chiami.
Paul Tomblin,

38
Ma gli svantaggi di questo sono enormi e permanenti. C'è una grande vittoria a breve termine e una perdita a lungo termine IMO.
Jon Skeet,

11
Jon - "È meglio di niente" è soggettivo :)
MetroidFan2002

6
@Rogerio: Hai affermato che il mio primo articolo "Cattivo" non è corretto. Il mio primo elemento "Bad" dice che le informazioni vengono perse in fase di compilazione. Affinché ciò non sia corretto, devi dire che le informazioni non vengono perse in fase di compilazione ... Per quanto riguarda la confusione con gli altri sviluppatori, sembra che tu sia l'unico confuso da ciò che sto dicendo.
Jon Skeet

26

Il problema più grande è che i generici Java sono solo una cosa in fase di compilazione e puoi sovvertirlo in fase di esecuzione. C # è elogiato perché esegue più controlli in fase di esecuzione. Ci sono alcune discussioni davvero interessanti in questo post e si collega ad altre discussioni.


Non è davvero un problema, non è mai stato creato per il runtime. È come se dicessi che le barche sono un problema perché non possono scalare le montagne. Come linguaggio, Java fa ciò di cui molte persone hanno bisogno: esprimere i tipi in modo significativo. Per le informazioni di tipo runtime, le persone possono ancora passare gli Classoggetti in giro.
Vincent Cantin

1
Ha ragione. la tua anologia è sbagliata perché le persone che progettano la barca DOVREBBERO averla progettata per scalare le montagne. I loro obiettivi di progettazione non erano molto ben pensati per ciò che era necessario. Ora siamo tutti bloccati solo con una barca e non possiamo scalare le montagne.
WiegleyJ

1
@VincentCantin - è decisamente un problema. Quindi, ce ne stiamo tutti lamentando. I generici Java sono cotti a metà.
Josh M.

17

Il problema principale è che Java in realtà non ha generici in fase di esecuzione. È una funzionalità in fase di compilazione.

Quando crei una classe generica in Java, usano un metodo chiamato "Type Erasure" per rimuovere effettivamente tutti i tipi generici dalla classe e sostanzialmente sostituirli con Object. La versione più alta dei generics è che il compilatore inserisce semplicemente i cast nel tipo generico specificato ogni volta che appare nel corpo del metodo.

Questo ha molti aspetti negativi. Uno dei più grandi, IMHO, è che non puoi usare la riflessione per ispezionare un tipo generico. I tipi non sono effettivamente generici nel codice byte e quindi non possono essere ispezionati come generici.

Ottima panoramica delle differenze qui: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
Non sono sicuro del motivo per cui sei stato votato. Da quello che ho capito hai ragione e quel link è molto informativo. Up-votato.
Jason Jackson,

@ Jason, grazie. C'era un errore di battitura nella mia versione originale, credo di essere stato svalutato per questo.
JaredPar

3
Err, perché puoi usare la reflection per esaminare un tipo generico, una firma del metodo generico. Non è possibile utilizzare la riflessione per esaminare le informazioni generiche associate a un'istanza parametrizzata. Ad esempio, se ho una classe con un metodo foo (List <String>), posso trovare in modo riflessivo "String"
oxbow_lakes

Ci sono molti articoli che dicono che la cancellazione del tipo rende impossibile trovare i parametri di tipo effettivi. Diciamo: Oggetto x = nuovo X [Y] (); puoi mostrare come, data x, trovare il tipo Y?
user48956

@oxbow_lakes - dover usare la riflessione per trovare un tipo generico è quasi altrettanto grave che non essere in grado di farlo. Ad esempio, è una cattiva soluzione.
Josh M.

14
  1. Implementazione runtime (cioè non cancellazione del tipo);
  2. La capacità di usare tipi primitivi (questo è correlato a (1));
  3. Mentre il carattere jolly è utile, la sintassi e sapere quando usarlo è qualcosa che lascia perplessi molte persone. e
  4. Nessun miglioramento delle prestazioni (a causa di (1); i generici Java sono zucchero sintattico per la fusione di oggetti).

(1) porta a comportamenti molto strani. Il miglior esempio a cui riesco a pensare è. Assumere:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

quindi dichiara due variabili:

MyClass<T> m1 = ...
MyClass m2 = ...

Adesso chiama getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

Il secondo ha il suo argomento di tipo generico rimosso dal compilatore perché è un tipo grezzo (il che significa che il tipo parametrizzato non viene fornito) anche se non ha nulla a che fare con il tipo parametrizzato.

Citerò anche la mia dichiarazione preferita del JDK:

public class Enum<T extends Enum<T>>

A parte i caratteri jolly (che è un miscuglio) penso solo che i generici .Net siano migliori.


Ma il compilatore te lo dirà, in particolare con l'opzione lint rawtypes.
Tom Hawtin - tackline

Non ho capito, perché l'ultima riga è un errore di compilazione? Sto scherzando con esso in Eclipse e non riesco a farlo fallire lì - se aggiungo abbastanza cose per la compilazione del resto.
oconnor0

public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop

@ oconnor0 Non è un errore di compilazione, ma un avviso del compilatore, senza motivo apparente:The expression of type List needs unchecked conversion to conform to List<String>
artbristol

Enum<T extends Enum<T>>può sembrare strano / ridondante all'inizio, ma in realtà è piuttosto interessante, almeno entro i limiti di Java / i suoi generici. Gli enum hanno un values()metodo statico che fornisce un array dei loro elementi digitati come enum, not Enum, e quel tipo è determinato dal parametro generico, il che significa che vuoi Enum<T>. Ovviamente, la digitazione ha senso solo nel contesto di un tipo enumerato e tutte le enumerazioni sono sottoclassi di Enum, quindi lo desideri Enum<T extends Enum>. Tuttavia, a Java non piace mescolare tipi grezzi con generici, quindi Enum<T extends Enum<T>>per coerenza.
JAB

10

Butto fuori un'opinione davvero controversa. I generici complicano la lingua e complicano il codice. Ad esempio, diciamo che ho una mappa che mappa una stringa in un elenco di stringhe. Ai vecchi tempi, potevo dichiararlo semplicemente come

Map someMap;

Ora, devo dichiararlo come

Map<String, List<String>> someMap;

E ogni volta che lo passo in qualche metodo, devo ripetere di nuovo quella grande e lunga dichiarazione. A mio parere, tutta quella digitazione in più distrae lo sviluppatore e lo porta fuori "dalla zona". Inoltre, quando il codice è pieno di molti cruft, a volte è difficile tornarci più tardi e setacciare rapidamente tutto il cruft per trovare la logica importante.

Java ha già una cattiva reputazione per essere uno dei linguaggi più prolissi di uso comune, e i generici si aggiungono semplicemente a questo problema.

E cosa compri veramente per tutta quella verbosità extra? Quante volte hai davvero avuto problemi in cui qualcuno ha inserito un numero intero in una raccolta che dovrebbe contenere stringhe o in cui qualcuno ha cercato di estrarre una stringa da una raccolta di numeri interi? Nei miei 10 anni di esperienza lavorativa nella creazione di applicazioni Java commerciali, questa non è mai stata una grande fonte di errori. Quindi, non sono davvero sicuro di cosa stai ottenendo per la verbosità extra. Mi sembra davvero un bagaglio burocratico extra.

Ora diventerò davvero controverso. Quello che vedo come il problema più grande con le raccolte in Java 1.4 è la necessità di typecast ovunque. Considero quei typecast come cruft extra e dettagliati che hanno molti degli stessi problemi dei generici. Quindi, ad esempio, non posso semplicemente fare

List someList = someMap.get("some key");

devo fare

List someList = (List) someMap.get("some key");

La ragione, ovviamente, è che get () restituisce un Object che è un supertipo di List. Quindi l'assegnazione non può essere eseguita senza un typecast. Di nuovo, pensa a quanto ti fa veramente guadagnare quella regola. Dalla mia esperienza, non molto.

Penso che Java sarebbe stato molto meglio se 1) non avesse aggiunto i generici ma 2) invece avesse consentito il casting implicito da un supertipo a un sottotipo. Consentire il rilevamento di cast errati in fase di esecuzione. Allora avrei potuto avere la semplicità di definire

Map someMap;

e successivamente

List someList = someMap.get("some key");

tutto il cruft sarebbe sparito e non credo proprio che introdurrei una nuova grande fonte di bug nel mio codice.


15
Mi dispiace, non sono d'accordo con quasi tutto quello che hai detto. Ma non ho intenzione di votare contro perché lo discuti bene.
Paul Tomblin,

2
Naturalmente, ci sono molti esempi di grandi sistemi di successo costruiti in linguaggi come Python e Ruby che fanno esattamente quello che ho suggerito nella mia risposta.
Clint Miller,

Penso che la mia intera obiezione a questo tipo di idea non sia che sia una cattiva idea di per sé, ma piuttosto che, poiché linguaggi moderni come Python e Ruby rendono la vita più facile e facile per gli sviluppatori, gli sviluppatori a loro volta diventano intellettualmente compiacenti e alla fine hanno un comprensione del proprio codice.
Dan Tao

4
"E cosa compri veramente per tutta quella verbosità in più? ..." Dice al povero programmatore di manutenzione cosa dovrebbe esserci in quelle raccolte. Ho riscontrato che questo è un problema quando si lavora con le applicazioni Java commerciali.
richj

4
"Quante volte hai davvero avuto problemi quando qualcuno ha inserito una [x] in una raccolta che dovrebbe contenere [y] s?" - Oh ragazzo ho perso il conto! Inoltre, anche se non ci sono bug, è un killer di leggibilità. E la scansione di molti file per scoprire quale sarà l'oggetto (o il debug) mi tira davvero fuori dalla zona. Potrebbe piacerti Haskell - anche una digitazione forte ma meno cruft (perché i tipi sono dedotti).
Martin Capodici

8

Un altro effetto collaterale del fatto che sono in fase di compilazione e non in fase di esecuzione è che non è possibile chiamare il costruttore del tipo generico. Quindi non puoi usarli per implementare una fabbrica generica ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++


6

I generici Java vengono controllati per verificarne la correttezza in fase di compilazione e quindi tutte le informazioni sul tipo vengono rimosse (il processo è chiamato cancellazione del tipo . Pertanto, il generico List<Integer>sarà ridotto al suo tipo grezzo , non generico List, che può contenere oggetti di classe arbitraria.

Ciò si traduce nella possibilità di inserire oggetti arbitrari nell'elenco in fase di esecuzione, così come ora è impossibile dire quali tipi sono stati usati come parametri generici. Quest'ultimo a sua volta si traduce in

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");

5

Vorrei che questo fosse un wiki in modo da poter aggiungere ad altre persone ... ma ...

I problemi:

  • Type Erasure (nessuna disponibilità runtime)
  • Nessun supporto per i tipi primativi
  • Incompatibilità con le annotazioni (sono state entrambe aggiunte nella 1.5, non sono ancora sicuro del motivo per cui le annotazioni non consentono ai generici oltre a affrettare le funzionalità)
  • Incompatibilità con gli array. (A volte vorrei davvero fare qualcosa come Class <? Estende MyObject >[], ma non sono autorizzato)
  • Sintassi e comportamento dei caratteri jolly stravaganti
  • Il fatto che il supporto generico non sia coerente tra le classi Java. L'hanno aggiunto alla maggior parte dei metodi di raccolta, ma ogni tanto ti imbatti in un'istanza in cui non è presente.

5

Ignorando l'intero tipo di disordine di cancellazione, i generici come specificato semplicemente non funzionano.

Questo compila:

List<Integer> x = Collections.emptyList();

Ma questo è un errore di sintassi:

foo(Collections.emptyList());

Dove foo è definito come:

void foo(List<Integer> x) { /* method body not important */ }

Quindi il controllo di un tipo di espressione dipende dal fatto che venga assegnato a una variabile locale o a un parametro effettivo di una chiamata al metodo. Quanto è pazzo?


3
L'inferenza incoerente di javac è una schifezza.
mP.

3
Presumo che il motivo per cui quest'ultima forma è stata rifiutata è perché potrebbe esserci più di una versione di "foo" a causa del sovraccarico del metodo.
Jason Creighton

2
questa critica non si applica più poiché Java 8 ha introdotto una migliore inferenza del tipo di destinazione
oldrinb

4

L'introduzione dei generici in Java è stato un compito difficile perché gli architetti stavano cercando di bilanciare funzionalità, facilità d'uso e compatibilità con il codice legacy. Abbastanza prevedibilmente, è stato necessario scendere a compromessi.

Alcuni ritengono inoltre che l'implementazione di Java dei generici abbia aumentato la complessità del linguaggio a un livello inaccettabile (vedere " Generics Considered Harmful " di Ken Arnold ). Le domande frequenti sui generici di Angelika Langer danno un'idea abbastanza chiara di quanto possano diventare complicate le cose.


3

Java non applica i generics in fase di esecuzione, ma solo in fase di compilazione.

Ciò significa che puoi fare cose interessanti come aggiungere i tipi sbagliati a raccolte generiche.


1
Il compilatore ti dirà che hai sbagliato se lo gestisci.
Tom Hawtin - tackline

@ Tom - non necessariamente. Ci sono modi per ingannare il compilatore.
Paul Tomblin,

2
Non ascolti quello che ti dice il compilatore ?? (vedi il mio primo commento)
Tom Hawtin - tackline

1
@ Tom, se si dispone di un ArrayList <String> e lo si passa a un metodo vecchio stile (ad esempio, codice legacy o di terze parti) che accetta un semplice List, quel metodo può quindi aggiungere qualsiasi cosa gli piaccia a tale List. Se applicato in fase di esecuzione, si otterrebbe un'eccezione.
Paul Tomblin,

1
static void addToList (List list) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (lista); Questo compila e funziona anche in fase di esecuzione. L'unica volta che vedi un problema è quando rimuovi dalla lista aspettandoti una stringa e ottieni un int.
Laplie Anderson

3

I generici Java sono solo in fase di compilazione e vengono compilati in codice non generico. In C #, l'effettivo MSIL compilato è generico. Ciò ha enormi implicazioni per le prestazioni perché Java esegue ancora il cast durante il runtime. Vedi qui per ulteriori informazioni .


2

Se ascolti Java Posse # 279 - Intervista con Joe Darcy e Alex Buckley , parlano di questo problema. Questo si collega anche a un post sul blog di Neal Gafter intitolato Reified Generics per Java che dice:

Molte persone non sono soddisfatte delle restrizioni causate dal modo in cui i generici vengono implementati in Java. In particolare, non sono contenti che i parametri di tipo generico non vengano reificati: non sono disponibili in fase di esecuzione. I generici vengono implementati utilizzando la cancellazione, in cui i parametri di tipo generico vengono semplicemente rimossi in fase di esecuzione.

Quel post del blog fa riferimento a una voce precedente, Puzzling Through Erasure: answer section , che sottolineava il punto sulla compatibilità della migrazione nei requisiti.

L'obiettivo era fornire la retrocompatibilità del codice sorgente e dell'oggetto e anche la compatibilità della migrazione.

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.