Java Class.cast () vs opérateur de cast

107

Ayant appris pendant mes jours C ++ sur les maux de l'opérateur de cast de style C, j'ai d'abord été heureux de constater que Java 5 java.lang.Classavait acquis une castméthode.

Je pensais que finalement nous avions une façon OO de gérer le casting.

Il s'avère que ce Class.castn'est pas la même chose qu'en static_castC ++. C'est plus comme reinterpret_cast. Il ne générera pas une erreur de compilation là où elle est attendue et se reportera à l'exécution. Voici un cas de test simple pour démontrer différents comportements.

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

Alors, ce sont mes questions.

  1. Devrait Class.cast()être banni sur le territoire des génériques? Là, il a plusieurs utilisations légitimes.
  2. Les compilateurs doivent-ils générer des erreurs de compilation quand Class.cast()est utilisé et que des conditions illégales peuvent être déterminées au moment de la compilation?
  3. Java devrait-il fournir un opérateur de cast comme une construction de langage similaire à C ++?
Alexander Pogrebnyak
la source
4
Réponse simple: (1) Où est "Generics land"? En quoi est-ce différent de la façon dont l'opérateur de distribution est utilisé maintenant? (2) Probablement. Mais dans 99% de tout le code Java jamais écrit, il est extrêmement improbable que quiconque l'utilise Class.cast()lorsque des conditions illégales peuvent être déterminées au moment de la compilation. Dans ce cas, tout le monde sauf vous utilise simplement l'opérateur de distribution standard. (3) Java a un opérateur de cast comme construction de langage. Ce n'est pas similaire au C ++. En effet, de nombreuses constructions de langage Java ne sont pas similaires à C ++. Malgré les similitudes superficielles, Java et C ++ sont assez différents.
Daniel Pryden

Réponses:

117

Je n'ai jamais utilisé Class.cast(Object)pour éviter les avertissements dans le "pays des génériques". Je vois souvent des méthodes faire des choses comme ceci:

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

Il est souvent préférable de le remplacer par:

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

C'est le seul cas d'utilisation que Class.cast(Object)je connaisse.

Concernant les avertissements du compilateur: je suppose que ce Class.cast(Object)n'est pas spécial pour le compilateur. Il pourrait être optimisé lorsqu'il est utilisé de manière statique (c'est-à-dire Foo.class.cast(o)plutôt que cls.cast(o)) mais je n'ai jamais vu personne l'utiliser - ce qui rend l'effort de construire cette optimisation dans le compilateur quelque peu inutile.

sfussenegger
la source
8
Je pense que la bonne façon dans ce cas serait de faire d'abord une instance de vérification comme ceci: if (cls.isInstance (o)) {return cls.cast (o); } Sauf si vous êtes sûr que le type sera correct, bien sûr.
Puce
1
Pourquoi la deuxième variante est-elle meilleure? La première variante n'est-elle pas plus efficace car le code de l'appelant ferait de toute façon une diffusion dynamique?
user1944408
1
@ user1944408 tant que vous parlez strictement de performances, cela peut être, bien qu'à peu près négligeable. Je n'aimerais pas l'idée d'obtenir des ClassCastExceptions là où il n'y a pas de cas évident. De plus, le second fonctionne mieux là où le compilateur ne peut pas déduire T, par exemple list.add (this. <String> doSomething ()) vs list.add (doSomething (String.class))
sfussenegger
2
Mais vous pouvez toujours obtenir des ClassCastExceptions lorsque vous appelez cls.cast (o) alors que vous n'obtenez aucun avertissement au moment de la compilation. Je préfère la première variante mais j'utiliserais la deuxième variante lorsque j'ai besoin par exemple de renvoyer null si je ne peux pas faire de casting. Dans ce cas, j'envelopperais cls.cast (o) dans un bloc try catch. Je suis également d'accord avec vous que c'est aussi mieux lorsque le compilateur ne peut pas en déduire T. Merci pour la réponse.
user1944408
1
J'utilise également la deuxième variante quand j'ai besoin que les cls de classe <T> soient dynamiques, par exemple si j'utilise la réflexion mais dans tous les autres cas je préfère le premier. Eh bien, je suppose que c'est juste un goût personnel.
user1944408
20

Tout d'abord, vous êtes fortement déconseillé de faire presque n'importe quel casting, vous devriez donc le limiter autant que possible! Vous perdez les avantages des fonctionnalités fortement typées de Java au moment de la compilation.

Dans tous les cas, Class.cast()doit être utilisé principalement lorsque vous récupérez le Classjeton par réflexion. C'est plus idiomatique d'écrire

MyObject myObject = (MyObject) object

plutôt que

MyObject myObject = MyObject.class.cast(object)

EDIT: Erreurs au moment de la compilation

Dans l'ensemble, Java n'effectue des vérifications de cast qu'au moment de l'exécution. Cependant, le compilateur peut émettre une erreur s'il peut prouver que de tels transtypages ne peuvent jamais réussir (par exemple, transtyper une classe en une autre classe qui n'est pas un supertype et convertir un type de classe final en classe / interface qui n'est pas dans sa hiérarchie de types). Ici, puisque Fooet qu'il Bary a des classes qui ne sont pas dans une autre hiérarchie, le casting ne peut jamais réussir.

notnoop
la source
Je suis tout à fait d'accord que les moulages doivent être utilisés avec parcimonie, c'est pourquoi avoir quelque chose qui peut être facilement recherché serait d'un grand avantage pour la refactorisation / maintenance du code. Mais le problème, c'est que même si à première vue Class.castsemblent correspondre à la facture, cela crée plus de problèmes que cela n'en résout.
Alexander Pogrebnyak
16

Il est toujours problématique et souvent trompeur d'essayer de traduire des constructions et des concepts entre les langues. Le casting ne fait pas exception. Surtout parce que Java est un langage dynamique et que C ++ est quelque peu différent.

Tout le casting en Java, peu importe comment vous le faites, est effectué au moment de l'exécution. Les informations de type sont conservées lors de l'exécution. C ++ est un peu plus un mélange. Vous pouvez convertir une structure en C ++ en une autre et il s'agit simplement d'une réinterprétation des octets qui représentent ces structures. Java ne fonctionne pas de cette façon.

Les génériques Java et C ++ sont également très différents. Ne vous inquiétez pas trop de la façon dont vous faites les choses C ++ en Java. Vous devez apprendre à faire les choses à la manière Java.

cletus
la source
Toutes les informations ne sont pas conservées lors de l'exécution. Comme vous pouvez le voir dans mon exemple, (Bar) foocela génère une erreur au moment de la compilation, mais Bar.class.cast(foo)pas. À mon avis, s'il est utilisé de cette manière, il le devrait.
Alexander Pogrebnyak
5
@Alexander Pogrebnyak: Ne fais pas ça alors! Bar.class.cast(foo)indique explicitement au compilateur que vous souhaitez effectuer le cast au moment de l'exécution. Si vous souhaitez vérifier à la compilation la validité de la distribution, votre seul choix est de faire la (Bar) foodistribution de style.
Daniel Pryden
Selon vous, quelle est la manière de procéder de la même manière? puisque java ne prend pas en charge l'héritage de plusieurs classes.
Yamur
13

Class.cast()est rarement utilisé dans le code Java. S'il est utilisé alors généralement avec des types qui ne sont connus qu'au moment de l'exécution (c'est-à-dire via leurs Classobjets respectifs et par un paramètre de type). Il n'est vraiment utile que dans le code qui utilise des génériques (c'est aussi la raison pour laquelle il n'a pas été introduit plus tôt).

