generic - autoboxing y unboxing en java
¿Por qué algunos idiomas necesitan boxeo y unboxing? (6)
Esto no es una cuestión de qué es el boxeo y el desempaquetado, sino que ¿ por qué los lenguajes como Java y C # lo necesitan?
Estoy muy familiarizado con C ++, STL y Boost.
En C ++ podría escribir algo como esto muy fácilmente,
std::vector<double> dummy;
Tengo algo de experiencia con Java, pero me sorprendió mucho porque tuve que escribir algo como esto,
ArrayList<Double> dummy = new ArrayList<Double>();
Mi pregunta, ¿por qué debería ser un Objeto, qué es tan difícil técnicamente incluir tipos primitivos cuando se habla de genéricos?
¿Qué es tan difícil técnicamente incluir tipos primitivos cuando se habla de genéricos?
En el caso de Java, es debido a la forma en que funcionan los genéricos. En Java, los genéricos son un truco en tiempo de compilación, que le impide colocar un objeto Image
en un ArrayList<String>
. Sin embargo, los genéricos de Java se implementan con borrado de tipo: la información de tipo genérico se pierde durante el tiempo de ejecución. Esto fue por razones de compatibilidad, porque los genéricos se agregaron bastante tarde en la vida de Java. Esto significa que, en tiempo de ejecución, un ArrayList<String>
es efectivamente un ArrayList<Object>
(o mejor: solo ArrayList
que espera y devuelve Object
en todos sus métodos) que se convierte automáticamente en String
cuando recupera un valor.
Pero como int
no se deriva de Object
, no puede ponerlo en un ArrayList que espere (en tiempo de ejecución) Object
y tampoco puede lanzar un Object
a int
. Esto significa que el int
primitivo debe estar envuelto en un tipo que herede de Object
, como Integer
.
C # por ejemplo, funciona de manera diferente. Los genéricos en C # también se aplican en tiempo de ejecución y no se requiere boxeo con una List<int>
. El boxeo en C # solo ocurre cuando intenta almacenar un tipo de valor como int
en una variable de tipo de referencia como object
. Dado que int
en C # se hereda de Object
en C #, escribir object obj = 2
es perfectamente válido, sin embargo, int será encuadrado, lo que se hace automáticamente por el compilador (no se expone ningún tipo de referencia Integer
al usuario ni nada).
Cada objeto no de cadena no de matriz almacenado en el montón contiene un encabezado de 8 o 16 bytes (tamaños para sistemas de 32/64 bits), seguido del contenido de los campos públicos y privados de ese objeto. Las matrices y cadenas tienen el encabezado anterior, más algunos bytes más que definen la longitud de la matriz y el tamaño de cada elemento (y posiblemente el número de dimensiones, la longitud de cada dimensión adicional, etc.), seguidos por todos los campos de la primera elemento, luego todos los campos del segundo, etc. Dada una referencia a un objeto, el sistema puede examinar fácilmente el encabezado y determinar qué tipo es.
Las ubicaciones de almacenamiento de tipo de referencia tienen un valor de cuatro u ocho bytes que identifica de forma única un objeto almacenado en el montón. En las implementaciones actuales, ese valor es un puntero, pero es más fácil (y semánticamente equivalente) considerarlo como un "ID de objeto".
Las ubicaciones de almacenamiento de tipo valor contienen el contenido de los campos del tipo de valor, pero no tienen ningún encabezado asociado. Si el código declara una variable de tipo Int32
, no es necesario almacenar información con ese Int32
diciendo qué es. El hecho de que esa ubicación contenga un Int32
se almacena efectivamente como parte del programa, por lo que no tiene que estar almacenado en la ubicación en sí. Esto representa un gran ahorro si, por ejemplo, uno tiene un millón de objetos, cada uno de los cuales tiene un campo de tipo Int32
. Cada uno de los objetos que contienen el Int32
tiene un encabezado que identifica la clase que puede operarlo. Dado que una copia de ese código de clase puede operar en cualquiera de los millones de instancias, el hecho de que el campo sea un Int32
sea parte del código es mucho más eficiente que tener el almacenamiento para cada uno de esos campos e información sobre qué es. .
El boxeo es necesario cuando se realiza una solicitud para pasar el contenido de una ubicación de almacenamiento de tipo de valor a un código que no sabe esperar ese tipo de valor en particular. El código que espera objetos de tipo desconocido puede aceptar una referencia a un objeto almacenado en el montón. Como cada objeto almacenado en el montón tiene un encabezado que identifica qué tipo de objeto es, el código puede usar ese encabezado siempre que sea necesario usar un objeto de una manera que requiera conocer su tipo.
Tenga en cuenta que en .net, es posible declarar qué se llaman clases y métodos genéricos. Cada declaración de este tipo genera automáticamente una familia de clases o métodos que son idénticos, excepto para el tipo de objeto sobre el que esperan actuar. Si se pasa un Int32
a una rutina DoSomething<T>(T param)
, se generará automáticamente una versión de la rutina en la que cada instancia de tipo T
se reemplaza efectivamente con Int32
. Esa versión de la rutina sabrá que cada ubicación de almacenamiento declarada como tipo T
contiene un Int32
, así como en el caso en que una rutina fue codificada para usar una ubicación de almacenamiento Int32
, no será necesario almacenar información de tipo con esas Ubicaciones propias.
Creo que esto también se debe a que los primitivos no heredan de Object. Supongamos que tiene un método que quiere poder aceptar cualquier cosa como parámetro, por ejemplo.
class Printer {
public void print(Object o) {
...
}
}
Es posible que deba pasar un valor primitivo simple a ese método, como:
printer.print(5);
Podrías hacer eso sin boxear / unboxing, porque 5 es un primitivo y no es un Objeto. Podría sobrecargar el método de impresión para cada tipo primitivo para habilitar dicha funcionalidad, pero es un dolor.
El boxeo y el unboxing son una necesidad que surge de la forma en que los lenguajes (como C # y Java) implementan sus estrategias de asignación de memoria.
Ciertos tipos se asignan en la pila y otros en el montón. Para tratar un tipo asignado a la pila como un tipo asignado al montón, se requiere el boxeo para mover el tipo asignado a la pila en el montón. Unboxing es el proceso inverso.
En C #, los tipos asignados a la pila se denominan tipos de valor (por ejemplo, System.Int32
y System.DateTime
) y los tipos asignados al montón se denominan tipos de referencia (por ejemplo, System.Stream
y System.String
).
En algunos casos es ventajoso poder tratar un tipo de valor como un tipo de referencia (la reflexión es un ejemplo), pero en la mayoría de los casos, es mejor evitar el boxeo y el desempaquetado.
En Java y C # (a diferencia de C ++) todo extiende Object, por lo que las clases de colección como ArrayList pueden contener Object o cualquiera de sus descendientes (básicamente cualquier cosa).
Sin embargo, por razones de rendimiento, las primitivas en java o los tipos de valor en C # recibieron un estado especial. No son objeto. No puedes hacer algo como (en Java):
7.toString()
Aunque toString es un método en Object. Con el fin de unir este guiño al rendimiento, se crearon objetos equivalentes. AutoBoxing elimina el código repetitivo de tener que poner una primitiva en su clase de contenedor y eliminarlo nuevamente, haciendo que el código sea más legible.
La diferencia entre los tipos de valor y los objetos en C # es más gris. Vea here acerca de cómo son diferentes.
Solo puedo decirle para Java por qué no admite tipos primitivos en genéricos.
Primero estaba el problema de que la pregunta para respaldar esto cada vez que surgía en la discusión si Java debería tener tipos primitivos. Lo que, por supuesto, dificultó la discusión de la pregunta real.
En segundo lugar, la razón principal para no incluirlo era que querían una compatibilidad con versiones anteriores binarias para que no se modificara en una máquina virtual que no conocía los genéricos. Esta razón de compatibilidad regresiva / compatibilidad de migración también es la razón por la cual la API de colecciones admite genéricos y permanece igual y no existe (como en C # cuando introdujeron genéricos) un nuevo conjunto completo de una API de colección genérica.
La compatibilidad se realizó utilizando ersure (información de parámetros de tipo genérico eliminada en el momento de la compilación), que también es la razón por la que se obtienen tantas advertencias de conversión no controladas en Java.
Aún podría agregar genéricos reificados, pero no es tan fácil. El simple hecho de agregar la información de tipo agregar tiempo de ejecución en lugar de eliminarla no funcionará ya que rompe la compatibilidad de fuente y binario (no puede seguir utilizando tipos sin formato y no puede llamar al código compilado existente porque no tienen los métodos correspondientes) ).
El otro enfoque es el que eligió C #: ver arriba
Y el autoboxing / unboxing automatizado no fue compatible con este caso de uso porque el autoboxing cuesta demasiado.