Qué es el concepto de Heap Pollution en Java

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Al trabajar con referencias de tipos genéricos, raw y arrays debemos conocer el concepto de Heap Pollution si no queremos que en algún punto del programa Java se produzca una excepción no esperada del tipo ClassCastException. No teniéndolo en cuenta nos encontraremos con un error de los más difíciles de depurar ya que la excepción solo nos dirá donde se produjo no donde se encuentra el código erróneo que lo provocó.

Java

La introducción de los tipos genéricos al lenguaje Java en la versión 5 hizo posible que pudiesemos parametrizar los tipos y que el compilador hiciese validaciones sobre ellos, también se permitieron eliminar muchos cast que hasta entonces eran necesarios al usar el framework de colecciones. Los tipos genéricos permiten evitar errores en tiempo de compilación, al mismo tiempo la eliminación de los cast hace el código más legible y más fácilmente refactorizable. Sin embargo, para mantener la compatibilidad con versiones anteriores se optó por hacer algunos sacrificios en la implementación de genéricos en pos de otros beneficios. Una situación potencialmente problemática es el denominado Heap Pollution.

El concepto de Heap Pollution consiste de forma breve (quizá inexacta) en que un tipo genérico contiene un objeto con un tipo que no le corresponde según su tipo genérico, con un ejemplo, que una lista del tipo List<String> contenga un Number entre sus elementos. Que un tipo genérico pueda contener un objeto que no sea de su tipo genérico es detectado en tiempo de compilación con los unchecked warning pero bajo algunas circunstancias se produce en tiempo de ejecución una excepción de tipo ClassCastException, si ignoramos las advertencias y nuestro código no es cuidadoso. Esto es posible porque en Java el tipado de los genéricos sólo está disponible en tiempo de compilación lo que significa que no son reified, el tipado genérico no está disponible en tiempo de ejecución como consecuencia del proceso conocido como type erasure, al trabajar con referencias de tipo raw y genéricas hay que tener cuidado en las asignaciones y las advertencias del compilador.

Veamos en código las circunstancias bajo las cuales se pueden producir Heap Pollution. Un tipo List<String> puede asignarse a un List y luego añadir a esa List un Integer momento en el que el compilador nos avisa con un unchecked warning indicando que no puede validar que la lista raw siendo List un tipo genérico se le está añadiendo una referencia del tipo que debería tener, el compilador nos informa de que esa responsabilidad la tenemos nosotros. También podemos asignar un List a un List<Number>, en este caso el compilador tampoco puede validar que la List sea realmente un List<Number> y lo indica también con un unchecked warning. Ignorando estas advertencias se produce un ClassCastException al acceder al elemento Integer que contiene la List<String> como se comprueba en los teses.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private List<String> strings;

@Before
public void before() {
    strings = new ArrayList<>();
    strings.add("Hello World!");
}

@Test(expected = ClassCastException.class)
public void genericsRaw() {
    List objects = strings;

    objects.add(42); // unchecked warning, heap pollution in strings

    for (String string : strings) {
        System.out.println(string); // ClassCastException is thrown
    }
}

@Test(expected = ClassCastException.class)
public void genericsRawNumber() {
    List objects = strings;
    List<Number> numbers = objects; // unchecked warning

    numbers.add(42); // heap pollution in strings

    for (String string : strings) {
        System.out.println(string); // ClassCastException is thrown
    }
}
MainTest-generics.java

Por otra parte en Java los arrays en tiempo de ejecución necesitan conocer el tipo reified que contendrá. Esto unido a que los varargs realmente se transforman en un array, el posible Heap Pollution se da también en los métodos que soportan varargs.

El compilador convierte los varargs de tipos genéricos de la siguiente forma:

1
2
3
4
5
6
private <T> void add(List<T> list, T... elements) se convierte a
private <T> void add(List<T> list, T[] elements) luego a
private void add(List list, Object[] elements)

private <T> void addFaulty(List<T>... list) se convierte a
private void addFaulty(List[] list)
Erasure-varargs.txt

En un método cuyo último argumento es un vararg y de tipo genérico puede producirse Heap Pollution como indica el compilador, si estamos seguros de que no se puede dar este caso en el código del método podemos eliminar la advertencia del compilador añadiendo la anotación @SafeVarargs en el método. Añadir la anotación solo implica que el compilador eliminará la advertencia pero aún con ella puede seguir produciéndose la excepción ClassCastException si el método no ha sido cuidadoso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void warningVarargs() {
    add(strings, "Hello", "World!");
}

@Test(expected = ClassCastException.class)
public void varargs() {
    List<String>[] array = new List[] { strings, strings };

    addFaulty(strings, array);

    for (List<String> a : array) {
        for (String s : a) {
            System.out.println(s); // ClassCastException is thrown
        }
    }
}

@SafeVarargs
private final <T> void add(List<T> list, T... elements) {
    for (T x : elements) {
        list.add(x);
    }
}

private <T> void addFaulty(List<T> list, List<T>... elements) { // possible heap polltion warning
    Object[] array = elements; // valid, no warning

    for (List<T> x : elements) {
        list.addAll(x);
    }

    array[0] = Arrays.asList(42); // heap pollution
}
MainTest-varargs.java

Tener en cuenta el Heap Pollution es importante ya que la excepción ClassCastException se produce más tarde y en un punto diferente de donde realmente está el error, mucho más tarde si el tipo genérico es serializado e incluso en otra JVM diferente. Estos errores son de los peores de depurar por la poca información que proporcionan ya que la traza de la excepción solo dice quien la lanzó no donde se introdujo el fallo.

La implementación de los generics en Java viene con la garantía conocida como cast-iron que consiste en que mientras el compilador no produzca una unchecked warning en tiempo de compilación se garantiza que en tiempo de ejecución no se producirá una ClassCastException por los cast introducidos en el proceso de erasure.

Si nos encontramos con una de estas excepciones con los genéricos en una colección una buena alternativa es hacer uso de los métodos Collections.checkedCollection, Collections.checkedSet, Collections.checkedMap y alguno más similar que evitará que en una colección se produzca Heap Pollution, la excepción ClassCastException se lanzará en el momento de añadir a la colección la referencia que provocaría el Heap Pollution.

Relacionados con casos de combinar genéricos y arrays y ClassCastException en el libro Java Generics and Collections se definen dos principios a seguir para evitar excepciones: The Principle of Truth in Advertising y The Principle of Indecent Exposure.

Salvo que hagamos operaciones complicadas entre genéricos de diferentes tipos no será muy habitual que nos encontremos ClassCastException por Heap Pollution, pero es un concepto interesante conocer, en cualquier caso el compilador nos informará con los unchecked warnings.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew test


Comparte el artículo: