Il metodo ha la stessa cancellazione di un altro metodo nel tipo


381

Perché non è legale avere i seguenti due metodi nella stessa classe?

class Test{
   void add(Set<Integer> ii){}
   void add(Set<String> ss){}
}

Ottengo il compilation error

Il metodo add (Set) ha la stessa cancellazione (Set) di un altro metodo nel tipo Test.

mentre ci posso aggirare, mi chiedevo perché a javac non piacesse.

Vedo che in molti casi la logica di questi due metodi sarebbe molto simile e potrebbe essere sostituita da una singola

public void add(Set<?> set){}

metodo, ma non è sempre così.

Questo è molto fastidioso se vuoi averne due constructorsche accettano questi argomenti perché non puoi semplicemente cambiare il nome di uno dei constructors.


1
cosa succede se si esauriscono le strutture dati e sono ancora necessarie più versioni?
Omry Yadan,

1
È possibile creare classi personalizzate che ereditano dalle versioni di base.
willlma,

1
OP, hai trovato una soluzione al problema del costruttore? Devo accettare due tipi di Liste non so come gestirlo.
Tomáš Zato - Ripristina Monica il

9
Quando lavoro con Java, mi manca molto C # ...
Svish,

1
@ TomášZato, l'ho risolto aggiungendo parametri fittizi al costruttore: Boolean noopSignatureOverload.
Nthalk,

Risposte:


357

Questa regola ha lo scopo di evitare conflitti nel codice legacy che utilizza ancora tipi non elaborati.

Ecco un esempio del perché ciò non era consentito, tratto dal JLS. Supponiamo, prima che i generici fossero introdotti a Java, ho scritto un codice come questo:

class CollectionConverter {
  List toList(Collection c) {...}
}

Estendi la mia classe, in questo modo:

class Overrider extends CollectionConverter{
  List toList(Collection c) {...}
}

Dopo l'introduzione dei farmaci generici, ho deciso di aggiornare la mia biblioteca.

class CollectionConverter {
  <T> List<T> toList(Collection<T> c) {...}
}

Non sei pronto per effettuare aggiornamenti, quindi lasci la tua Overriderclasse da solo. Per sostituire correttamente iltoList() metodo, i progettisti del linguaggio hanno deciso che un tipo grezzo era "equivalente a sostituzione" di qualsiasi tipo generato. Ciò significa che sebbene la firma del tuo metodo non sia più formalmente uguale alla firma della mia superclasse, il tuo metodo ha ancora la precedenza.

Ora passa il tempo e decidi di essere pronto per aggiornare la tua classe. Ma fai un po 'di casino e invece di modificare il toList()metodo raw esistente, aggiungi un nuovo metodo come questo:

class Overrider extends CollectionConverter {
  @Override
  List toList(Collection c) {...}
  @Override
  <T> List<T> toList(Collection<T> c) {...}
}

A causa dell'equivalenza di sostituzione dei tipi non elaborati, entrambi i metodi sono in una forma valida per ignorare il toList(Collection<T>)metodo. Ma ovviamente, il compilatore deve risolvere un singolo metodo. Per eliminare questa ambiguità, alle classi non è consentito disporre di più metodi che sono equivalenti a override, ovvero più metodi con gli stessi tipi di parametri dopo la cancellazione.

La chiave è che questa è una regola del linguaggio progettata per mantenere la compatibilità con il vecchio codice utilizzando tipi non elaborati. Non è una limitazione richiesta dalla cancellazione dei parametri di tipo; poiché la risoluzione del metodo si verifica in fase di compilazione, l'aggiunta di tipi generici all'identificatore del metodo sarebbe stata sufficiente.


3
Ottima risposta ed esempio! Non sono sicuro, tuttavia, se comprendo appieno la tua ultima frase ("Poiché la risoluzione del metodo si verifica in fase di compilazione, prima della cancellazione, non è necessaria la reificazione del tipo per farlo funzionare"). Potresti elaborare un po '?
Jonas Eicher,

2
Ha senso. Ho appena trascorso un po 'di tempo a pensare alla reificazione del tipo nei metodi template, ma sì: il compilatore si assicura che venga selezionato il metodo giusto prima della cancellazione del tipo. Bellissimo. Se non è stato contaminato dai problemi di compatibilità del codice legacy.
Jonas Eicher,

1
@daveloyall No, non sono a conoscenza di tale opzione per javac.
Erickson,

13
Non la prima volta che riscontro un errore Java che non è affatto un errore e potrebbe essere compilato se solo gli autori di Java usassero gli avvisi come fanno tutti gli altri. Solo loro pensano di sapere tutto meglio.
Tomáš Zato - Ripristina Monica il

1
@ TomášZato No, non conosco soluzioni alternative pulite per questo. Se vuoi qualcosa di sporco, potresti passare il List<?>e alcuni enumche definisci per indicare il tipo. La logica specifica del tipo potrebbe effettivamente essere in un metodo sull'enum. In alternativa, potresti voler creare due classi diverse, anziché una classe con due costruttori diversi. La logica comune sarebbe in una superclasse o in un oggetto helper a cui delegano entrambi i tipi.
Erickson,

118

