Múltiples comodines en un método genérico hace que el compilador Java (y yo! muy confundido


Primero consideremos un escenario simple ( ver fuente completa en ideone.com):

import java.util.*;

public class TwoListsOfUnknowns {
    static void doNothing(List<?> list1, List<?> list2) { }

    public static void main(String[] args) {
        List<String> list1 = null;
        List<Integer> list2 = null;
        doNothing(list1, list2); // compiles fine!
    }
}

Los dos comodines no están relacionados, por lo que puede llamar a doNothing con un List<String> y un List<Integer>. En otras palabras, los dos ? pueden referirse a tipos completamente diferentes. Por lo tanto, lo siguiente no compila, lo que es de esperar ( también en ideone.com):

import java.util.*;

public class TwoListsOfUnknowns2 {
    static void doSomethingIllegal(List<?> list1, List<?> list2) {
        list1.addAll(list2); // DOES NOT COMPILE!!!
            // The method addAll(Collection<? extends capture#1-of ?>)
            // in the type List<capture#1-of ?> is not applicable for
            // the arguments (List<capture#2-of ?>)
    }
}

Hasta ahora todo bien, pero aquí es donde las cosas comienzan a ser muy confusas ( como se ve en ideone.com):

import java.util.*;

public class LOLUnknowns1 {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }
}

El código anterior compila para mí en Eclipse y en sun-jdk-1.6.0.17 en ideone.com¿ Pero debería? ¿No es posible que tengamos un List<List<Integer>> lol y un List<String> list, las dos situaciones análogas de comodines no relacionados de TwoListsOfUnknowns?

De hecho, la siguiente ligera modificación hacia esa dirección no compila, lo que es de esperar ( como se ve en ideone.com):

import java.util.*;

public class LOLUnknowns2 {
    static void rightfullyIllegal(
            List<List<? extends Number>> lol, List<?> list) {

        lol.add(list); // DOES NOT COMPILE! As expected!!!
            // The method add(List<? extends Number>) in the type
            // List<List<? extends Number>> is not applicable for
            // the arguments (List<capture#1-of ?>)
    }
}

Así que parece que el compilador está haciendo su trabajo, pero luego obtenemos esto ( como visto en ideone.com):

import java.util.*;

public class LOLUnknowns3 {
    static void probablyIllegalAgain(
            List<List<? extends Number>> lol, List<? extends Number> list) {

        lol.add(list); // compiles fine!!! how come???
    }
}

De nuevo, podemos tener, por ejemplo, a List<List<Integer>> lol y a List<Float> list, por lo que esto no debe compilar, ¿verdad?

De hecho, volvamos al LOLUnknowns1 más simple (dos comodines ilimitados) e intentemos ver si podemos invocar probablyIllegal de alguna manera. Vamos a probar el caso "fácil" primero y elegir el mismo tipo para los dos comodines ( como se ve en ideone.com):

import java.util.*;

public class LOLUnknowns1a {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<List<String>> lol = null;
        List<String> list = null;
        probablyIllegal(lol, list); // DOES NOT COMPILE!!
            // The method probablyIllegal(List<List<?>>, List<?>)
            // in the type LOLUnknowns1a is not applicable for the
            // arguments (List<List<String>>, List<String>)
    }
}

¡Esto no tiene sentido! Aquí ni siquiera estamos tratando de usar dos tipos diferentes, y no lo hace compilar! Haciendo que sea un List<List<Integer>> lol y List<String> list también da un error de compilación similar! De hecho, a partir de mi experimentación, la única forma en que el código se compila es si el primer argumento es un tipo explícito null ( como se ve en ideone.com):

import java.util.*;

public class LOLUnknowns1b {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<String> list = null;
        probablyIllegal(null, list); // compiles fine!
            // throws NullPointerException at run-time
    }
}

Así que las preguntas son, con respecto a LOLUnknowns1, LOLUnknowns1a y LOLUnknowns1b:

  • ¿Qué tipos de argumentos acepta probablyIllegal?
  • Should lol.add(list); compile at all? ¿Es seguro?
  • ¿Es esto un error del compilador o soy yo ¿malinterpretar las reglas de conversión de captura para comodines?

Apéndice A: ¿Doble jajaja?

En caso de que alguien tenga curiosidad, esto compila bien ( como se ve en ideone.com):

import java.util.*;

public class DoubleLOL {
    static void omg2xLOL(List<List<?>> lol1, List<List<?>> lol2) {
        // compiles just fine!!!
        lol1.addAll(lol2);
        lol2.addAll(lol1);
    }
}

Apéndice B: Comodines anidados? ¿qué significan realmente???

Más investigación indica que tal vez múltiples comodines no tiene nada que ver con el problema, sino más bien un anidado comodín es la fuente de la confusión.

import java.util.*;

public class IntoTheWild {

