O cast de Java apresenta sobrecarga? Por quê?

104

Existe alguma sobrecarga quando lançamos objetos de um tipo para outro? Ou o compilador simplesmente resolve tudo e não há custo em tempo de execução?

São coisas gerais ou há casos diferentes?

Por exemplo, suponha que temos um array de Object [], onde cada elemento pode ter um tipo diferente. Mas sempre sabemos com certeza que, digamos, o elemento 0 é um Double, o elemento 1 é uma String. (Eu sei que este é um design errado, mas vamos supor que tive que fazer isso.)

As informações de tipo do Java ainda são mantidas em tempo de execução? Ou tudo é esquecido após a compilação, e se fizermos (Double) elementos [0], vamos apenas seguir o ponteiro e interpretar esses 8 bytes como um double, o que quer que seja?

Não estou muito certo sobre como os tipos são feitos em Java. Se você tem alguma recomendação sobre livros ou artigos, obrigado também.

Phil
fonte
O desempenho de instanceof e casting é muito bom. Postei algum tempo em Java7 em torno de diferentes abordagens para o problema aqui: stackoverflow.com/questions/16320014/…
Wheezil
Esta outra pergunta tem respostas muito boas stackoverflow.com/questions/16741323/…
user454322

Respostas:

78

Existem 2 tipos de fundição:

Cast implícito , quando você converte de um tipo para um tipo mais amplo, que é feito automaticamente e não há sobrecarga:

String s = "Cast";
Object o = s; // implicit casting

Casting explícito , quando você passa de um tipo mais largo para um mais estreito. Para este caso, você deve usar explicitamente a transmissão como esta:

Object o = someObject;
String s = (String) o; // explicit casting

Nesse segundo caso, há sobrecarga no tempo de execução, pois os dois tipos devem ser verificados e, caso a conversão não seja viável, a JVM deve lançar uma ClassCastException.

Retirado de JavaWorld: O custo de fundição

Casting é usado para converter entre tipos - entre tipos de referência em particular, para o tipo de operação de casting no qual estamos interessados ​​aqui.

As operações de upcast (também chamadas de conversões de ampliação na Especificação da linguagem Java) convertem uma referência de subclasse em uma referência de classe ancestral. Esta operação de casting é normalmente automática, pois é sempre segura e pode ser implementada diretamente pelo compilador.

As operações de downcast (também chamadas de conversões de estreitamento na Especificação da linguagem Java) convertem uma referência de classe ancestral em uma referência de subclasse. Essa operação de conversão cria sobrecarga de execução, uma vez que Java requer que a conversão seja verificada no tempo de execução para ter certeza de que é válida. Se o objeto referenciado não for uma instância do tipo de destino para a conversão ou uma subclasse desse tipo, a tentativa de conversão não é permitida e deve lançar um java.lang.ClassCastException.

Alex Ntousias
fonte
101
Esse artigo JavaWorld tem mais de 10 anos, então eu consideraria qualquer declaração que ele faça sobre desempenho com um grão muito grande do seu melhor sal.
Skaffman
1
@skaffman, Na verdade, eu consideraria qualquer afirmação que ele fizesse (independentemente de desempenho ou não) com um grão de sal.
Pacerier de
será o mesmo caso, se eu não atribuir o objeto fundido à referência e apenas chamar o método nele? como((String)o).someMethodOfCastedClass()
Parth Vishvajit
3
Agora o artigo tem quase 20 anos. E as respostas também têm muitos anos. Esta pergunta precisa de uma resposta moderna.
Raslanove
Que tal tipos primitivos? Quero dizer, por exemplo - a conversão de int para curto causa sobrecarga semelhante?
luke1985
44

Para uma implementação razoável de Java:

Cada objeto possui um cabeçalho contendo, entre outras coisas, um ponteiro para o tipo de tempo de execução (por exemplo Doubleou String, mas nunca poderia ser CharSequenceou AbstractList). Assumindo que o compilador de tempo de execução (geralmente HotSpot no caso da Sun) não pode determinar o tipo estaticamente, algumas verificações precisam ser realizadas pelo código de máquina gerado.

Primeiro, esse ponteiro para o tipo de tempo de execução precisa ser lido. De qualquer forma, isso é necessário para chamar um método virtual em uma situação semelhante.

Para lançar para um tipo de classe, sabe-se exatamente quantas superclasses existem até você atingir java.lang.Object, então o tipo pode ser lido em um deslocamento constante do ponteiro de tipo (na verdade, os primeiros oito no HotSpot). Novamente, isso é análogo a ler um ponteiro de método para um método virtual.

Então, o valor lido só precisa de uma comparação com o tipo estático esperado do elenco. Dependendo da arquitetura do conjunto de instruções, outra instrução precisará ramificar (ou falhar) em uma ramificação incorreta. ISAs, como o ARM de 32 bits, têm instrução condicional e podem fazer com que o caminho triste passe pelo caminho feliz.

