Java Class.cast () vs. Cast-Operator

107

Nachdem ich während meiner C ++ - Tage über die Übel des Cast-Operators im C-Stil unterrichtet worden war, stellte ich zunächst erfreut fest, dass Java 5 java.lang.Classeine castMethode erworben hatte.

Ich dachte, dass wir endlich eine OO-Art haben, mit Casting umzugehen.

Es stellt sich heraus, dass Class.castes nicht dasselbe ist wie static_castin C ++. Es ist eher so reinterpret_cast. Es wird kein Kompilierungsfehler generiert, wo er erwartet wird, sondern auf die Laufzeit verschoben. Hier ist ein einfacher Testfall, um verschiedene Verhaltensweisen zu demonstrieren.

package test;

import static org.junit.Assert.assertTrue;

import org.junit.Test;


public class TestCast
{
    static final class Foo
    {
    }

    static class Bar
    {
    }

    static final class BarSubclass
        extends Bar
    {
    }

    @Test
    public void test ( )
    {
        final Foo foo = new Foo( );
        final Bar bar = new Bar( );
        final BarSubclass bar_subclass = new BarSubclass( );

        {
            final Bar bar_ref = bar;
        }

        {
            // Compilation error
            final Bar bar_ref = foo;
        }
        {
            // Compilation error
            final Bar bar_ref = (Bar) foo;
        }

        try
        {
            // !!! Compiles fine, runtime exception
            Bar.class.cast( foo );
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }

        {
            final Bar bar_ref = bar_subclass;
        }

        try
        {
            // Compiles fine, runtime exception, equivalent of C++ dynamic_cast
            final BarSubclass bar_subclass_ref = (BarSubclass) bar;
        }
        catch ( final ClassCastException ex )
        {
            assertTrue( true );
        }
    }
}

Das sind also meine Fragen.

  1. Sollte Class.cast()nach Generics Land verbannt werden? Dort hat es einige legitime Verwendungszwecke.
  2. Sollten Compiler Kompilierungsfehler erzeugen, wenn sie Class.cast()verwendet werden und illegale Bedingungen zur Kompilierungszeit ermittelt werden können?
  3. Sollte Java einen Cast-Operator als Sprachkonstrukt ähnlich wie C ++ bereitstellen?
Alexander Pogrebnyak
quelle
4
Einfache Antwort: (1) Wo ist "Generics Land"? Wie unterscheidet sich das von der Verwendung des Cast-Operators? (2) Wahrscheinlich. In 99% des gesamten jemals geschriebenen Java-Codes ist es jedoch äußerst unwahrscheinlich, dass jemand ihn verwendet, Class.cast()wenn zum Zeitpunkt der Kompilierung illegale Bedingungen festgestellt werden können. In diesem Fall verwenden alle außer Ihnen nur den Standard-Cast-Operator. (3) Java hat einen Cast-Operator als Sprachkonstrukt. Es ist nicht ähnlich wie C ++. Dies liegt daran, dass viele der Java-Sprachkonstrukte C ++ nicht ähnlich sind. Trotz der oberflächlichen Ähnlichkeiten sind Java und C ++ sehr unterschiedlich.
Daniel Pryden

Antworten:

116

Ich habe immer nur Class.cast(Object)Warnungen im "Land der Generika" vermieden. Ich sehe oft Methoden, die solche Dinge tun:

@SuppressWarnings("unchecked")
<T> T doSomething() {
    Object o;
    // snip
    return (T) o;
}

Es ist oft am besten, es zu ersetzen durch:

<T> T doSomething(Class<T> cls) {
    Object o;
    // snip
    return cls.cast(o);
}

Das ist der einzige Anwendungsfall, auf den Class.cast(Object)ich jemals gestoßen bin.

In Bezug auf Compiler-Warnungen: Ich vermute, dass dies Class.cast(Object)nichts Besonderes für den Compiler ist. Es könnte optimiert werden, wenn es statisch verwendet wird (dh Foo.class.cast(o)nicht cls.cast(o)), aber ich habe noch nie jemanden gesehen, der es verwendet - was den Aufwand, diese Optimierung in den Compiler zu integrieren, etwas wertlos macht.

sfussenegger
quelle
8
Ich denke, der richtige Weg in diesem Fall wäre, zuerst eine Instanz der Prüfung wie folgt durchzuführen: if (cls.isInstance (o)) {return cls.cast (o); } Außer wenn Sie sicher sind, dass der Typ korrekt ist, natürlich.
Puce
1
Warum ist die zweite Variante besser? Ist die erste Variante nicht effizienter, weil der Code des Anrufers sowieso eine dynamische Umwandlung durchführen würde?
user1944408
1
@ user1944408 Solange Sie streng über die Leistung sprechen, kann es sein, obwohl ziemlich vernachlässigbar. Ich würde die Idee, ClassCastExceptions zu bekommen, nicht mögen, wenn es keinen offensichtlichen Fall gibt. Außerdem funktioniert der zweite besser, wenn der Compiler T nicht ableiten kann, z. B. list.add (this. <String> doSomething ()) vs. list.add (doSomething (String.class))
sfussenegger
2
Sie können jedoch weiterhin ClassCastExceptions erhalten, wenn Sie cls.cast (o) aufrufen, während Sie während der Kompilierungszeit keine Warnungen erhalten. Ich bevorzuge die erste Variante, aber ich würde die zweite Variante verwenden, wenn ich zum Beispiel null zurückgeben muss, wenn ich kein Casting durchführen kann. In diesem Fall würde ich cls.cast (o) in einen try catch-Block einschließen. Ich stimme Ihnen auch zu, dass dies auch besser ist, wenn der Compiler nicht auf T schließen kann. Vielen Dank für die Antwort.
user1944408
1
Ich verwende auch die zweite Variante, wenn Class <T> cls dynamisch sein soll, zum Beispiel wenn ich Reflektion verwende, aber in allen anderen Fällen bevorzuge ich die erste. Nun, ich denke, das ist nur ein persönlicher Geschmack.
user1944408
20

