Java Class.cast () vs. operador cast

107

Tendo aprendido durante meus dias de C ++ sobre os males do operador de elenco de estilo C, fiquei satisfeito em primeiro lugar ao descobrir que em Java 5 java.lang.Classhavia adquirido um castmétodo.

Achei que finalmente tínhamos uma maneira OO de lidar com o elenco.

Acontece que Class.castnão é o mesmo que static_castem C ++. É mais parecido reinterpret_cast. Não irá gerar um erro de compilação onde é esperado e, em vez disso, irá adiar para o tempo de execução. Aqui está um caso de teste simples para demonstrar diferentes comportamentos.

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

Então, essas são minhas perguntas.

  1. Deve Class.cast()ser banido para terras de Genéricos? Lá, ele tem alguns usos legítimos.
  2. Os compiladores devem gerar erros de compilação quando Class.cast()são usados ​​e condições ilegais podem ser determinadas em tempo de compilação?
  3. O Java deve fornecer um operador de elenco como uma construção de linguagem semelhante ao C ++?
Alexander Pogrebnyak
fonte
4
Resposta simples: (1) Onde fica a "terra dos genéricos"? Como isso é diferente de como o operador de elenco é usado agora? (2) Provavelmente. Mas em 99% de todo o código Java já escrito, é extremamente improvável que alguém o use Class.cast()quando condições ilegais podem ser determinadas em tempo de compilação. Nesse caso, todos, exceto você, apenas usam o operador de elenco padrão. (3) Java tem um operador cast como uma construção de linguagem. Não é semelhante ao C ++. Isso ocorre porque muitas das construções da linguagem Java não são semelhantes a C ++. Apesar das semelhanças superficiais, Java e C ++ são bastante diferentes.
Daniel Pryden

Respostas:

116

Eu sempre usei Class.cast(Object)para evitar avisos na "terra dos genéricos". Costumo ver métodos fazendo coisas assim:

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

Geralmente, é melhor substituí-lo por:

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

Esse é o único caso de uso para Class.cast(Object) que eu já encontrei.

Com relação aos avisos do compilador: Suspeito que isso Class.cast(Object)não seja especial para o compilador. Ele poderia ser otimizado quando usado estaticamente (ou seja, em Foo.class.cast(o)vez de cls.cast(o)), mas nunca vi ninguém usando - o que torna o esforço de construir essa otimização no compilador um tanto inútil.

Sfussenegger
fonte
8
Acho que a maneira correta neste caso seria fazer uma instância de verificação primeiro como esta: if (cls.isInstance (o)) {return cls.cast (o); } Exceto se você tiver certeza de que o tipo estará correto, é claro.
Puce
1
Por que a segunda variante é melhor? A primeira variante não é mais eficiente porque o código do chamador faria conversão dinâmica de qualquer maneira?
user1944408
1
@ user1944408 contanto que você esteja estritamente falando sobre desempenho, pode ser, embora seja insignificante. Eu não gostaria da ideia de obter ClassCastExceptions onde não houvesse um caso óbvio. Além disso, o segundo funciona melhor quando o compilador não consegue inferir T, por exemplo, list.add (this. <String> doSomething ()) vs. list.add (doSomething (String.class))
sfussenegger
2
Mas você ainda pode obter ClassCastExceptions ao chamar cls.cast (o) enquanto não está recebendo nenhum aviso em tempo de compilação. Eu prefiro a primeira variante, mas usaria a segunda variante quando preciso, por exemplo, retornar nulo se não puder fazer o casting. Nesse caso, eu envolveria cls.cast (o) em um bloco try catch. Também concordo com você que isso também é melhor quando o compilador não pode inferir T. Obrigado pela resposta.
user1944408
1
Também uso a segunda variante quando preciso que a Classe <T> cls seja dinâmica, por exemplo, se uso reflexão, mas em todos os outros casos prefiro a primeira. Bem, acho que é apenas um gosto pessoal.
user1944408
20

Primeiro, você está fortemente desencorajado a fazer quase qualquer elenco, então você deve limitá-lo tanto quanto possível! Você perde os benefícios dos recursos fortemente tipados em tempo de compilação do Java.

Em qualquer caso, Class.cast()deve ser usado principalmente quando você recupera o Classtoken por meio de reflexão. É mais idiomático escrever

MyObject myObject = (MyObject) object

ao invés de

MyObject myObject = MyObject.class.cast(object)

EDIT: Erros em tempo de compilação

Acima de tudo, o Java realiza verificações de elenco apenas em tempo de execução. No entanto, o compilador pode emitir um erro se puder provar que tais casts nunca terão sucesso (por exemplo, lançar uma classe para outra classe que não seja um supertipo e lançar um tipo de classe final para classe / interface que não está em sua hierarquia de tipo). Aqui, uma vez que Fooe Barsão classes que não estão em uma outra hierarquia, o elenco nunca pode ter sucesso.

notnoop
fonte
Eu concordo totalmente que casts devem ser usados ​​com moderação, é por isso que ter algo que pode ser facilmente pesquisado seria um grande benefício para refatoração / manutenção de código. Mas o problema é que, embora à primeira vista Class.castpareça caber na conta, ele cria mais problemas do que resolve.
Alexander Pogrebnyak
16

