En Java, todo es un objeto, excepto las primitivas como int
. Resulta que esa pequeña advertencia ha tenido grandes implicaciones para el idioma, que se han agravado a lo largo de los años. Esta decisión de diseño aparentemente menor causa problemas en áreas clave como colecciones y genéricos. También limita ciertas optimizaciones de rendimiento. Project Valhalla, el refactor del lenguaje Java, tiene como objetivo corregir estos problemas. El líder del proyecto Valhalla, Brian Goetz, ha dicho que Valhalla “curará la brecha entre los primitivos y los objetos”.
Es justo decir que Project Valhalla es un refactor épico, que busca abordar la deuda técnica enterrada en la plataforma desde el inicio de Java. Esta evolución completa demuestra que Java no solo es un clásico, sino que permanece a la vanguardia del diseño de lenguajes de programación. Echemos un vistazo a los componentes técnicos clave del Proyecto Valhalla y por qué son tan críticos para el futuro de Java.
Problemas de rendimiento en Java
Cuando se introdujo Java por primera vez allá por los años 90, se decidió que todos los tipos creados por los usuarios serían clases. Solo un puñado de tipos primitivos se dejaron de lado como especiales. Estos no se manejaron como estructuras de clases basadas en punteros, sino que se asignaron directamente a los tipos de sistemas operativos. Los ocho tipos primitivos son int
, byte
, short
, long
, float
, double
, boolean
y char
.
La asignación directa de estas variables al sistema operativo fue mejor para el rendimiento porque las operaciones numéricas funcionaron mejor cuando se despojaron de la sobrecarga referencial de los objetos. Además, todos los datos finalmente se resuelven en estos ocho tipos primitivos en un programa. Las clases son solo una especie de capa estructural y organizativa que ofrece formas más poderosas de manejar tipos primitivos. El único otro tipo de estructura es la matriz. Primitivas, clases y arreglos comprenden toda la gama del poder expresivo de Java.
Pero los primitivos son una categoría diferente de animales de las clases y matrices. Como programadores, hemos aprendido a lidiar con las diferencias de manera intuitiva. Las primitivas se pasan por valor mientras que los objetos se pasan por referencia, por ejemplo. El por qué de esto va bastante profundo. Todo se reduce a la cuestión de la identidad. Podemos decir que los valores primitivos son fungibles: int x = 4
es el entero 4, no importa donde aparezca. Vemos esta distinción en equals()
contra ==
, donde el primero prueba la equivalencia de valor de los objetos y el segundo prueba la identidad. Si dos referencias comparten el mismo espacio en la memoria, satisfacen ==
, lo que significa que son el mismo objeto. Cualquier int
s establecido en 4 también satisfará ==
mientras int
no es compatible .equals()
en absoluto.
La máquina virtual de Java (JVM) puede aprovechar la forma en que se manejan las primitivas para optimizar la forma en que las almacena, las recupera y las opera. En particular, si la plataforma determina que una variable no está alterada (es decir, es una constante o inmutable), entonces está disponible para ser optimizada.
Los objetos, por el contrario, son resistentes a este tipo de optimización porque tienen una identidad. Como instancia de una clase, un objeto contiene datos que pueden ser primitivos y otras clases. El objeto en sí se direcciona con un identificador de puntero. Esto crea una red de referencias: el objeto gráfico. Cada vez que se cambia algún valor, o incluso si podría cambiarse: la JVM se ve obligada a mantener un registro definitivo del objeto para hacer referencia. La necesidad de hacer referencia a objetos es una barrera para algunas optimizaciones de rendimiento.
Las dificultades de rendimiento no se detienen ahí. La naturaleza de los objetos como cubos de referencias significa que existen en la memoria de una manera muy fluida. Esponjoso es mi término técnico para describir el hecho de que la JVM no puede comprimir objetos para minimizar su consumo de memoria. Cuando un objeto tiene una referencia a otro objeto como parte de su composición, la JVM se ve obligada a mantener esa relación de puntero. (En algunos casos, una optimización inteligente podría ayudar a determinar que una referencia anidada es el único identificador de una entidad en particular).
En su publicación de blog State of Valhalla, Goetz utiliza una serie de puntos para ilustrar la naturaleza no densa de las referencias. Podemos usar una clase. Por ejemplo, digamos que tenemos un Landmark
clase con un nombre y un campo de geolocalización. Estos implican una estructura de memoria como la que se muestra aquí:
Figura 1. Una huella de memoria ‘esponjosa’ de objetos Java.
Lo que nos gustaría lograr es la capacidad de sostener un objeto, cuando sea apropiado, como se muestra en la Figura 2.
Figura 2. Un objeto denso en la memoria.
Esa es una descripción general de los desafíos de rendimiento que se incluyeron en la plataforma Java por las primeras decisiones de diseño. Ahora, consideremos cómo estas decisiones afectan el rendimiento en tres áreas clave.
Problema 1: llamada de método y paso por valor
La estructura predeterminada de los objetos en la memoria es ineficiente tanto para la memoria como para el almacenamiento en caché. Además, existe la oportunidad de obtener ganancias en las convenciones de llamada de métodos. Ser capaz de pasar argumentos de llamada por valor a métodos con sintaxis de clase (cuando corresponda) generaría importantes beneficios de rendimiento.
Problema 2: Cajas y autoboxing
Más allá de las ineficiencias, la distinción entre primitive
y class
crea dificultades en el nivel del lenguaje. Crear “cajas” primitivas como Integer
y Long
(junto con el autoboxing) es un intento de paliar los problemas causados por esta distinción. Sin embargo, en realidad no los soluciona e introduce un grado de sobrecarga tanto para el desarrollador como para la máquina. Como desarrollador, debe conocer y recordar la diferencia entre int
y Integer
(y ArrayList<Integer>
, int[]
, Integer[]
y la falta de un ArrayList<int>
). La máquina, mientras tanto, tiene que convertir entre los dos.
En cierto modo, el boxeo nos da lo peor de ambos mundos. Ocultar los matices subyacentes de cómo funcionan estas entidades dificulta el acceso tanto al poder de la sintaxis de clase y el desempeño de las primitivas.
Problema 3: Genéricos y streams
Todas estas consideraciones llegan a un punto crítico en los genéricos. Los genéricos están destinados a hacer que la generalización a través de la funcionalidad sea más fácil y más explícita, pero la presencia quisquillosa de este conjunto de variables que no son objetos (las primitivas) solo hace que se rompa. <int>
no existe—no puede existir porque int
no es una clase en absoluto; no desciende de Object
.
Este problema se manifiesta luego en bibliotecas como colecciones y flujos, donde el ideal de las funciones de biblioteca genéricas se ve obligado a lidiar con la realidad de int
versus Integer
, long
versus Long
y así sucesivamente, ofreciendo IntStream
y otras variaciones no genéricas.
La solución de Valhalla: clases de valor y tipos primitivos
El Proyecto Valhalla ataca estos problemas desde la raíz. El primer y más fundamental concepto es el clase de valor. La idea aquí es que puede definir una clase que participe de todo lo bueno de las clases, como tener métodos y poder cumplir con los genéricos, pero sin la identidad. En la práctica, eso significa que las clases son inmutables y no pueden tener un diseño polimórfico (donde la superclase puede operar sobre las subclases a través de propiedades abstractas).
Las clases de valor nos brindan una forma clara y definitiva de obtener las características de rendimiento que buscamos mientras seguimos accediendo a los beneficios de la sintaxis y el comportamiento de la clase. Eso significa que los creadores de bibliotecas también pueden usarlos y, por lo tanto, mejorar el diseño de su API.
Un paso más allá es el clase primitiva, que es como una clase de valor más extremo. En esencia, la clase primitiva es un envoltorio delgado alrededor de una verdadera variable primitiva, pero con métodos de clase. Esto es algo así como cajas primitivas optimizadas y personalizadas. La mejora está en hacer que el sistema de boxeo sea más explícito y extensible. Además, el valor primitivo envuelto por una clase primitiva retiene las características de rendimiento de la primitiva (sin encajonamiento ni desempaquetado bajo el capó). Por lo tanto, la clase primitiva puede usarse dondequiera que puedan estar las clases, en un Object[]
matriz, por ejemplo. Los tipos primitivos no aceptarán valores NULL (no se pueden establecer en NULL).
En general, podríamos decir que Project Valhalla acerca los primitivos y los tipos definidos por el usuario. Esto brinda a los desarrolladores más opciones en el espectro entre objetos y primitivos puros y hace explícitas las compensaciones. También hace que estas operaciones en general sean más consistentes. En particular, el nuevo sistema primitivo suavizará cómo funcionan los primitivos y los objetos, cómo se encuadran y cómo se pueden agregar otros nuevos.
Cómo cambiará la sintaxis de Java
Valhalla ha visto algunas propuestas de sintaxis diferentes, pero ahora el proyecto está tomando una forma y una dirección claras. Dos nuevas palabras clave modifican la class
palabra clave: value
y primitive
. Una clase declarada con el value class
la sintaxis entregará su identidad y, en el proceso, obtendrá mejoras en el rendimiento. Además de las restricciones de mutabilidad y polimorfismo, la mayoría de las cosas que esperaría de una clase aún se aplican y dichas clases pueden participar plenamente en el código genérico (como object[]
o ArrayList<T>
). Las clases de valor tienen el valor predeterminado nulo.
El primitive class
sintaxis crea una clase que está un paso más allá de los objetos tradicionales y hacia las primitivas tradicionales. Estas clases tienen por defecto el valor subyacente de los campos (0 para int
0.0 para double
, etc.) y no puede ser nulo. Las clases primitivas ganan más en optimización y sacrifican más en términos de características. Las clases primitivas no son seguras contra desgarros de 32 bits. La clase primitiva se usará en última instancia para modelar todas las primitivas en la plataforma, lo que significa que las adiciones de primitivas definidas por el usuario y la biblioteca participarán en el mismo sistema que las integradas.
IdentityObject y ValueObject
IdentityObject
y ValueObject
son dos nuevas interfaces que se están introduciendo en Project Valhalla. Estos permitirán determinar en tiempo de ejecución con qué tipo de clase se está tratando.
Quizás el cambio de sintaxis más radical para los desarrolladores de Java experimentados es la adición de la .ref
miembro. Todos los tipos tendrán ahora la V.ref()
campo. Este campo opera como el cuadro de las primitivas, por lo que int.ref
es análogo a envolver un int
con un Integer
. Las clases normales resolverán .ref
a su referencia. El efecto general es crear una forma coherente de solicitar una referencia sobre una variable, independientemente de su tipo. Esto también tiene el efecto de hacer que todas las matrices de Java sean “covariantes”, es decir, todas descienden de Object[]
. Por lo tanto, int[]
ahora desciende de Object[]
y se puede utilizar donde sea necesario.
Conclusión
Las clases de valor y las clases primitivas tendrán un gran impacto en Java y su ecosistema. La hoja de ruta actual planea introducir clases de valor primero, seguidas de clases primitivas. Lo siguiente será la migración de las clases primitivas de boxeo existentes (como Integer
) para usar la nueva clase primitiva. Con esas características en la mano, la siguiente característica, llamada genéricos universales, permitirá que las clases primitivas se usen directamente con los genéricos, suavizando muchas de las complejidades de la reutilización en las API. Finalmente, los genéricos especializados (que permiten toda la capacidad expresiva de T extends Foo
) se integrará con clases primitivas.
Project Valhalla y los proyectos que lo componen todavía están en etapas de diseño, pero nos estamos acercando y la actividad en torno al proyecto indica que no pasará mucho tiempo antes de que las clases de valor caigan en una vista previa de JDK.
Más allá de todo el interesante trabajo técnico está la sensación de vitalidad continua de Java. Que haya voluntad y capacidad para pasar por el proceso de identificar dónde puede evolucionar la plataforma de manera fundamental es evidencia de un compromiso real para mantener la relevancia de Java. Project Loom es otra empresa que da peso a una visión optimista del futuro de Java.
Derechos de autor © 2023 IDG Communications, Inc.
Be First to Comment