¿Cómo funcionan los genéricos de los genéricos?


Si bien entiendo algunos de los casos de esquina de los genéricos, me estoy perdiendo algo con el siguiente ejemplo.

Tengo la siguiente clase

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

La línea 4 me da el error

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

Obviamente, el compilador piensa que los diferentes ? no son realmente iguales. Aunque mi instinto me lo dice, esto es correcto.

¿Puede alguien proporcionar un ejemplo donde obtendría un error de tiempo de ejecución si la línea 4 fuera legal?

EDITAR:

Para evitar confusión, sustituí el =null en la línea 3 por una asignación concreta

Author: Bhesh Gurung, 2013-05-09

5 answers

Como Kenny ha señalado en su comentario, usted puede conseguir alrededor de esto con:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Esto nos dice inmediatamente que la operación no es insegura, es solo una víctima de inferencia limitada . Si fuera inseguro, lo anterior no se compilaría.

Dado que el uso de parámetros de tipo explícitos en un método genérico como el anterior solo es necesario para actuar como una pista , podemos suponer que se requiere aquí es una limitación técnica del motor de inferencia. De hecho, el compilador Java 8 está actualmente programado para incluir muchas mejoras a la inferencia de tipos. No estoy seguro de si su caso específico será resuelto.

Entonces, ¿qué está pasando realmente?

Bueno, el error de compilación que estamos obteniendo muestra que el parámetro de tipo T de Collections.singletonList se infiere que es capture<Test<? extends Number>>. En otras palabras, el comodín tiene algunos metadatos asociados que se vincula a un contexto específico.

  • La mejor manera de pensar en un la captura de un comodín (capture<? extends Foo>) es como un parámetro de tipo sin nombre de los mismos límites (es decir, <T extends Foo>, pero sin poder hacer referencia a T).
  • La mejor manera de "liberar" el poder de la captura es vinculándola a un parámetro de tipo con nombre de un método genérico. Voy a demostrar esto en un ejemplo a continuación. Consulte el tutorial de Java "Métodos de Captura de Comodines y de Ayuda" (gracias por la referencia @WChargin) para obtener más información.

Digamos que queremos tener un método que desplaza una lista, envolviendo hacia atrás. Entonces asumamos que nuestra lista tiene un tipo desconocido (comodín).

public static void main(String... args) {
    List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    List<? extends String> cycledTwice = cycle(cycle(list));
}

public static <T> List<T> cycle(List<T> list) {
    list.add(list.remove(0));
    return list;
}

Esto funciona bien, porque T se resuelve a capture<? extends String>, no ? extends String. Si en su lugar usamos esta implementación no genérica de cycle:

public static List<? extends String> cycle(List<? extends String> list) {
    list.add(list.remove(0));
    return list;
}

Fallaría al compilar, porque no hemos hecho accesible la captura asignándola a un parámetro de tipo.

Así que esto comienza a explicar por qué el consumidor de singletonList se beneficiaría del tipo-inferer resolviendo T a Test<capture<? extends Number>, y devolviendo así un List<Test<capture<? extends Number>>> en lugar de un List<Test<? extends Number>>.

Pero ¿por qué no se puede asignar uno al otro?

¿Por qué no podemos asignar un List<Test<capture<? extends Number>>> a un List<Test<? extends Number>>?

Bueno, si pensamos en el hecho de que capture<? extends Number> es el equivalente de un parámetro de tipo anónimo con un límite superior de Number, entonces podemos convertir esta pregunta en "¿Por qué no se compila lo siguiente?"(it doesn't!):

public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
    return t;
} 

Esto tiene una buena razón para no compilar. Si lo hizo, entonces esto sería posible:

//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);

Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>

Entonces, ¿por qué funciona ser explícito?

Volvamos al principio. Si lo anterior no es seguro, entonces ¿cómo es que esto está permitido:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Para que esto funcione, implica que se permite lo siguiente: {[28]]}

Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;

Bueno, esta sintaxis no es válida, ya que no podemos hacer referencia a la captura explícitamente, ¡así que evaluémosla usando la misma técnica que la anterior! Vamos a enlazar la captura a una variante diferente de"assign":

public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
    return t;
} 

Esto compila con éxito. Y no es difícil ver por qué debería ser seguro. Es el caso de uso de algo como

List<? extends Number> l = new List<Double>();
 22
Author: Mark Peters,
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
2013-05-08 22:55:28

