Java Class.cast () frente al operador de conversión

107

Habiendo aprendido durante mis días en C ++ sobre los males del operador de conversión de estilo C, al principio me complació descubrir que en Java 5 java.lang.Classhabía adquirido un castmétodo.

Pensé que finalmente tenemos una forma OO de lidiar con el casting.

Resulta que Class.castno es lo mismo que static_casten C ++. Es más como reinterpret_cast. No generará un error de compilación donde se espera, sino que se aplazará hasta el tiempo de ejecución. Aquí hay un caso de prueba simple para demostrar diferentes comportamientos.

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

Entonces, estas son mis preguntas.

  1. ¿Debería Class.cast()ser desterrado a tierras genéricas? Allí tiene bastantes usos legítimos.
  2. ¿Deberían los compiladores generar errores de compilación cuando Class.cast()se utiliza y se pueden determinar condiciones ilegales en el momento de la compilación?
  3. ¿Debería Java proporcionar un operador de conversión como una construcción de lenguaje similar a C ++?
Alexander Pogrebnyak
fuente
4
Respuesta simple: (1) ¿Dónde está "tierra genérica"? ¿En qué se diferencia eso de cómo se usa ahora el operador de reparto? (2) Probablemente. Pero en el 99% de todo el código Java jamás escrito, es extremadamente improbable que alguien lo use Class.cast()cuando se pueden determinar condiciones ilegales en tiempo de compilación. En ese caso, todos, excepto usted, solo usan el operador de conversión estándar. (3) Java tiene un operador de conversión como construcción de lenguaje. No es similar a C ++. Esto se debe a que muchas de las construcciones del lenguaje Java no son similares a C ++. A pesar de las similitudes superficiales, Java y C ++ son bastante diferentes.
Daniel Pryden

Respuestas:

117

Solo he usado Class.cast(Object)para evitar advertencias en "tierras genéricas". A menudo veo métodos que hacen cosas como esta:

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

A menudo es mejor reemplazarlo por:

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

Ese es el único caso de uso con el que Class.cast(Object)me he encontrado.

Con respecto a las advertencias del compilador: sospecho que Class.cast(Object)no es especial para el compilador. Podría optimizarse cuando se usa estáticamente (es decir, en Foo.class.cast(o)lugar de cls.cast(o)) pero nunca he visto a nadie usarlo, lo que hace que el esfuerzo de construir esta optimización en el compilador sea algo inútil.

sfussenegger
fuente
8
Creo que la forma correcta en este caso sería hacer una instancia de verificación primero como esta: if (cls.isInstance (o)) {return cls.cast (o); } Excepto si está seguro de que el tipo será correcto, por supuesto.
Puce
1
¿Por qué es mejor la segunda variante? ¿No es la primera variante más eficiente porque el código de la persona que llama haría un lanzamiento dinámico de todos modos?
user1944408
1
@ user1944408 siempre que esté hablando estrictamente sobre el rendimiento, puede ser, aunque bastante insignificante. Sin embargo, no me gustaría la idea de obtener ClassCastExceptions donde no hay un caso obvio. Además, el segundo funciona mejor donde el compilador no puede inferir T, por ejemplo, list.add (this. <String> doSomething ()) vs.list.add (doSomething (String.class))
sfussenegger
2
Pero aún puede obtener ClassCastExceptions cuando llama a cls.cast (o) mientras no recibe ninguna advertencia en tiempo de compilación. Prefiero la primera variante, pero usaría la segunda variante cuando necesito, por ejemplo, devolver nulo si no puedo hacer casting. En ese caso, envolvería cls.cast (o) en un bloque try catch. También estoy de acuerdo con usted en que esto también es mejor cuando el compilador no puede inferir T. Gracias por la respuesta.
user1944408
1
También uso la segunda variante cuando necesito que los cls de Class <T> sean dinámicos, por ejemplo, si uso la reflexión pero en todos los demás casos prefiero la primera. Bueno, supongo que es solo un gusto personal.
user1944408
20

En primer lugar, se le desaconseja encarecidamente hacer casi cualquier yeso, por lo que debe limitarlo tanto como sea posible. Pierde los beneficios de las funciones fuertemente tipadas en tiempo de compilación de Java.

En cualquier caso, Class.cast()debe usarse principalmente cuando recupera el Classtoken mediante reflexión. Es más idiomático escribir

MyObject myObject = (MyObject) object

más bien que

MyObject myObject = MyObject.class.cast(object)

EDITAR: Errores en tiempo de compilación

Sobre todo, Java realiza comprobaciones de conversión solo en tiempo de ejecución. Sin embargo, el compilador puede emitir un error si puede probar que tales conversiones nunca pueden tener éxito (por ejemplo, lanzar una clase a otra clase que no es un supertipo y lanzar un tipo de clase final a la clase / interfaz que no está en su jerarquía de tipos). Aquí, dado que Fooy Barhay clases que no están en la jerarquía de las otras, el elenco nunca puede tener éxito.

notnoop
fuente
Estoy totalmente de acuerdo en que los casts deben usarse con moderación, es por eso que tener algo que se pueda buscar fácilmente sería de gran beneficio para la refactorización / mantenimiento del código. Pero el problema es que aunque a primera vista Class.castparece encajar en la factura, crea más problemas de los que resuelve.
Alexander Pogrebnyak
16

Siempre es problemático y a menudo engañoso intentar traducir construcciones y conceptos entre idiomas. El casting no es una excepción. Particularmente porque Java es un lenguaje dinámico y C ++ es algo diferente.

