Primitivas Java versus Objetos

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:

image

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:

image

 

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, para o tipo char. Para as classes de wrapper, o valor padrão énull.

Isso significa que os tipos primitivos podem adquirir valores apenas de seus domínios, enquanto os tipos de referência podem adquirir um valor (null) que em algum sentido não pertence a seus domínios.

Embora não seja considerado uma boa prática deixar as variáveis ​​não inicializadas, às vezes podemos atribuir um valor após sua criação.

Em tal situação, quando uma variável do tipo primitivo tem um valor igual ao padrão do tipo, devemos descobrir se a variável foi realmente inicializada.

Não há tal problema com variáveis ​​de classe de wrapper, uma vez que o valornull é uma indicação bastante evidente de que a variável não foi inicializada.

4. Uso

Como vimos, os tipos primitivos são muito mais rápidos e requerem muito menos memória. Portanto, podemos preferir usá-los.

Por outro lado, a especificação da linguagem Java atual não permite o uso de tipos primitivos nos tipos parametrizados (genéricos), nas coleções Java ou na API Reflection.

Quando nosso aplicativo precisa de coleções com um grande número de elementos, devemos considerar o uso de arrays com o tipo mais "econômico" possível, conforme ilustrado no gráfico acima.

5. Conclusão

Neste tutorial, ilustramos que os objetos em Java são mais lentos e têm maior impacto na memória que seus análogos primitivos.

Como sempre, trechos de código podem ser encontrados em nossorepository on GitHub.