2014-09-12 10 views
13

Ho qualche codice che assomigli a questo (parte di un test negativo del metodo get):No ClassCastException in fase di lancio di tipo generico diverso da classe reale

import java.util.*; 
public class Test { 
    Map<String, Object> map = new HashMap<>(); 
    public static void main (String ... args) { 
     Test test = new Test(); 
     test.put("test", "value"); // Store a String 
     System.out.println("Test: " + test.get("test", Double.class)); // Retrieve it as a Double 
    } 

    public <T> T get(String key, Class<T> clazz) { 
     return (T) map.get(key); 
    } 

    public void put(String key, Object value) { 
     map.put(key, value); 
    } 
} 

mi aspettavo di gettare un ClassCastException ma viene stampato con successo:

Test: value 

Perché non gettare?

+0

Cosa stampa la chiamata 'System.out.println'? – mariosangiorgio

+1

@mariosangiorgio: stampa 'Test: valore'. – Dici

+0

Ho la sensazione che sia collegato allo stesso motivo per cui non è possibile passare un tipo primitivo per un argomento di tipo generico. Hai provato a cambiare i valori in 'Map' da' Object' a qualcos'altro, come 'String'? Forse è permesso a causa della proprietà polimorfica di 'Object' –

risposta

4

È istruttivo considerare ciò che la classe si presenta come dopo la rimozione di parametri di tipo (cancellazione di tipo):

public class Test { 
    Map map = new HashMap(); 
    public static void main (String ... args) { 
     Test test = new Test(); 
     test.put("test", "value"); 
     System.out.println("Test: " + test.get("test", Double.class)); 
    } 

    public Object get(String key, Class clazz) { 
     return map.get(key); 
    } 

    public void put(String key, Object value) { 
     map.put(key, value); 
    } 
} 

Questo compila e produce lo stesso risultato che si vede.

La parte difficile è questa linea:

System.out.println("Test: " + test.get("test", Double.class)); 

Se avessi fatto questo:

Double foo = test.get("test", Double.class); 

poi, dopo la cancellazione di tipo il compilatore avrebbe inserito un cast (perché dopo la cancellazione di tipo test.get() rendimenti Object):

Double foo = (Double)test.get("test", Double.class); 

Quindi, analogamente, il compi ler avrebbe inserito un getto nella riga sopra troppo, così:

System.out.println("Test: " + (Double)test.get("test", Double.class)); 

Tuttavia, esso non inserisce un cast, perché il cast non è necessario che essa compilare e comportarsi correttamente, poiché concatenazione stringa (+) funziona su tutti gli oggetti allo stesso modo; deve solo sapere che il tipo è Object, non una sottoclasse specifica.Pertanto, il compilatore può omettere un cast non necessario e lo fa in questo caso.

17

Questo perché si esegue il casting sul tipo generico T, che viene cancellato in fase di runtime, come tutti i generici Java. Quindi, ciò che accade in fase di esecuzione, è che stai trasmettendo a Object e non a Double.

Si noti che, per esempio, se è stata definita come T<T extends Number>, si sarebbe casting per Number (ma ancora non Double).

Se si desidera eseguire un controllo del tipo di runtime, è necessario utilizzare il parametro effettivo clazz (che è disponibile in fase di esecuzione) e non il tipo generico T (che viene cancellato). Per esempio, si potrebbe fare qualcosa di simile:

public <T> T get(String key, Class<T> clazz) { 
    return clazz.cast(map.get(key)); 
} 
+1

Questo potrebbe essere solo la riga 'return clazz.cast (map.get (chiave));' –

+0

Hai ragione - Suppongo che avrei dovuto controllare il Javadoc, dal momento che non è il genere di cosa Di solito ho a che fare con. Aggiornerò la mia risposta –

+0

@LouisWasserman, Cyäegha: nota che entrambi differiscono un po 'per come gestiscono 'null'. 'class.cast (null)' funziona senza problemi, mentre 'null.getClass()' lancia una NullPointerException. –

2

Sembra che Java è in grado di elaborare il cast di un tipo derivato, se si utilizza il metodo Class.cast, la chiamata per ottenere genera un'eccezione come previsto:

public <T> T get(String key, Class<T> clazz) { 
    return clazz.cast(map.get(key)) ; 
} 

Purtroppo, non sono in grado di spiegarlo più a fondo.

Modifica: Questo potrebbe essere lo Oracle doc.

5

Non si ottiene un ClassCastException perché il contesto in cui si restituisce il valore dalla mappa non richiede al compilatore di eseguire un controllo a questo punto poiché equivale ad assegnare il valore a una variabile di tipo Object (Vedi rgettman's answer).

La chiamata a:

test.get("test", Double.class); 

fa parte di un'operazione di concatenazione di stringhe utilizzando l'operatore +. L'oggetto restituito dalla tua mappa viene trattato come se fosse un Object. Per visualizzare l'oggetto restituito come String è necessaria una chiamata al metodo toString() e poiché questo è un metodo su Object non è richiesto alcun cast.

Se si prende la chiamata a test.get("test", Double.class); al di fuori del contesto della concatenazione di stringhe, si vedrà che non funziona, ad es.

Questo non può essere compilato:

// can't assign a Double to a variable of type String... 
String val = test.get("test", Double.class); 

Ma questo fa:

String val = test.get("test", Double.class).toString(); 

Per dirla in altro modo, il tuo codice:

System.out.println("Test: " + test.get("test", Double.class)); 

è equivalente a:

Object obj = test.get("test", Double.class); 
System.out.println("Test: " + obj); 

o:

Object obj = test.get("test", Double.class); 
String value = obj.toString();  
System.out.println("Test: " + value); 
7

ho trovato una differenza nel codice di byte quando un metodo viene chiamato sulla restituita "doppio" e quando non viene chiamato il metodo.

Ad esempio, se si dovesse chiamare doubleValue() (o anche getClass()) sul "Double" restituito, si verifica ClassCastException. Utilizzando javap -c Test, ottengo il seguente bytecode:

34: ldc   #15     // class java/lang/Double 
36: invokevirtual #16 // Method get (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; 
39: checkcast  #15     // class java/lang/Double 
42: invokevirtual #17     // Method java/lang/Double.doubleValue:()D 
45: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder; 

L'operazione checkcast deve essere gettando il ClassCastException. Inoltre, nell'implicito StringBuilder, sarebbe stato chiamato append(double).

Senza una chiamata a doubleValue() (o getClass()):

34: ldc   #15     // class java/lang/Double 
36: invokevirtual #16 // Method get:(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; 
39: invokevirtual #17 // Method java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; 

Non c'è checkcast operazione, e append(Object) è chiamato sulla implicita StringBuilder, perché dopo la cancellazione di tipo, T è solo Object comunque.