Toda la transmisión en Java, no importa cómo lo hagas, se realiza en tiempo de ejecución. La información de tipo se mantiene en tiempo de ejecución. C ++ es un poco más una mezcla. Puede convertir una estructura en C ++ a otra y es simplemente una reinterpretación de los bytes que representan esas estructuras. Java no funciona de esa manera.

Además, los genéricos en Java y C ++ son muy diferentes. No se preocupe demasiado por cómo hace las cosas de C ++ en Java. Necesita aprender a hacer las cosas a la manera de Java.

cletus
fuente
No toda la información se mantiene en tiempo de ejecución. Como puede ver en mi ejemplo (Bar) foo, genera un error en el momento de la compilación, pero Bar.class.cast(foo)no lo hace. En mi opinión, si se usa de esta manera, debería.
Alexander Pogrebnyak
5
@Alexander Pogrebnyak: ¡Entonces no hagas eso! Bar.class.cast(foo)le dice explícitamente al compilador que desea realizar la conversión en tiempo de ejecución. Si desea una verificación en tiempo de compilación de la validez del elenco, su única opción es hacer el (Bar) fooelenco de estilo.
Daniel Pryden
¿Cuál crees que es la forma de hacer lo mismo? ya que java no admite la herencia de clases múltiples.
Yamur
13

Class.cast()rara vez se usa en código Java. Si se usa, generalmente con tipos que solo se conocen en tiempo de ejecución (es decir, a través de sus respectivos Classobjetos y por algún parámetro de tipo). Solo es realmente útil en código que usa genéricos (esa es también la razón por la que no se introdujo antes).

Es no similar a reinterpret_cast, ya que no permitirá romper el sistema de tipos en tiempo de ejecución más que un reparto normal no (es decir, se puede romper parámetros de tipo genérico, pero no puede romper tipos "reales").

Los males del operador de conversión de estilo C generalmente no se aplican a Java. El código Java que se parece a una conversión de estilo C es más similar a un tipo dynamic_cast<>()con un tipo de referencia en Java (recuerde: Java tiene información de tipo de tiempo de ejecución).

En general, comparar los operadores de conversión de C ++ con la conversión de Java es bastante difícil, ya que en Java solo puede emitir referencias y no se produce ninguna conversión en los objetos (solo los valores primitivos se pueden convertir utilizando esta sintaxis).

Joachim Sauer
fuente
dynamic_cast<>()con un tipo de referencia.
Tom Hawtin - tackline el
@Tom: ¿esa edición es correcta? Mi C ++ está muy oxidado, tuve que volver a buscar en Google mucho ;-)
Joachim Sauer
+1 para: "Los males del operador de conversión de estilo C generalmente no se aplican a Java". Muy cierto. Estaba a punto de publicar esas palabras exactas como comentario sobre la pregunta.
Daniel Pryden
4

En general, se prefiere el operador de conversión al método de conversión de Clase #, ya que es más conciso y el compilador puede analizarlo para detectar problemas evidentes con el código.

Class # cast asume la responsabilidad de la verificación de tipos en tiempo de ejecución en lugar de durante la compilación.

Ciertamente, existen casos de uso para Class # cast, particularmente cuando se trata de operaciones reflectivas.

Desde que lambda llegó a Java, personalmente me gusta usar Class # cast con la API de colecciones / flujo si estoy trabajando con tipos abstractos, por ejemplo.

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
fuente
3

C ++ y Java son lenguajes diferentes.

El operador de conversión de estilo Java C es mucho más restringido que la versión C / C ++. Efectivamente, la transmisión de Java es como la transmisión dinámica de C ++ si el objeto que tiene no se puede convertir a la nueva clase, obtendrá una excepción de tiempo de ejecución (o si hay suficiente información en el código, un tiempo de compilación). Por lo tanto, la idea de C ++ de no usar conversiones de tipo C no es una buena idea en Java

mmmmmm
fuente
0

Además de eliminar las advertencias de transmisión fea como la mayoría de las menciones, Class.cast es una transmisión en tiempo de ejecución que se usa principalmente con una transmisión genérica, debido a que la información genérica se borrará en el tiempo de ejecución y de alguna manera cada genérico se considerará Objeto, esto lleva a no lanzar una ClassCastException temprana.

por ejemplo serviceLoder use este truco al crear los objetos, marque S p = service.cast (c.newInstance ()); esto arrojará una excepción de conversión de clase cuando SP = (S) c.newInstance (); no lo hará y puede mostrar una advertencia 'Tipo de seguridad: conversión sin marcar de Objeto a S' . (igual que Objeto P = (Objeto) c.newInstance ();)

-simplemente comprueba que el objeto emitido es una instancia de la clase de conversión, luego usará el operador de conversión para transmitir y ocultar la advertencia suprimiéndola.

Implementación de Java para transmisión dinámica:

@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
fuente
0

Personalmente, he usado esto antes para construir un convertidor de JSON a POJO. En el caso de que el JSONObject procesado con la función contenga una matriz o JSONObjects anidados (lo que implica que los datos aquí no son de un tipo primitivo o String), intento invocar el método setter usando class.cast()de esta manera:

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

No estoy seguro de si esto es extremadamente útil, pero como se dijo aquí antes, la reflexión es uno de los pocos casos de uso legítimos en los class.cast()que puedo pensar, al menos ahora tienes otro ejemplo.

Wep0n
fuente