Erstens wird dringend davon abgeraten, fast jede Besetzung durchzuführen, daher sollten Sie diese so weit wie möglich einschränken! Sie verlieren die Vorteile der stark typisierten Funktionen von Java zur Kompilierungszeit.

In jedem Fall Class.cast()sollte hauptsächlich verwendet werden, wenn Sie das ClassToken durch Reflektion abrufen . Es ist idiomatischer zu schreiben

MyObject myObject = (MyObject) object

eher, als

MyObject myObject = MyObject.class.cast(object)

BEARBEITEN: Fehler beim Kompilieren

Insgesamt führt Java Cast-Checks nur zur Laufzeit durch. Der Compiler kann jedoch einen Fehler ausgeben, wenn er nachweisen kann, dass solche Casts niemals erfolgreich sein können (z. B. eine Klasse in eine andere Klasse umwandeln, die kein Supertyp ist, und einen endgültigen Klassentyp in eine Klasse / Schnittstelle umwandeln, die nicht in ihrer Typhierarchie enthalten ist). Hier , da Foound Barsind Klassen , die nicht in jeder anderen Hierarchie sind, kann die Besetzung nie gelingen.

notnoop
quelle
Ich stimme voll und ganz zu, dass Casts sparsam verwendet werden sollten. Deshalb wäre es für das Refactoring / die Pflege von Code von großem Vorteil, wenn etwas leicht durchsucht werden kann. Das Problem ist jedoch, dass es auf den ersten Blick Class.castmehr Probleme zu bereiten scheint, als es zu lösen scheint.
Alexander Pogrebnyak
16

Es ist immer problematisch und oft irreführend, Konstrukte und Konzepte zwischen Sprachen zu übersetzen. Casting ist keine Ausnahme. Insbesondere, weil Java eine dynamische Sprache ist und C ++ etwas anders ist.

Alle Castings in Java, egal wie Sie es machen, werden zur Laufzeit durchgeführt. Typinformationen werden zur Laufzeit gespeichert. C ++ ist eher eine Mischung. Sie können eine Struktur in C ++ in eine andere umwandeln und es handelt sich lediglich um eine Neuinterpretation der Bytes, die diese Strukturen darstellen. Java funktioniert so nicht.

Auch Generika in Java und C ++ sind sehr unterschiedlich. Kümmere dich nicht zu sehr darum, wie du C ++ in Java machst. Sie müssen lernen, wie man Dinge auf Java-Art macht.

Cletus
quelle
Nicht alle Informationen werden zur Laufzeit gespeichert. Wie Sie meinem Beispiel (Bar) fooentnehmen können, wird beim Kompilieren zwar ein Fehler generiert, dies jedoch Bar.class.cast(foo)nicht. Meiner Meinung nach sollte es so verwendet werden.
Alexander Pogrebnyak
5
@ Alexander Pogrebnyak: Dann mach das nicht! Bar.class.cast(foo)teilt dem Compiler explizit mit, dass Sie die Umwandlung zur Laufzeit durchführen möchten. Wenn Sie die Gültigkeit der Besetzung zur Kompilierungszeit überprüfen möchten, können Sie nur die (Bar) fooStilumwandlung durchführen.
Daniel Pryden
Was denkst du ist der Weg, um den gleichen Weg zu tun? da Java die Vererbung mehrerer Klassen nicht unterstützt.
Yamur
13

Class.cast()wird im Java-Code selten verwendet. Wenn es verwendet wird, dann normalerweise mit Typen, die nur zur Laufzeit bekannt sind (dh über ihre jeweiligen ClassObjekte und durch einen Typparameter). Es ist nur in Code wirklich nützlich, der Generika verwendet (das ist auch der Grund, warum es nicht früher eingeführt wurde).

Es ist nicht ähnlich wie reinterpret_cast, weil es Ihnen nicht erlaubt, das Typsystem zur Laufzeit mehr zu brechen als eine normale Besetzung (dh Sie können generische Typparameter brechen , aber keine "echten" Typen brechen ).

Die Übel des Cast-Operators im C-Stil gelten im Allgemeinen nicht für Java. Der Java-Code, der wie eine Besetzung im C-Stil aussieht, ähnelt am ehesten einem dynamic_cast<>()mit einem Referenztyp in Java (denken Sie daran: Java verfügt über Laufzeittypinformationen).