No hay un error potencial de tiempo de ejecución, está justo fuera de la capacidad del compilador para determinar estáticamente eso. Cada vez que causa una inferencia de tipo, genera automáticamente una nueva captura de <? extends Number>, y dos capturas no se consideran equivalentes.

Por lo tanto, si elimina la inferencia de la invocación de singletonList especificando <T> para ello:

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

Funciona bien. El código generado no es diferente de si su llamada hubiera sido legal, es solo una limitación de la compilador que no puede averiguar eso por sí solo.

La regla de que una inferencia crea una captura y las capturas no son compatibles es lo que impide que este ejemplo de tutorial se compile y luego explote en tiempo de ejecución:

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

Sí, la especificación del lenguaje y el compilador probablemente podrían hacerse más sofisticados para distinguir su ejemplo de eso, pero no lo es y es lo suficientemente simple como para evitarlo.

 8
Author: Affe,
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
2013-05-08 22:22:25

Tal vez esto pueda explicar el problema del compilador:

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

Esta lista de comodines genric puede contener cualquier elemento que se extienda desde Number. Así que está bien asignarle una lista entera. Sin embargo, ahora podría agregar un Doble a myNums porque el Doble también se extiende desde el Número, lo que llevaría a un problema de tiempo de ejecución. Por lo tanto, el compilador prohíbe todos los accesos de escritura a myNums y solo puedo usar métodos de lectura en él, porque solo sé lo que consigo se puede convertir en Número.

Y así el el compilador se queja de muchas cosas que puede hacer con un comodín genérico. A veces él está loco por las cosas que usted puede asegurarse de que están a salvo y bien.

Pero afortunadamente hay un truco para evitar este error para que pueda probar por su cuenta lo que tal vez puede romper esto:

public static void main(String[] args) {

    List<? extends Number> list1 = new ArrayList<BigDecimal>();
    List<List<? extends Number>> list2 = copyHelper(list1);


}

private static <T> List<List<T>> copyHelper(List<T> list) {
    return Collections.singletonList(list);

}
 0
Author: mszalbach,
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
2013-05-08 21:15:23

La razón es que el compilador no sabe que sus tipos comodín son del mismo tipo .

Tampoco sabe que tu instancia es null. Aunque null es un miembro de todos los tipos, el compilador considera solo los tipos declarados, no lo que el valor de la variable podría contener, cuando se verifica el tipo.

Si el código se ejecuta, no causaría una excepción, pero eso es solo porque el valor es null. Todavía existe un posible desajuste de tipo, y eso es lo que el trabajo del compilador es-para no permitir los desajustes de tipo.

 0
Author: Bohemian,
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
2013-05-08 21:19:11

Echa un vistazo a type erasure. El problema es que el "tiempo de compilación" es la única oportunidad que Java tiene para hacer cumplir estos genéricos, por lo que si lo deja pasar no sería capaz de decir si intentó insertar algo inválido. Esto es realmente algo bueno, porque significa que una vez que el programa compila, los genéricos no incurren en ninguna penalización de rendimiento en tiempo de ejecución.

Vamos a tratar de ver su ejemplo de otra manera (vamos a usar dos tipos que extienden el Número pero se comportan muy diferente). Considere el siguiente programa:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<BigDecimal>> l = Collections.singletonList(t);
    for(q16449799<BigDecimal> i : l) {
      System.out.println(i.val);
    }
  }
}

Esto produce (como uno esperaría):

3.141592653589793115997963468544185161590576171875

Ahora asumiendo que el código que presentaste no causó un error de compilador:

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<AtomicLong>> l = Collections.singletonList(t);
    for(q16449799<AtomicLong> i : l) {
      System.out.println(i.val);
    }
  }
}

¿Qué esperarías que fuera la salida? No puedes razonablemente lanzar un BigDecimal a un AtomicLong (podrías construir un AtomicLong a partir del valor de un BigDecimal, pero el casting y la construcción son cosas diferentes y los genéricos se implementan como sugar en tiempo de compilación para asegurarte de que los casts sean exitoso). En cuanto al comentario de @KennyTM, se busca un tipo concreto cuando inicias el ejemplo, pero intenta compilar esto:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T> {
  public T val;

  public static void main(String ... args) {
    q16449799<? extends Number> t = new q16449799<BigDecimal>();

    t.val = new BigDecimal(Math.PI);

    List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
    for(q16449799<? extends Number> i : l) {
      System.out.println(i.val);
    }
  }
}

Esto se producirá un error en el momento en que intente establecer un valor en t.val.

 0
Author: Jason Sperske,
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
2013-05-08 21:49:46