2014-09-12 4 views
13

Ich habe einige Codes, der so etwas wie diese (Teil eines negativen Test des Verfahrens get) aussieht:Keine Classcast beim Gießen zu generischem Typ unterschiedlich tatsächliche Klasse

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); 
    } 
} 

ich es erwarte ein ClassCastException zu werfen aber es läuft durch erfolgreich drucken:

Test: value 

Warum wirft es nicht?

+0

Wie wird der Aufruf 'System.out.println' gedruckt? – mariosangiorgio

+1

@mariosangiorgio: Es wird 'Test: Wert' ausgedruckt. – Dici

+0

Ich habe das Gefühl, es ist mit dem gleichen Grund verbunden, warum Sie einen primitiven Typ für ein generisches Argument nicht übergeben können. Haben Sie versucht, die Werte in der 'Map' von' Object' in etwas anderes zu ändern, zB 'String'? Vielleicht ist es wegen der polymorphen Eigenschaft von 'Object' erlaubt –

Antwort

4

Es ist lehrreich, zu überlegen, was die Klasse sieht aus wie nach Typ Parameter Entfernung (Typ Löschung):

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); 
    } 
} 

Dies kompiliert und erzeugt das gleiche Ergebnis, die Sie sehen.

Der schwierige Teil ist diese Zeile:

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

Wenn Sie hatte dies getan:

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

dann nach Typ Löschung der Compiler eine Besetzung (weil nach Typ Löschung eingefügt haben würde test.get() kehrt Object):

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

So analog, die Compi ler konnte eine Besetzung in der obigen Zeile auch, wie diese eingefügt hat:

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

jedoch dafür, es nicht eine Besetzung, weil die Besetzung notwendig ist, nicht einfügen nicht da String-Verkettung zu kompilieren und verhalten sich korrekt, (+) funktioniert auf alle Objekte auf die gleiche Weise; es muss nur wissen, dass der Typ Object ist, keine spezifische Unterklasse.Daher kann der Compiler eine unnötige Umwandlung auslassen und tut dies in diesem Fall.

17

Das liegt daran, dass Sie auf den generischen Typ T verweisen, der zur Laufzeit wie alle Java-Generics gelöscht wird. Was zur Laufzeit tatsächlich passiert, ist, dass Sie an Object und nicht an Double senden.

Beachten Sie, dass, zum Beispiel, wenn T als <T extends Number> definiert wurde, mögen Sie Number Gießen (aber noch nicht zu Double).

Wenn Sie eine Laufzeitprüfung durchführen möchten, müssen Sie den tatsächlichen Parameter clazz (der zur Laufzeit verfügbar ist) und nicht den generischen Typ T (der gelöscht wird) verwenden. Zum Beispiel könnte man so etwas tun:

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

Das könnte nur die eine Zeile sein 'return clazz.cast (map.get (key));' –

+0

Du hast Recht - ich denke, ich hätte das Javadoc überprüfen sollen, da es nicht so ist Ich muss mich normalerweise damit befassen. Ich werde meine Antwort aktualisieren. –

+0

@LouisWasserman, Cyäegha: Beachten Sie, dass beide sich ein wenig dadurch unterscheiden, wie sie mit 'null' umgehen. 'class.cast (null)' funktioniert ohne Probleme, während 'null.getClass()' eine NullPointerException auslöst. –

2

Es scheint, dass Java ist nicht in der Lage die Besetzung mit einem abgeleiteten Typ zu verarbeiten, aber wenn Sie die Class.cast Methode verwenden, um der Anruf löst eine Ausnahme zu erhalten, wie erwartet:

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

Leider kann ich das nicht genauer erklären.

Bearbeiten: Sie könnten daran interessiert sein Oracle doc.

5

Sie erhalten keine ClassCastException, da der Kontext, in dem Sie den Wert aus der Zuordnung zurückgeben, den Compiler zu diesem Zeitpunkt keine Überprüfung durchführen muss, da es dem Wert einer Variablen vom Typ Object (Siehe rgettman's answer).

Der Aufruf:

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

ist Teil einer String-Verkettungsoperation unter Verwendung des + Operators. Das Objekt, das von Ihrer Map zurückgegeben wird, wird gerade behandelt, als wäre es ein Object. Um das zurückgegebene 'Objekt' als String anzuzeigen, ist ein Aufruf der toString() Methode erforderlich und da dies eine Methode ist, ist Object kein Cast erforderlich.

Wenn Sie den Aufruf an test.get("test", Double.class); außerhalb des Kontextes der String-Verkettung nehmen, werden Sie sehen, dass es nicht funktioniert, d.h.

Dies gilt nicht kompilieren:

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

Aber das tut:

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

es anders auszudrücken, Ihr Code:

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

entspricht:

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

oder:

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

fand ich einen Unterschied in dem Byte-Code, wenn ein Verfahren auf dem zurückgegebenen „Double“ und wenn keine Methode aufgerufen wird, aufgerufen wird.

Zum Beispiel, wenn Sie doubleValue() (oder sogar getClass()) auf der zurückgegebenen "Double" aufrufen, dann tritt die ClassCastException auftritt. Mit javap -c Test, erhalte ich die folgende 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; 

Der checkcast Betrieb der ClassCastException werfen werden müssen. Auch wäre implizit StringBuilder, append(double) genannt worden.

Ohne einen Aufruf doubleValue() (oder 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; 

Es gibt keinen checkcast Betrieb und append(Object) auf den impliziten StringBuilder genannt werden, weil nach Typ Löschung, T ist nur Object sowieso.