Ce n'est pas similaire à reinterpret_cast, car il ne vous permettra pas de casser le système de types à l'exécution, pas plus qu'un cast normal (c'est-à-dire que vous pouvez casser les paramètres de type générique, mais ne pouvez pas casser les types "réels").

Les maux de l'opérateur de conversion de style C ne s'appliquent généralement pas à Java. Le code Java qui ressemble à un cast de style C est le plus similaire à un dynamic_cast<>()avec un type de référence en Java (rappelez-vous: Java a des informations de type d'exécution).

En général, la comparaison des opérateurs de conversion C ++ avec la conversion Java est assez difficile car en Java, vous ne pouvez convertir que des références et aucune conversion ne se produit aux objets (seules les valeurs primitives peuvent être converties à l'aide de cette syntaxe).

Joachim Sauer
la source
dynamic_cast<>()avec un type de référence.
Tom Hawtin - tackline
@Tom: est-ce que cette modification est correcte? Mon C ++ est très rouillé, j'ai dû en re-google beaucoup ;-)
Joachim Sauer
+1 pour: "Les maux de l'opérateur de conversion de style C ne s'appliquent généralement pas à Java." Plutôt vrai. J'étais sur le point de publier ces mots exacts en guise de commentaire sur la question.
Daniel Pryden
4

En général, l'opérateur de conversion est préféré à la méthode de conversion Class # car il est plus concis et peut être analysé par le compilateur pour cracher des problèmes flagrants avec le code.

La classe # cast prend la responsabilité de la vérification de type au moment de l'exécution plutôt que pendant la compilation.

Il y a certainement des cas d'utilisation pour Class # cast, en particulier lorsqu'il s'agit d'opérations réfléchissantes.

Depuis que lambda est arrivé à Java, j'aime personnellement utiliser Class # cast avec l'API collections / stream si je travaille avec des types abstraits, par exemple.

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
la source
3

C ++ et Java sont des langages différents.

L'opérateur de conversion de style Java C est beaucoup plus restreint que la version C / C ++. En fait, le cast Java est comme le dynamic_cast C ++ si l'objet que vous avez ne peut pas être converti dans la nouvelle classe, vous obtiendrez une exception à l'exécution (ou s'il y a suffisamment d'informations dans le code à la compilation). Ainsi, l'idée C ++ de ne pas utiliser de casts de type C n'est pas une bonne idée en Java

mmmmmm
la source
0

En plus de supprimer les avertissements de cast laids comme la plupart mentionnés, Class.cast est principalement utilisé avec le casting générique, car les informations génériques seront effacées au moment de l'exécution et comment chaque générique sera considéré comme Object, cela conduit à ne pas lancer une exception ClassCastException.

par exemple serviceLoder utilisez cette astuce lors de la création des objets, vérifiez S p = service.cast (c.newInstance ()); cela lèvera une exception de conversion de classe lorsque SP = (S) c.newInstance (); ne sera pas et peut afficher un avertissement `` Sécurité de type: transtypage non coché d'Objet en S '' .

-simplement, il vérifie que l'objet casté est une instance de la classe de conversion puis il utilisera l'opérateur de conversion pour convertir et masquer l'avertissement en le supprimant.

implémentation java pour la diffusion dynamique:

@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
la source
0

Personnellement, je l'ai déjà utilisé pour créer un convertisseur JSON en POJO. Dans le cas où le JSONObject traité avec la fonction contient un tableau ou des JSONObjects imbriqués (ce qui implique que les données ici ne sont pas de type primitif ou String), j'essaie d'appeler la méthode setter en utilisant class.cast()de cette manière:

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

Je ne sais pas si cela est extrêmement utile, mais comme nous l'avons déjà dit, la réflexion est l'un des très rares cas d'utilisation légitimes auxquels class.cast()je puisse penser, au moins vous avez un autre exemple maintenant.

Wep0n
la source