java - interfaces - Problemas para entender los límites inferiores cuando se utiliza con lambda y la interfaz funcional
java 8 lambda explicacion (2)
Acabo de probarlo, la tarea en sí compila. Lo que cambia es si realmente puede llamar a predicate.test()
.
Retrocedamos un poco y usemos un marcador de posición GenericClass<T>
para una explicación. Para los argumentos de tipo, Foo
amplía Bar
y Bar
extiende Baz
.
Extiende: cuando declaras una GenericClass<? extends Bar>
GenericClass<? extends Bar>
, está diciendo "No sé cuál es realmente su argumento de tipo genérico, pero es una subclase de Bar
". La instancia real siempre tendrá un argumento de tipo no comodín, pero en esta parte del código no se sabe cuál es su valor. Ahora considere lo que eso significa para las invocaciones de métodos.
Sabes que lo que GenericClass<Foo>
realidad es una GenericClass<Foo>
o una GenericClass<Bar>
. Considere un método que devuelve T
En el primer caso, su tipo de devolución es Foo
. En este último, Bar
. De cualquier forma, es un subtipo de Bar
y es seguro asignarlo a una variable de Bar
.
Considere un método que tiene un parámetro T
Si es un GenericClass<Foo>
, pasarle una Bar
es un error - Bar
no es un subtipo de Foo
.
Entonces, con un límite superior puede usar valores de retorno genéricos, pero no parámetros de método genéricos.
Súper: cuando declaras una GenericClass<? super Bar>
GenericClass<? super Bar>
, estás diciendo "No sé cuál es realmente su argumento de tipo genérico, pero es una superclase de Bar
". Ahora considere lo que eso significa para las invocaciones de métodos.
Sabes que lo que tienes en realidad es una GenericClass<Bar>
o una GenericClass<Baz>
. Considere un método que devuelve T
En el primer caso, devuelve Bar
. En este último, Baz
. Si devuelve un Baz
, entonces asignar ese valor a una variable de Bar
es un error. No sabes cuál es, por lo que no puedes asumir nada seguro aquí.
Considere un método que tiene un parámetro T
Si es una GenericClass<Bar>
, pasarle una Bar
es legal. Si es una GenericClass<Baz>
, pasarle una Bar
sigue siendo legal porque Bar
es un subtipo de Baz
.
Por lo tanto, con un límite inferior puede usar parámetros de método genéricos, pero no valores de retorno genéricos.
En resumen: <? extends T>
<? extends T>
significa que puede usar valores de retorno genéricos pero no parámetros. <? super T>
<? super T>
significa que puede usar parámetros genéricos pero no devolver valores. Predicate.test()
tiene un parámetro genérico, por lo que necesita super
.
Otra cosa a considerar: los límites indicados por un comodín son sobre el argumento de tipo real del objeto. Sus consecuencias sobre los tipos que puedes usar con ese objeto son todo lo contrario. Un comodín de límite superior ( extends
) es un límite inferior en los tipos de variables a las que puede asignar valores devueltos. Un comodín de límite inferior ( super
) es un límite superior en los tipos que puede pasar como parámetros. predicate.test(new Object())
no compilará porque, con un límite inferior de String
, solo aceptará subclases de String
.
Mientras estudiaba en Java8 Streams, me encontré con el siguiente fragmento de código:
Predicate<? super String> predicate = s -> s.startsWith("g");
Como el parámetro genérico es un límite inferior, pensé que esto no se compilaría. De la forma en que lo veo, si un Objeto es un supertipo para una Cadena, entonces pasar un Tipo de Objeto debería romperlo, ya que el Objeto no tiene una función startsWith (). Sin embargo, me sorprendió ver que funcionaba sin ningún problema.
Aún más, cuando modifiqué el predicado para tomar un límite superior:
<? extends String>,
no compilaría
Pensé que entendía el significado de los límites superior e inferior, pero obviamente me falta algo. ¿Alguien puede ayudar a explicar por qué el límite inferior funciona con esta lambda?
El tipo de argumento Lambda es exacto, no puede ser ? super
? super
o ? extends
? extends
. Esto está cubierto por JLS 15.27.3. Tipo de una expresión Lambda . Introduce el concepto de tipo de objetivo de tierra (que es básicamente el tipo de lambda). Entre otras cosas, se afirma que:
Si
T
es un tipo de interfaz funcional con parámetro comodín y la expresión lambda está implícitamente tipada, entonces el tipo de objetivo de tierra es la parametrización sin comodín ( §9.9 ) de T.
Énfasis mío Entonces esencialmente cuando escribes
Predicate<? super String> predicate = s -> s.startsWith("g");
Su tipo lambda es Predicate<String>
. Es lo mismo que:
Predicate<? super String> predicate = (Predicate<String>)(s -> s.startsWith("g"));
O incluso
Predicate<String> pred = (Predicate<String>)(s -> s.startsWith("g"));
Predicate<? super String> predicate = pred;
Dado el hecho de que los argumentos del tipo lambdas son concretos, después de eso se aplican las reglas de conversión de tipo normal: Predicate<String>
es un Predicate<? super String>
Predicate<? super String>
, o Predicate<? extends String>
Predicate<? extends String>
. Entonces ambos Predicate<? super String>
Predicate<? super String>
y Predicate<? extends String>
Predicate<? extends String>
debe compilarse. Y ambos realmente funcionan para mí en javac 8u25, 8u45, 8u71 y ecj 3.11.1.