As interfaces são mais difíceis devido à herança múltipla da interface. Geralmente, as duas últimas conversões para interfaces são armazenadas em cache no tipo de tempo de execução. Nos primeiros dias (mais de uma década atrás), as interfaces eram um pouco lentas, mas isso não é mais relevante.

Esperançosamente, você pode ver que esse tipo de coisa é irrelevante para o desempenho. Seu código-fonte é mais importante. Em termos de desempenho, o maior acerto em seu cenário é passível de perdas de cache por perseguir ponteiros de objeto em todo o lugar (as informações de tipo serão, obviamente, comuns).

Tom Hawtin - tackline
fonte
1
interessante - isso significa que, para classes sem interface, se eu escrever Superclass sc = (Superclass) subclass; que o compilador (jit ie: tempo de carregamento) irá "estaticamente" colocar o deslocamento de Object em cada uma das Superclasses e Subclasses em seus cabeçalhos de "Class" e então através de um simples add + compare será capaz de resolver as coisas? - isso é bom e rápido :) Para interfaces, eu não assumiria pior do que uma pequena hashtable ou btree?
peterk
@peterk Para lançar entre classes, tanto o endereço do objeto quanto o "vtbl" (tabela de ponteiros de método, mais tabela de hierarquia de classes, cache de interface, etc) não mudam. Portanto, o elenco [classe] verifica o tipo e, se ele se ajustar, nada mais precisa acontecer.
Tom Hawtin - tackline
8

Por exemplo, suponha que temos um array de Object [], onde cada elemento pode ter um tipo diferente. Mas sempre sabemos com certeza que, digamos, o elemento 0 é um Double, o elemento 1 é uma String. (Eu sei que este é um design errado, mas vamos supor que tive que fazer isso.)

O compilador não observa os tipos dos elementos individuais de uma matriz. Ele simplesmente verifica se o tipo de cada expressão de elemento pode ser atribuído ao tipo de elemento da matriz.

As informações de tipo do Java ainda são mantidas em tempo de execução? Ou tudo é esquecido após a compilação, e se fizermos (Double) elementos [0], vamos apenas seguir o ponteiro e interpretar esses 8 bytes como um double, o que quer que seja?

Algumas informações são mantidas em tempo de execução, mas não os tipos estáticos dos elementos individuais. Você pode dizer isso observando o formato do arquivo da classe.

É teoricamente possível que o compilador JIT possa usar "análise de escape" para eliminar verificações de tipo desnecessárias em algumas atribuições. No entanto, fazer isso na medida em que você está sugerindo estaria além dos limites da otimização realista. A recompensa de analisar os tipos de elementos individuais seria muito pequena.

Além disso, as pessoas não deveriam escrever código de aplicativo assim de qualquer maneira.

Stephen C
fonte
1
E quanto aos primitivos? (float) Math.toDegrees(theta)Haverá uma sobrecarga significativa aqui também?
SD
2
Há uma sobrecarga para alguns moldes primitivos. Se é significativo depende do contexto.
Stephen C de
6

A instrução de código de byte para executar a conversão em tempo de execução é chamada checkcast. Você pode desmontar o código Java usando javappara ver quais instruções são geradas.

Para arrays, o Java mantém as informações de tipo em tempo de execução. Na maioria das vezes, o compilador detectará erros de tipo para você, mas há casos em que você encontrará um ArrayStoreExceptionao tentar armazenar um objeto em uma matriz, mas o tipo não corresponde (e o compilador não o detectou) . A especificação da linguagem Java fornece o seguinte exemplo:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpaé válido porque ColoredPointé uma subclasse de Point, mas pa[0] = new Point()não é válido.

Isso se opõe aos tipos genéricos, onde não há informações de tipo mantidas em tempo de execução. O compilador insere checkcastinstruções onde necessário.

Essa diferença na digitação de tipos e arrays genéricos torna frequentemente inadequado misturar arrays e tipos genéricos.

JesperE
fonte
2

Em teoria, há uma sobrecarga introduzida. No entanto, as JVMs modernas são inteligentes. Cada implementação é diferente, mas não é irracional supor que poderia haver uma implementação que o JIT otimizou as verificações de conversão quando poderia garantir que nunca haveria um conflito. Quanto a quais JVMs específicos oferecem isso, não sei dizer. Devo admitir que gostaria de saber os detalhes da otimização JIT, mas isso é uma preocupação dos engenheiros de JVM.

A moral da história é escrever um código compreensível primeiro. Se você estiver enfrentando lentidão, analise e identifique o seu problema. As chances são boas de que não seja devido ao elenco. Nunca sacrifique um código limpo e seguro na tentativa de otimizá-lo ATÉ QUE VOCÊ SABE QUE PRECISA.

HesNotTheStig
fonte