Primitivas Java versus Objetos
1. Visão geral
Neste tutorial, mostramos os prós e contras do uso de tipos primitivos Java e suas contrapartes agrupadas.
2. Sistema de tipo Java
Java tem um sistema de tipo duplo que consiste em primitivas comoint,booleane tipos de referência comoInteger,Boolean. Todo tipo primitivo corresponde a um tipo de referência.
Cada objeto contém um único valor do tipo primitivo correspondente. Oswrapper classes are immutable (de modo que seu estado não pode mudar uma vez que o objeto é construído) e são finais (para que não possamos herdar deles).
Sob o capô, o Java realiza uma conversão entre os tipos primitivo e de referência, se um tipo real for diferente do declarado:
Integer j = 1; // autoboxing
int i = new Integer(1); // unboxing
O processo de conversão de um tipo primitivo em um de referência é chamado de autoboxing, enquanto o processo oposto é chamado de unboxing.
3. Prós e contras
A decisão de qual objeto deve ser usado baseia-se em qual desempenho do aplicativo tentamos obter, quanta memória disponível temos, quantidade de memória disponível e quais valores padrão devemos manipular.
Se não enfrentarmos nada disso, podemos ignorar essas considerações, embora valha a pena conhecê-las.
3.1. Pegada de memória de item único
Apenas para referência, osprimitive type variables têm o seguinte impacto na memória:
-
booleano - 1 bit
-
byte - 8 bits
-
curto, char - 16 bits
-
int, float - 32 bits
-
longo, duplo - 64 bits
Na prática, esses valores podem variar dependendo da implementação da Máquina Virtual. Na VM da Oracle, o tipo booleano, por exemplo, é mapeado para valores int 0 e 1, por isso leva 32 bits, conforme descrito aqui:Primitive Types and Values.
Variáveis desses tipos vivem na pilha e, portanto, são acessadas rapidamente. Para os detalhes, recomendamos nossotutorial no modelo de memória Java.
Os tipos de referência são objetos, eles vivem na pilha e são relativamente lentos para acessar. Eles têm uma certa sobrecarga em relação aos seus homólogos primitivos.
Os valores concretos da sobrecarga são geralmente específicos da JVM. Aqui, apresentamos resultados para uma máquina virtual de 64 bits com estes parâmetros:
java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)
Para obter a estrutura interna de um objeto, podemos usar a ferramentaJava Object Layout (veja nosso outrotutorial sobre como obter o tamanho de um objeto).
Acontece que uma única instância de um tipo de referência neste JVM ocupa 128 bits, exceto paraLongeDouble que ocupam 192 bits:
-
Booleano - 128 bits
-
Byte - 128 bits
-
Curto, Personagem - 128 bits
-
Inteiro, Flutuante - 128 bits
-
Longo, Duplo - 192 bits
Podemos ver que uma única variável do tipoBoolean ocupa tanto espaço quanto 128 primitivas, enquanto uma variávelInteger ocupa tanto espaço quanto quatroint.
3.2. Pegada de memória para matrizes
A situação se torna mais interessante se compararmos quanta memória ocupa matrizes dos tipos em consideração.
Quando criamos matrizes com vários números de elementos para cada tipo, obtemos um gráfico:
que demonstra que os tipos são agrupados em quatro famílias com relação a como a memóriam(s) depende do número de elementos s da matriz:
-
longo, duplo: m (s) = 128 + 64 s
-
curto, caractere: m (s) = 128 + 64 [s / 4]
-
byte, booleano: m (s) = 128 + 64 [s / 8]
-
o resto: m (s) = 128 + 64 [s / 2]
onde os colchetes denotam a função padrão do teto.
Surpreendentemente, os arrays dos tipos primitivos long e double consomem mais memória do que suas classes de wrapperLongeDouble.
Podemos ver quesingle-element arrays of primitive types are almost always more expensive (except for long and double) than the corresponding reference type.
3.3. atuação
O desempenho de um código Java é uma questão bastante sutil, depende muito do hardware no qual o código é executado, do compilador que pode executar determinadas otimizações, do estado da máquina virtual, da atividade de outros processos no sistema operacional.
Como já mencionamos, os tipos primitivos vivem na pilha, enquanto os tipos de referência vivem na pilha. Esse é um fator dominante que determina a rapidez com que os objetos são acessados.
Para demonstrar o quanto as operações para tipos primitivos são mais rápidas do que aquelas para classes wrapper, vamos criar uma matriz de cinco milhões de elementos em que todos os elementos são iguais, exceto o último; em seguida, fazemos uma busca por esse elemento:
while (!pivot.equals(elements[index])) {
index++;
}
e compare o desempenho desta operação para o caso em que a matriz contenha variáveis dos tipos primitivos e para o caso em que contenha objetos dos tipos de referência.
Usamos a conhecida ferramenta de benchmarkingJMH (veja nossotutorial sobre como usá-la), e os resultados da operação de pesquisa podem ser resumidos neste gráfico:
Mesmo para uma operação tão simples, podemos ver que é necessário mais tempo para realizar a operação para as classes de wrapper.
No caso de operações mais complicadas, como soma, multiplicação ou divisão, a diferença de velocidade pode disparar.
3.4. Valores padrão
Os valores padrão dos tipos primitivos são0 (na representação correspondente, ou seja, 0,0.0d etc) para tipos numéricos,false para o tipo booleano,