É sempre problemático e muitas vezes enganoso tentar traduzir construções e conceitos entre idiomas. O elenco não é exceção. Particularmente porque Java é uma linguagem dinâmica e C ++ é um pouco diferente.

Todo o cast em Java, não importa como você o faça, é feito em tempo de execução. As informações de tipo são mantidas em tempo de execução. C ++ é um pouco mais que uma mistura. Você pode converter uma estrutura em C ++ para outra e é apenas uma reinterpretação dos bytes que representam essas estruturas. Java não funciona assim.

Além disso, os genéricos em Java e C ++ são muito diferentes. Não se preocupe excessivamente em como você faz as coisas em C ++ em Java. Você precisa aprender a fazer as coisas do jeito Java.

cletus
fonte
Nem todas as informações são mantidas em tempo de execução. Como você pode ver no meu exemplo (Bar) foo, gera um erro em tempo de compilação, mas Bar.class.cast(foo)não. Na minha opinião, se for usado dessa maneira, deveria.
Alexander Pogrebnyak
5
@Alexander Pogrebnyak: Não faça isso então! Bar.class.cast(foo)diz explicitamente ao compilador que você deseja fazer a conversão em tempo de execução. Se você quiser verificar em tempo de compilação a validade do elenco, sua única opção é fazer o (Bar) fooelenco de estilo.
Daniel Pryden
Qual você acha que é a maneira de fazer da mesma maneira? uma vez que o java não suporta herança múltipla de classes.
Yamur
13

Class.cast()raramente é usado em código Java. Se for usado, geralmente com tipos que só são conhecidos em tempo de execução (ou seja, por meio de seus respectivosClass objetos e por algum parâmetro de tipo). Só é realmente útil em código que usa genéricos (essa também é a razão pela qual não foi introduzido anteriormente).

É não semelhante a reinterpret_cast, porque ele vai não permitem que você quebrar o sistema de tipos em tempo de execução mais do que um elenco normal faz (ou seja, você pode quebrar parâmetros de tipo genérico, mas não pode quebrar tipos "reais").

Os males do operador de elenco C-style geralmente não se aplicam ao Java. O código Java que se parece com um elenco de estilo C é mais semelhante a um dynamic_cast<>()com um tipo de referência em Java (lembre-se: Java tem informações de tipo de tempo de execução).

Geralmente, comparar os operadores de conversão de C ++ com a conversão de Java é muito difícil, pois em Java você só pode lançar referência e nenhuma conversão acontece para objetos (apenas valores primitivos podem ser convertidos usando esta sintaxe).

Joachim Sauer
fonte
dynamic_cast<>()com um tipo de referência.
Tom Hawtin - tackline
@Tom: essa edição está correta? Meu C ++ está muito enferrujado, tive que refazer o google ;-)
Joachim Sauer
+1 para: "Os males do operador de elenco de estilo C geralmente não se aplicam a Java." Bem verdade. Eu estava prestes a postar essas palavras exatas como um comentário sobre a questão.
Daniel Pryden
4

Geralmente, o operador cast é preferível ao método cast Class #, pois é mais conciso e pode ser analisado pelo compilador para revelar problemas gritantes com o código.

Class # cast assume a responsabilidade pela verificação de tipo em tempo de execução, e não durante a compilação.

Certamente, há casos de uso para elenco de classe #, particularmente quando se trata de operações reflexivas.

Desde que lambda veio para java, eu pessoalmente gosto de usar Class # cast com a API Collections / stream se estou trabalhando com tipos abstratos, por exemplo.

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

C ++ e Java são linguagens diferentes.

O operador cast do Java C-style é muito mais restrito do que a versão C / C ++. Efetivamente, o cast de Java é como o C ++ dynamic_cast se o objeto que você possui não pode ser convertido para a nova classe, você obterá uma exceção de tempo de execução (ou se houver informações suficientes no código em um tempo de compilação). Assim, a ideia C ++ de não usar casts de tipo C não é uma boa ideia em Java

mmmmmm
fonte
0

Além de remover avisos de elenco feio como mais mencionado, Class.cast é elenco de tempo de execução usado principalmente com elenco genérico, devido às informações genéricas serão apagadas em tempo de execução e de alguma forma cada genérico será considerado Objeto, isso leva a não lance uma ClassCastException antecipada.

por exemplo, serviceLoder use este truque ao criar os objetos, verifique S p = service.cast (c.newInstance ()); isso lançará uma exceção de lançamento de classe quando SP = (S) c.newInstance (); não irá e pode mostrar um aviso 'Segurança de tipo: conversão não verificada de objeto em S' . (igual a Object P = (Object) c.newInstance ();)

-Simplesmente, ele verifica se o objeto cast é uma instância da classe de cast, então ele usará o operador cast para lançar e ocultar o aviso, suprimindo-o.

implementação java para elenco dinâmico:

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

Pessoalmente, já usei isso antes para construir um conversor JSON para POJO. No caso de o JSONObject processado com a função conter uma matriz ou JSONObjects aninhados (implicando que os dados aqui não são de um tipo primitivo ou String), tento invocar o método setter usando class.cast()desta forma:

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

Não tenho certeza se isso é extremamente útil, mas como disse aqui antes, reflexão é um dos poucos casos de uso legítimos class.cast()que posso pensar, pelo menos você tem outro exemplo agora.

Wep0n
fonte