    public static void main(String[] args) {
        List<?> list = new ArrayList<String>(); // compiles fine!

        List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // ArrayList<List<String>> to List<List<?>>
    }
}

Así que parece que un List<List<String>> no es un List<List<?>>. De hecho, mientras que cualquier List<E> es un List<?>, no parece que cualquier List<List<E>> es un List<List<?>> (como se ve en ideone.com):

import java.util.*;

public class IntoTheWild2 {
    static <E> List<?> makeItWild(List<E> list) {
        return list; // compiles fine!
    }
    static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) {
        return lol;  // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // List<List<E>> to List<List<?>>
    }
}

Surge entonces una nueva pregunta: ¿qué es un List<List<?>>?

Author: polygenelubricants, 2010-08-23

3 answers

Como indica el Apéndice B, esto no tiene nada que ver con múltiples comodines, sino más bien, malinterpretar lo que List<List<?>> realmente significa.

Primero recordemos lo que significa que Java generics es invariante: {[73]]}

  1. Un Integer es un Number
  2. A List<Integer> es NO a List<Number>
  3. A List<Integer> IS a List<? extends Number>

Ahora simplemente aplicamos el mismo argumento a nuestra situación de lista anidada (ver apéndice para más detalles):

  1. A List<String> es (captable por) a List<?>
  2. A List<List<String>> es NO (captable por) a List<List<?>>
  3. A List<List<String>> IS (captureable by) a List<? extends List<?>>

Con este entendimiento, se pueden explicar todos los fragmentos de la pregunta. La confusión surge al creer (falsamente) que un tipo como List<List<?>> puede capturar tipos como List<List<String>>, List<List<Integer>>, etc. Esto es NO verdadero.

Es decir, un List<List<?>>:

  • es NO una lista cuyos elementos son listas de algún tipo desconocido.
    • ... eso sería un List<? extends List<?>>
  • En cambio, es una lista cuyos elementos son listas de CUALQUIER tipo.

Fragmentos de código

Aquí hay un fragmento para ilustrar los puntos anteriores:

List<List<?>> lolAny = new ArrayList<List<?>>();

lolAny.add(new ArrayList<Integer>());
lolAny.add(new ArrayList<String>());

// lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!!

List<? extends List<?>> lolSome;

lolSome = new ArrayList<List<String>>();
lolSome = new ArrayList<List<Integer>>();

Más fragmentos

Aquí hay otro ejemplo con comodín anidado acotado:

List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>();

lolAnyNum.add(new ArrayList<Integer>());
lolAnyNum.add(new ArrayList<Float>());
// lolAnyNum.add(new ArrayList<String>());     // DOES NOT COMPILE!!

// lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!!

List<? extends List<? extends Number>> lolSomeNum;

lolSomeNum = new ArrayList<List<Integer>>();
lolSomeNum = new ArrayList<List<Float>>();
// lolSomeNum = new ArrayList<List<String>>(); // DOES NOT COMPILE!!

Volver a la pregunta

A volver a los fragmentos de la pregunta, lo siguiente se comporta como se espera ( como se ve en ideone.com):

public class LOLUnknowns1d {
    static void nowDefinitelyIllegal(List<? extends List<?>> lol, List<?> list) {
        lol.add(list); // DOES NOT COMPILE!!!
            // The method add(capture#1-of ? extends List<?>) in the
            // type List<capture#1-of ? extends List<?>> is not 
            // applicable for the arguments (List<capture#3-of ?>)
    }
    public static void main(String[] args) {
        List<Object> list = null;
        List<List<String>> lolString = null;
        List<List<Integer>> lolInteger = null;

        // these casts are valid
        nowDefinitelyIllegal(lolString, list);
        nowDefinitelyIllegal(lolInteger, list);
    }
}

lol.add(list); es ilegal porque podemos tener un List<List<String>> lol y a List<Object> list. De hecho, si comentamos la declaración ofensiva, el código compila y eso es exactamente lo que tenemos con la primera invocación en main.

Todos los probablyIllegal métodos en la pregunta, no son ilegales. Todos son perfectamente legales y seguros. No hay absolutamente ningún error en el compilador. Es haciendo exactamente lo que se supone que debe hacer.


Referencias

Cuestiones conexas


Apéndice: Las reglas de conversión de captura

(Esto se planteó en la primera revisión de la respuesta; es un complemento digno del argumento invariante de tipo.)

5.1.10 Conversión de captura