Im Allgemeinen ist der Vergleich der C ++ - Casting-Operatoren mit dem Java-Casting ziemlich schwierig, da Sie in Java immer nur Referenzen umwandeln können und keine Konvertierung von Objekten erfolgt (nur primitive Werte können mit dieser Syntax konvertiert werden).

Joachim Sauer
quelle
dynamic_cast<>()mit einem Referenztyp.
Tom Hawtin - Tackline
@ Tom: Ist die Bearbeitung korrekt? Mein C ++ ist sehr rostig, ich musste viel neu googeln ;-)
Joachim Sauer
+1 für: "Die Übel des Cast-Operators im C-Stil gelten im Allgemeinen nicht für Java." Ziemlich wahr. Ich wollte gerade diese genauen Worte als Kommentar zu der Frage veröffentlichen.
Daniel Pryden
4

Im Allgemeinen wird der Cast-Operator der Class # Cast-Methode vorgezogen, da er präziser ist und vom Compiler analysiert werden kann, um offensichtliche Probleme mit dem Code auszuspucken.

Class # cast übernimmt die Verantwortung für die Typprüfung zur Laufzeit und nicht während der Kompilierung.

Es gibt sicherlich Anwendungsfälle für Class # Cast, insbesondere wenn es um reflektierende Operationen geht.

Seit Lambda zu Java gekommen ist, verwende ich persönlich gerne Class # cast mit der Sammlungen / Stream-API, wenn ich zum Beispiel mit abstrakten Typen arbeite.

Dog findMyDog(String name, Breed breed) {
    return lostAnimals.stream()
                      .filter(Dog.class::isInstance)
                      .map(Dog.class::cast)
                      .filter(dog -> dog.getName().equalsIgnoreCase(name))
                      .filter(dog -> dog.getBreed() == breed)
                      .findFirst()
                      .orElse(null);
}
Caleb
quelle
3

C ++ und Java sind verschiedene Sprachen.

Der Cast-Operator im Java C-Stil ist wesentlich eingeschränkter als die C / C ++ - Version. Tatsächlich ähnelt die Java-Umwandlung der C ++ - Dynamic_cast. Wenn das Objekt, das Sie haben, nicht in die neue Klasse umgewandelt werden kann, wird eine Laufzeitausnahme angezeigt (oder wenn im Code genügend Informationen zur Kompilierungszeit vorhanden sind). Daher ist die C ++ - Idee, keine Casts vom Typ C zu verwenden, in Java keine gute Idee

mmmmmm
quelle
0

Zusätzlich zum Entfernen hässlicher Cast-Warnungen, wie am häufigsten erwähnt, ist Class.cast ein Laufzeit-Cast, der hauptsächlich für generisches Casting verwendet wird. Generische Informationen werden zur Laufzeit gelöscht und einige, wie jedes generische Objekt betrachtet wird, führen dazu, dass dies nicht der Fall ist eine frühe ClassCastException auslösen.

Verwenden Sie diesen Trick beispielsweise beim Erstellen der Objekte. Überprüfen Sie S p = service.cast (c.newInstance ()). Dies löst eine Klassenumwandlungsausnahme aus, wenn SP = (S) c.newInstance (); wird und wird möglicherweise keine Warnung ' Typensicherheit: Deaktivierte Umwandlung von Objekt nach S' anzeigen (wie Objekt P = (Objekt) c.newInstance ();)

- Es wird einfach überprüft, ob das gegossene Objekt eine Instanz der Besetzungsklasse ist, und es wird der Besetzungsoperator verwendet, um die Warnung durch Unterdrücken umzuwandeln und auszublenden.

Java-Implementierung für dynamische Besetzung:

@SuppressWarnings("unchecked")
public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
        throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
}




    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
Amjad Abdul-Ghani
quelle
0

Persönlich habe ich dies bereits verwendet, um einen JSON-POJO-Konverter zu erstellen. Für den Fall, dass das mit der Funktion verarbeitete JSONObject ein Array oder verschachtelte JSONObjects enthält (was bedeutet, dass die Daten hier nicht vom primitiven Typ sind oder String), versuche ich, die Setter-Methode folgendermaßen aufzurufen class.cast():

public static Object convertResponse(Class<?> clazz, JSONObject readResultObject) {
    ...
    for(Method m : clazz.getMethods()) {
        if(!m.isAnnotationPresent(convertResultIgnore.class) && 
            m.getName().toLowerCase().startsWith("set")) {
        ...
        m.invoke(returnObject,  m.getParameters()[0].getClass().cast(convertResponse(m.getParameters()[0].getType(), readResultObject.getJSONObject(key))));
    }
    ...
}

class.cast()Ich bin mir nicht sicher, ob dies äußerst hilfreich ist, aber wie hier bereits erwähnt, ist Reflexion einer der wenigen legitimen Anwendungsfälle, an die ich denken kann. Zumindest haben Sie jetzt ein anderes Beispiel.

Wep0n
quelle