Descripción general
En este tutorial, mostramos los pros y los contras de usar tipos primitivos de Java y sus contrapartes empaquetadas.
Sistema de tipo Java
Java tiene un sistema de tipo doble que consiste en primitivas como int, booleano y tipos de referencia como Integer, Booleano. Cada tipo primitivo corresponde a un tipo de referencia.
Cada objeto contiene un único valor del tipo primitivo correspondiente. Las clases de envoltura son inmutables (por lo que su estado no puede cambiar una vez que se construye el objeto) y son finales (por lo que no podemos heredar de ellas).
Bajo el capó, Java realiza una conversión entre los tipos primitivo y de referencia si un tipo real es diferente del declarado:
Integer j = 1; // autoboxingint i = new Integer(1); // unboxing
El proceso de convertir un tipo primitivo a uno de referencia se llama autoboxing, el proceso opuesto se llama unboxing.
Pros y Contras
La decisión de qué objeto se va a utilizar se basa en el rendimiento de la aplicación que intentamos lograr, la cantidad de memoria disponible que tenemos, la cantidad de memoria disponible y los valores predeterminados que debemos manejar.
Si no nos enfrentamos a ninguno de ellos, podemos ignorar estas consideraciones, aunque vale la pena conocerlas.
3.1. Huella de memoria de un solo elemento
Solo para la referencia, las variables de tipo primitivo tienen el siguiente impacto en la memoria:
- booleano – 1 bit
- byte-8 bits
- corto, char – 16 bits
- int, float-32 bits
- largo, doble – 64 bits
En la práctica, estos valores pueden variar según la implementación de la máquina virtual. En la máquina virtual de Oracle, el tipo booleano, por ejemplo, se asigna a los valores int 0 y 1, por lo que toma 32 bits, como se describe aquí: Tipos y valores primitivos.
Las variables de este tipo viven en la pila y, por lo tanto, se accede rápidamente. Para más detalles, recomendamos nuestro tutorial sobre el modelo de memoria Java.
Los tipos de referencia son objetos, viven en el montón y su acceso es relativamente lento. Tienen una cierta sobrecarga con respecto a sus contrapartes primitivas.
Los valores concretos de la sobrecarga son en general específicos de JVM. Aquí, presentamos los resultados de una máquina virtual de 64 bits con estos parámetros:
java 10.0.1 2018-04-17Java(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 obtener la estructura interna de un objeto, podemos usar la herramienta de Diseño de objetos Java (consulte nuestro otro tutorial sobre cómo obtener el tamaño de un objeto).
Resulta que una sola instancia de un tipo de referencia en esta JVM ocupa 128 bits, excepto Long y Double, que ocupan 192 bits:
- Booleano – 128 bits
- Byte – 128 bits
- Corto, Carácter – 128 bits
- Entero, Flotador – 128 bits
- Largo, Doble – 192 bits
Podemos ver que una sola variable de tipo booleano ocupa tanto espacio como 128 primitivas, mientras que una variable entera ocupa tanto espacio como cuatro int.
3.2. Huella de memoria para matrices
La situación se vuelve más interesante si comparamos cuánta memoria ocupan las matrices de los tipos considerados.
Cuando podemos crear matrices con los distintos número de elementos de cada tipo, se obtiene una parcela:
que demuestra que los tipos se agrupan en cuatro familias con respecto a cómo la memoria m(s) depende del número de elementos de la matriz:
- long, double: m(s) = 128 + 64 s
- short, char: m(s) = 128 + 64
- byte, boolean: m(s) = 128 + 64
- el resto: m ( s) = 128 + 64
donde los corchetes indican la función de techo estándar.
Sorprendentemente, los arrays de los tipos primitivos long y double consumen más memoria que sus clases de envoltura Long y Double.
Podemos ver que las matrices de un solo elemento de tipos primitivos son casi siempre más caras (excepto largas y dobles) que el tipo de referencia correspondiente.
3.3. Rendimiento
El rendimiento de un código Java es un problema bastante sutil, depende en gran medida del hardware en el que se ejecuta el código, del compilador que podría realizar ciertas optimizaciones, del estado de la máquina virtual, de la actividad de otros procesos en el sistema operativo.
Como ya hemos mencionado, los tipos primitivos viven en la pila, mientras que los tipos de referencia viven en el montón. Este es un factor dominante que determina la rapidez con la que se accede a los objetos.
Para demostrar cuánto las operaciones para tipos primitivos son más rápidas que las de las clases de envoltura, vamos a crear un array de cinco millones de elementos en el que todos los elementos son iguales excepto el último; luego realizamos una búsqueda para ese elemento:
while (!pivot.equals(elements)) { index++;}
y comparamos el rendimiento de esta operación para el caso cuando el array contiene variables de los tipos primitivos y para el caso cuando contiene objetos de los tipos de referencia.
Utilizamos la conocida herramienta de evaluación comparativa de JMH (consulte nuestro tutorial sobre cómo usarla), y los resultados de la operación de búsqueda se pueden resumir en esta tabla:
Incluso para una operación tan simple, podemos ver que se requiere más tiempo para realizar la operación para las clases de envoltura.
En el caso de operaciones más complicadas como la suma, multiplicación o división, la diferencia de velocidad podría dispararse.
3.4. Valores predeterminados
Los valores predeterminados de los tipos primitivos son 0 (en la representación correspondiente, es decir, 0, 0.0d etc) para tipos numéricos, false para el tipo booleano, \u0000 para el tipo char. Para las clases wrapper, el valor predeterminado es null.
Significa que los tipos primitivos pueden adquirir valores solo de sus dominios, mientras que los tipos de referencia pueden adquirir un valor (nulo) que en algún sentido no pertenece a sus dominios.
Aunque no se considera una buena práctica dejar variables sin inicializar, a veces podemos asignar un valor después de su creación.
En tal situación, cuando una variable de tipo primitivo tiene un valor que es igual a su tipo predeterminado, debemos averiguar si la variable ha sido realmente inicializada.
No hay tal problema con las variables de clase de envoltura, ya que el valor nulo es una indicación bastante evidente de que la variable no se ha inicializado.
Uso
Como hemos visto, los tipos primitivos son mucho más rápidos y requieren mucha menos memoria. Por lo tanto, es posible que prefiramos usarlos.
Por otro lado, la especificación actual del lenguaje Java no permite el uso de tipos primitivos en los tipos parametrizados (genéricos), en las colecciones Java o en la API de Reflexión.
Cuando nuestra aplicación necesita colecciones con un gran número de elementos, debemos considerar el uso de matrices con el tipo más «económico» posible, como se ilustra en la gráfica anterior.
Conclusión
En este tutorial, ilustramos que los objetos en Java son más lentos y tienen un impacto de memoria mayor que sus análogos primitivos.
Como siempre, los fragmentos de código se pueden encontrar en nuestro repositorio en GitHub.
empezar con el Muelle 5 y el Resorte de Arranque 2, a través del Aprender de Primavera del curso:
>> COMPRUEBE EL CURSO