Let G name a generic type declaration with n formal type parámetros Un1...Unn con los correspondientes límites U1...Un. Existe una captura de conversión de G1...Tn> a G1...Sn>, donde, para 1 :

  1. If Ti es un argumento de tipo comodín de la forma ? entonces {
  2. If Ti es un argumento de tipo comodín de la forma ? extends Bi, entonces ...
  3. Si Ti es un comodín tipo de argumento de la forma ? super Bi, entonces ...
  4. De lo contrario, S i = Ti.

La conversión de captura no se aplica recursivamente.

Esta sección puede ser confusa, especialmente con respecto a la aplicación no recursiva de la conversión de captura (por la presente CC), pero la clave es que no todos ? pueden CC; depende de dónde aparezca. No hay aplicación recursiva en la regla 4, pero cuando se aplican las reglas 2 o 3, entonces el respectivo Bi puede ser el resultado de un CC.

Vamos a trabajar a través de algunos ejemplos simples:

  • List<?> puede CC List<String>
    • El ? puede CC por regla 1
  • List<? extends Number> puede CC List<Integer>
    • El ? puede CC por regla 2
    • Al aplicar la regla 2, Bi es simplemente Number
  • List<? extends Number> puede NO CC List<String>
    • El ? puede CC por regla 2, pero se produce un error de tiempo de compilación debido a tipos incompatibles

Ahora probemos algo de anidamiento: {[73]]}

  • List<List<?>> puede NO CC List<List<String>>
    • Se aplica la regla 4, y CC no es recursivo, por lo que el ? puede NO CC
  • List<? extends List<?>> puede CC List<List<String>>
    • La primera ? puede CC por regla 2
    • Al aplicar la regla 2, B i ahora es un List<?>, que puede CC List<String>
    • Ambos ? pueden CC
  • List<? extends List<? extends Number>> puede CC List<List<Integer>>
    • El primero ? puede CC por regla 2
    • Al aplicar la regla 2, B i ahora es un List<? extends Number>, que puede CC List<Integer>
    • Ambos ? pueden CC
  • List<? extends List<? extends Number>> puede NO CC List<List<Integer>>
    • El primer ? puede CC por regla 2
    • Al aplicar la regla 2, B i ahora es un List<? extends Number>, que puede CC, pero da un error de tiempo de compilación cuando se aplica a List<Integer>
    • Ambos ? pueden CC

Para ilustrar más por qué algunos ? pueden CC y otros no, considere la siguiente regla: puede NO instanciar directamente un tipo de comodín. Es decir, lo siguiente da un error de tiempo de compilación:

    // WildSnippet1
    new HashMap<?,?>();         // DOES NOT COMPILE!!!
    new HashMap<List<?>, ?>();  // DOES NOT COMPILE!!!
    new HashMap<?, Set<?>>();   // DOES NOT COMPILE!!!

Sin embargo, lo siguiente compila solo fine:

    // WildSnippet2
    new HashMap<List<?>,Set<?>>();            // compiles fine!
    new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!

La razón por la que WildSnippet2 compila es porque, como se explicó anteriormente, ninguno de los ? puede CC. En WildSnippet1, ya sea el K o el V (o ambos) del HashMap<K,V> puede CC, lo que hace que la instanciación directa a través de new sea ilegal.

 66
Author: polygenelubricants,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:09:58
  • No debe aceptarse ningún argumento con genéricos. En el caso de LOLUnknowns1b el null se acepta como si el primer argumento se escribiera como List. Por ejemplo, esto compila :

    List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    
  • IMHO lol.add(list); ni siquiera debería compilar, pero como lol.add() necesita un argumento de tipo List<?> y como list encaja en List<?> funciona.
    Un extraño ejemplo que me hace pensar en esta teoría es :

    static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) {
        lol.add(list); // compiles fine!!! how come???
    }
    

    lol.add() necesita un argumento de tipo List<? extends Number> y list se escribe como List<? extends Integer>, encaja. No funcionará si no coincide. Lo mismo para el doble LOL, y otros comodines anidados, siempre y cuando la primera captura coincida con la segunda, todo está bien (y no debería ser).

  • Una vez más, no estoy seguro, pero realmente parece un error.

  • Me alegro de no ser el único en usar variables lol todo el tiempo.

Recursos :
http://www.angelikalanger.com , una FAQ acerca de los genéricos

Ediciones:

  1. Añadido comentario sobre el Doble Lol
  2. Y comodines anidados.
 2
Author: Colin Hebert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-23 12:02:03

No es un experto, pero creo que puedo entenderlo.

Cambiemos tu ejemplo a algo equivalente, pero con tipos más distintivos:{[12]]}

static void probablyIllegal(List<Class<?>> x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

Cambiemos la Lista a [] para ser más esclarecedora:

static void probablyIllegal(Class<?>[] x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

Ahora, x es no una matriz de algún tipo de clase. es una matriz de cualquier tipo de clase. puede contener un Class<String> y a Class<Int>. esto no se puede expresar con el parámetro de tipo ordinario:

static<T> void probablyIllegal(Class<T>[] x  //homogeneous! not the same!

Class<?> es un super tipo de Class<T> para cualquiera T. Si pensamos en un tipo es a conjunto de objetos, conjunto Class<?> es la unión de todos los los conjuntos de Class<T> para todo T. (¿se incluye a sí mismo? No sé...)

 0
Author: irreputable,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-23 17:43:46