Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

C# versus Java: genericidad

La adición de tipos genéricos a Java y .NET ilustra con meridiana claridad la diferencia entre ambas plataformas, y explica por qué la ventaja de .NET no es el fruto de una mera y feliz coyuntura, sino que se debe a causas estructurales.

Antes de la adición de estos tipos al repertorio, si usted quería manejar listas de cadenas y listas de números reales, necesitaba dos tipos de datos independientes, programados a la medida para cada necesidad. Como alternativa, se podía utilizar alguna lista de propósito general, como el List de .NET 1.1, un tipo diseñado para almacenar cualquier tipo de objetos:

System.Collections.List lista = new System.Collections.List();
lista.Add(34.0);
double valor = (double)lista[0];

El fragmento de código anterior muestra dos problemas del uso de contenedores diseñados para el almacenamiento de objetos en general:

  • Aunque es posible, gracias a la regla de compatibilidad polimórfica, añadir cualquier tipo de objeto al contenedor, para extraerlo, si queremos preservar su tipo, estamos obligados a efectuar una conversión de tipos en tiempo de ejecución, que suele resultar costosa.
  • Incluso la adición presenta problemas. Si el tipo de objeto a añadir es un tipo con semántica de asignación por referencia, no hay mayor dificultad. Pero para tipos con semántica de valor, como el tipo double del ejemplo, el entorno de ejecución se ve obligado a reservar memoria para una nueva instancia, copiar el estado del valor original e insertar la nueva instancia. Esta operación es común para Java y .NET, y se conoce como boxing. En este caso, la conversión inversa, que tiene lugar cuando accedemos al objeto, implica la conversión inversa: el llamado unboxing. Es innecesario aclarar que el boxing es una operación costosa, porque exige la asignación de memoria dinámica.

Gracias a los tipos genéricos, ahora podemos escribir, tanto en Java como en cualquiera de los lenguajes soportados por .NET:

List<double> lista1 = new List<double>();
lista1.Add(34.0);
double valor = lista1[0];
List<string> lista2 = new List<string>();
lista2.Add("'Twas brillig, and the slithy toves...");
string cadena = lista2[0];

Ahora bien, ¿qué hay detrás de este fragmento de código? ¿Cómo se implementa este útil recurso en cada plataforma?

  • En .NET, los tipos genéricos se han añadido directamente a la máquina virtual. Cuando un tipo genérico, como la lista antes mencionada, se utiliza para almacenar tipos con semántica de referencia, la máquina virtual genera, o reutiliza si ya está disponible, una implementación que almacena valores que caben en el tamaño de un puntero. Si se pide una lista que almacene tipos con semántica de valor, se sintetiza una nueva implementación para el tamaño de instancias adecuado. En una plataforma de 32 bits, por ejemplo, las listas de números reales exigirían una nueva, y eficiente, implementación, pero las listas de valores enteros reutilizarían, también de manera eficiente, el mismo código que las listas de tipos con semántica de referencia.
  • En Java, por el contrario, la máquina virtual sigue siendo la misma. La implementación de los tipos genéricos corre a cargo del compilador, que traduce el tipo genérico original como una estructura que siempre almacena punteros generales a objetos. En otras palabras, siempre se usa la implementación pésima que antes mencionábamos y que queríamos evitar a toda costa.

Esta diferencia en la implementación trae todo tipo de consecuencias, y todas son negativas para Java:

  • Sufre la eficiencia, en general, porque la conversión de tipos durante la extracción y accesos sigue siendo necesaria. Observe que estas conversiones no son necesarias, y no se ejecutan, en .NET, porque el sistema de tipos garantiza su seguridad, y porque la implementación no pasa por una etapa innecesaria en código intermedio, como sí ocurre en Java.
  • Además, en el caso de tipos genéricos que deben recibir tipos con semántica de valor, la eficiencia sale muy perjudicada cuando el tamaño de las instancias de estos tipos no coincide con el tamaño del puntero en la plataforma. El tipo final sigue necesitando las operaciones de boxing y unboxing para funcionar, generando basura para el colector de memoria inalcanzable.
  • Hay un problema mucho más sutil, pero no menos importante: en la implementación de Java se pierde la identidad del tipo genérico. Hay que recordar que la reflexión es una característica muy importante en estas plataformas. La implementación en .NET conserva la fidelidad del tipo, mientras que la de Java, que se conoce técnicamente como type erasure, impide el uso efectivo del API de reflexión con las instancias cerradas de los tipos genéricos. Si usted pasa, por ejemplo, una lista de clientes de un proceso a otro por medio de remoting, el tipo recibido no guarda relación alguna con el tipo que usted ha creído enviar.

¿Qué posibilidad hay de que Java corrija estos problemas? A juzgar por los precedentes, poca o ninguna. En Sun están obsesionados por mantener una funcionalidad mínima en la máquina virtual de Java, como demostraron en la triste historia de los tipos delegados, y la adición de genericidad a nivel de la máquina significa una reescritura importante de ésta, que difícilmente consentirán.

Incluso si, en algún momento, se impusiese el sentido común y cambiase el viento, hay que tener en cuenta que Microsoft ofrece una implementación eficiente, robusta y segura de tipos genéricos desde la versión 2.0 de su plataforma, que data del 2005, y que en el momento en que escribo este artículo ya lleva dos años y medio a disposición de la comunidad de programadores y analistas.