studio - retornar un objeto en java
¿Por qué este método genérico con un límite puede devolver algún tipo? (1)
¿Por qué se compila el siguiente código?
El método
IElement.getX(String)
devuelve una instancia del tipo
IElement
o de sus subclases.
El código en la clase
Main
invoca el
getX(String)
.
El compilador permite almacenar el valor de retorno en una variable del tipo
Integer
(que obviamente no está en la jerarquía de
IElement
).
public interface IElement extends CharSequence {
<T extends IElement> T getX(String value);
}
public class Main {
public void example(IElement element) {
Integer x = element.getX("x");
}
}
¿No debería ser el tipo de retorno una instancia de
IElement
, incluso después del borrado del tipo?
El bytecode del método
getX(String)
es:
public abstract <T extends IElement> T getX(java.lang.String);
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #7 // <T::LIElement;>(Ljava/lang/String;)TT;
Editar:
String
reemplazada de manera consistente con
Integer
.
Esto es realmente una inferencia de tipo legítima *.
Podemos reducir esto al siguiente ejemplo ( Ideone ):
interface Foo {
<F extends Foo> F bar();
public static void main(String[] args) {
Foo foo = null;
String baz = foo.bar();
}
}
El compilador puede inferir un tipo de intersección (sin sentido, realmente)
String & Foo
porque
Foo
es una interfaz.
Para el ejemplo en la pregunta, se infiere
Integer & IElement
.
No tiene sentido porque la conversión es imposible. No podemos hacer tal elenco nosotros mismos:
// won''t compile because Integer is final
Integer x = (Integer & IElement) element;
La inferencia de tipos básicamente funciona con:
- un conjunto de variables de inferencia para cada uno de los parámetros de tipo de un método.
- Un conjunto de límites a los que debe ajustarse.
- a veces restricciones , que se reducen a límites.
Al final del algoritmo, cada variable se resuelve en un tipo de intersección basado en el conjunto enlazado, y si son válidas, la invocación se compila.
El proceso comienza en 8.1.3 :
Cuando comienza la inferencia, generalmente se genera un conjunto enlazado a partir de una lista de declaraciones de parámetros de tipo
P 1 , ..., P p
y las variables de inferencia asociadasα 1 , ..., α p
. Tal conjunto enlazado se construye de la siguiente manera. Para cada l (1 ≤ l ≤ p) :
[...]
De lo contrario, para cada tipo
T
delimitado por&
en un TypeBound , el límiteα l <: T[P 1 :=α 1 , ..., P p :=α p ]
aparece en el conjunto [...].
Entonces, esto significa primero que el compilador comienza con un límite de
F <: Foo
(lo que significa que
F
es un subtipo de
Foo
).
Pasando a 18.5.2 , se considera el tipo de objetivo de retorno:
Si la invocación es una expresión poli, […] deje que
R
sea el tipo de retorno dem
,T
sea el tipo de destino de la invocación y luego:
[...]
De lo contrario, la fórmula de restricción
‹R θ → T›
se reduce e incorpora con [el conjunto enlazado].
La fórmula de restricción
‹R θ → T›
se reduce a otro límite de
R θ <: T
, por lo que tenemos
F <: String
.
Más tarde, estos se resuelven de acuerdo con 18.4 :
[…] Se define una instanciación candidata
T i
para cadaα i
:
- De lo contrario, donde
α i
tiene límites superiores adecuadosU 1 , ..., U k
,T i = glb(U 1 , ..., U k )
.Los límites
α 1 = T 1 , ..., α n = T n
se incorporan con el conjunto de límites actual.
Recuerde que nuestro conjunto de límites es
F <: Foo, F <: String
.
glb(String, Foo)
se define como
String & Foo
.
Aparentemente, este es un tipo legítimo para
glb
, que solo requiere que:
Es un error en tiempo de compilación si, para cualquiera de las dos clases ( no interfaces )
V i
yV j
,V i
no es una subclase deV j
o viceversa.
Finalmente:
Si la resolución tiene éxito con las instancias
T 1 , ..., T p
para las variables de inferenciaα 1 , ..., α p
, deje que beθ''
sea la sustitución[P 1 :=T 1 , ..., P p :=T p ]
. Entonces:
- Si la conversión no verificada no era necesaria para que el método fuera aplicable, entonces el tipo de invocación de
m
se obtiene aplicandoθ''
al tipo dem
.
Por lo tanto, el método se invoca con
String & Foo
como el tipo de
F
Por supuesto, podemos asignar esto a una
String
, convirtiendo así de manera imposible un
Foo
en una
String
.
El hecho de que
String
/
Integer
son clases finales aparentemente no se considera.
* Nota: el borrado de tipo no está / estaba completamente relacionado con el problema.
Además, aunque esto también se compila en Java 7, creo que es razonable decir que no debemos preocuparnos por la especificación allí. La inferencia de tipos de Java 7 era esencialmente una versión menos sofisticada de Java 8. Se compila por razones similares.
Como addendum, aunque extraño, esto probablemente nunca causará un problema que aún no estaba presente.
Raramente es útil escribir un método genérico cuyo tipo de retorno se infiera únicamente del objetivo de retorno, porque solo se puede devolver
null
desde dicho método sin conversión.
Supongamos, por ejemplo, que tenemos un mapa analógico que almacena subtipos de una interfaz particular:
interface FooImplMap {
void put(String key, Foo value);
<F extends Foo> F get(String key);
}
class Bar implements Foo {}
class Biz implements Foo {}
Ya es perfectamente válido cometer un error como el siguiente:
FooImplMap m = ...;
m.put("b", new Bar());
Biz b = m.get("b"); // casting Bar to Biz
Entonces, el hecho de que
también
podemos hacer
Integer i = m.get("b");
No es una
nueva
posibilidad de error.
Si estuviéramos programando un código como este, para empezar ya era potencialmente poco sólido.
En general, un parámetro de tipo solo debe inferirse únicamente del tipo de destino si no hay ningún motivo para vincularlo, por ejemplo,
Collections.emptyList()
y
Optional.empty()
:
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
Esto es A-OK porque
Optional.empty()
no puede producir ni consumir una
T