I generici Java utilizzano la cancellazione del tipo. Il bit tra parentesi angolari ( <Integer>e <String>) viene rimosso, quindi si finirebbe con due metodi che hanno una firma identica (la add(Set)vedi nell'errore). Ciò non è consentito perché il runtime non saprebbe quale utilizzare per ciascun caso.

Se Java ottiene mai generici reificati, allora potresti farlo, ma probabilmente è improbabile ora.


24
Mi dispiace ma la tua risposta (e le altre risposte) non spiega perché qui c'è un errore. La risoluzione del sovraccarico viene eseguita al momento della compilazione e il compilatore ha sicuramente le informazioni sul tipo necessarie per decidere quale metodo collegare per indirizzo o per qualunque metodo a cui si fa riferimento nel bytecode che ritengo non sia una firma. Penso anche che alcuni compilatori consentiranno di compilare questo.
Stilgar,

5
@Stilgar cosa impedisce di chiamare o ispezionare il metodo tramite la riflessione? L'elenco dei metodi restituiti da Class.getMethods () avrebbe due metodi identici, il che non avrebbe senso.
Adrian Mouat,

5
Le informazioni di riflessione possono / dovrebbero contenere i metadati necessari per lavorare con i generici. In caso contrario, come fa il compilatore Java a conoscere i metodi generici quando si importa una libreria già compilata?
Stilgar

4
Quindi il metodo getMethod deve essere riparato. Ad esempio, introdurre un sovraccarico che specifica il sovraccarico generico e fare in modo che il metodo originale restituisca solo la versione non generica, non restituendo alcun metodo anotato come generico. Naturalmente questo avrebbe dovuto essere fatto nella versione 1.5. Se lo fanno ora romperanno la retrocompatibilità del metodo. Attendo la mia affermazione che la cancellazione del tipo non impone questo comportamento. È l'implementazione che non ha ottenuto abbastanza lavoro probabilmente a causa delle risorse limitate.
Stilgar

1
Questa non è una risposta precisa, ma riassume rapidamente il problema in una finzione utile: le firme del metodo sono troppo simili, il compilatore potrebbe non dire la differenza e quindi otterrai "problemi di compilazione non risolti".
worc,

46

Questo perché Java Generics è implementato con Type Erasure .

I tuoi metodi verrebbero tradotti, al momento della compilazione, in qualcosa del tipo:

La risoluzione del metodo si verifica in fase di compilazione e non considera i parametri di tipo. ( vedi la risposta di erickson )

void add(Set ii);
void add(Set ss);

Entrambi i metodi hanno la stessa firma senza i parametri di tipo, quindi l'errore.


21

Il problema è che Set<Integer>e in Set<String>realtà sono trattati come Setda JVM. La selezione di un tipo per il Set (String o Integer nel tuo caso) è solo lo zucchero sintattico utilizzato dal compilatore. La JVM non riesce a distinguere tra Set<String>e Set<Integer>.


7
È vero che il runtime JVM non ha informazioni per distinguerle ognuna Set, ma poiché la risoluzione del metodo avviene in fase di compilazione, quando sono disponibili le informazioni necessarie, ciò non è rilevante. Il problema è che consentire questi sovraccarichi sarebbe in conflitto con l'indennità per i tipi grezzi, quindi sono stati resi illegali nella sintassi Java.
Erickson,

@erickson Anche quando il compilatore sa quale metodo chiamare, non può come nel bytecode, entrambi sembrano esattamente uguali. Dovresti cambiare il modo in cui viene specificata una chiamata di metodo, poiché (Ljava/util/Collection;)Ljava/util/List;non funziona. Potresti usare (Ljava/util/Collection<String>;)Ljava/util/List<String>;, ma questa è una modifica incompatibile e potresti incontrare problemi irrisolvibili in luoghi in cui tutto ciò che hai è un tipo cancellato. Probabilmente dovresti eliminare completamente la cancellazione, ma è piuttosto complicato.
maaartinus,

@maaartinus Sì, sono d'accordo che dovresti cambiare l'identificatore del metodo. Sto cercando di risolvere alcuni dei problemi irrisolvibili che li hanno portati a rinunciare a quel tentativo.
Erickson,

7

Definire un singolo metodo senza digitare like void add(Set ii){}

Puoi menzionare il tipo mentre chiami il metodo in base alla tua scelta. Funzionerà con qualsiasi tipo di set.


3

Potrebbe essere possibile che il compilatore traduca Set (intero) in Set (oggetto) nel codice byte java. In tal caso, Set (Integer) verrebbe utilizzato solo in fase di compilazione per il controllo della sintassi.


5
È tecnicamente solo il tipo grezzo Set. I generici non esistono nel codice byte, sono zucchero sintattico per il casting e offrono sicurezza di tipo in fase di compilazione.
Andrzej Doyle,

1

Mi sono imbattuto in questo quando ho provato a scrivere qualcosa del genere: Continuable<T> callAsync(Callable<T> code) {....} e Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} diventano per il compilatore le 2 definizioni di Continuable<> callAsync(Callable<> veryAsyncCode) {...}

La cancellazione del tipo significa letteralmente cancellare le informazioni sugli argomenti di tipo dai generici. Questo è MOLTO fastidioso, ma questa è una limitazione che sarà con Java per un po '. Per i costruttori non si può fare molto, ad esempio 2 nuove sottoclassi specializzate con parametri diversi nel costruttore. Oppure usa metodi di inizializzazione invece ... (costruttori virtuali?) Con nomi diversi ...

per metodi operativi simili, la ridenominazione sarebbe utile, ad esempio

class Test{
   void addIntegers(Set<Integer> ii){}
   void addStrings(Set<String> ss){}
}

O con alcuni nomi più descrittivi, di auto-documentazione per i casi Oyu, come addNamese addIndexeso tali.

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.