El contexto
Estoy trabajando en un proyecto que depende en gran medida de los tipos genéricos. Una de sus principales componentes es el llamado TypeToken
, lo que proporciona una manera de representar los tipos genéricos en tiempo de ejecución y la aplicación de algunas funciones de utilidad en ellos. Para evitar el borrado Tipo de Java, estoy usando la notación de corchetes ( {}
) para crear una subclase genera automáticamente ya que esto hace que el tipo reifiable.
Lo que TypeToken
básicamente hace
Esta es una versión muy simplificada de TypeToken
lo que es mucho más indulgente que la implementación original. Sin embargo, estoy usando este enfoque para que pueda asegurarse de que el problema real no está en una de esas funciones de utilidad.
public class TypeToken<T> {
private final Type type;
private final Class<T> rawType;
private final int hashCode;
/* ==== Constructor ==== */
@SuppressWarnings("unchecked")
protected TypeToken() {
ParameterizedType paramType = (ParameterizedType) this.getClass().getGenericSuperclass();
this.type = paramType.getActualTypeArguments()[0];
// ...
}
Cuando funciona
Básicamente, esta aplicación funciona perfectamente en casi todas las situaciones. No tiene ningún problema con el manejo de la mayoría de los tipos. Los siguientes ejemplos funcionan a la perfección:
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};
Ya que no comprueba los tipos, la implementación anterior permite todo tipo que los permisos del compilador, incluyendo TypeVariables.
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}
En este caso, type
es una GenericArrayType
celebración de una TypeVariable
como su tipo de componente. Esto es perfectamente normal.
La situación extraña cuando se utiliza lambdas
Sin embargo, cuando se inicializa un TypeToken
interior de una expresión lambda, las cosas empiezan a cambiar. (La variable de tipo viene de la test
función anterior)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};
En este caso, type
sigue siendo una GenericArrayType
, pero se mantiene null
como su tipo de componente.
Pero si va a crear una clase interna anónima, las cosas comienzan a cambiar de nuevo:
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};
En este caso, el tipo de componente de nuevo tiene el valor correcto (TypeVariable)
Las preguntas resultantes
- ¿Qué ocurre con la TypeVariable en el lambda-ejemplo? ¿Por qué la inferencia de tipos no respeta el tipo genérico?
- ¿Cuál es la diferencia entre el explícitamente declarado y el ejemplo declarada implícitamente-? Es la inferencia de tipos, la única diferencia?
- ¿Cómo puedo solucionar este problema sin necesidad de utilizar la declaración explícita repetitivo? Esto es especialmente importante en las pruebas unitarias ya que quiero comprobar si el constructor lanza excepciones o no.
Para aclarar un poco: Este no es un problema que es "relevante" para el programa ya que no permita que los tipos no resoluble en absoluto, pero sigue siendo un fenómeno interesante que me gustaría entender.
Mi investigación
actualización 1
Mientras tanto, he hecho algunas investigaciones sobre este tema. En el lenguaje Java Especificación §15.12.2.2 he encontrado una expresión que podría tener algo que ver con ella - "pertinente a la aplicabilidad", mencionando "expresión lambda implícitamente escrito" como una excepción. Obviamente, es el capítulo incorrecto, pero la expresión se utiliza en otros lugares, incluyendo el capítulo sobre la inferencia de tipos.
Pero para ser honesto: En realidad no he descubierto aún lo que todos los operadores como :=
o Fi0
significar lo que hace que sea muy difícil de entender en detalle. Estaría alegre si alguien podría aclarar esto un poco y si esta podría ser la explicación del comportamiento extraño.
actualización 2
He pensado de ese enfoque nuevo y llegué a la conclusión de que, incluso si el compilador eliminaría el tipo ya que no es "pertinente a la aplicabilidad", que no justifica para establecer el tipo de componente que null
en lugar del tipo más generoso, Objeto. No puedo pensar en una sola razón por la cual los diseñadores del lenguaje decidieron hacerlo.
actualización 3
Acabo a realizar la prueba el mismo código con la última versión de Java (he usado 8u191
antes). Muy a mi pesar, esto no ha cambiado nada, aunque la inferencia de tipos de Java ha sido mejorada ...
Update 4
He solicitado una entrada en la base de datos oficial Bug Java / Rastreador hace unos días y que acaba de ser aceptada. Dado que los desarrolladores que revisaron mi informe asigna el P4 prioridad al fallo, puede ser que tome un tiempo hasta que lo arreglará. Puede encontrar el informe aquí .
Un enorme Shoutout a Tom Hawtin - tackline de mencionar que esto podría ser un error esencial en el propio Java SE. Sin embargo, un informe de Mike Strobel probablemente sería manera más detallada que la mía debido a su impresionante conocimiento de fondo. Sin embargo, cuando escribí el informe, la respuesta de Strobel aún no estaba disponible.
tldr:
- Hay un error en el
javac
que registra el método de cerramiento equivocada para las clases internas lambda-incrustado. Como resultado, las variables de tipo en el actual método de cerramiento no pueden ser resueltos por esos clases internas.- Puede afirmarse que hay dos tipos de errores en la
java.lang.reflect
implementación de la API:
- Algunos métodos se documentan como lanzar excepciones cuando se encuentran tipos inexistentes, pero nunca lo hacen. En su lugar, permiten referencias nulas se propaguen.
- Las diversas
Type::toString()
modificaciones de la actualidad tiran o se propaganNullPointerException
cuando un tipo no se puede resolver.
La respuesta tiene que ver con las firmas genéricas que consiguen generalmente emiten en archivos de clase que hacen uso de los genéricos.
Por lo general, cuando se escribe una clase que tiene una o más genéricos supertipos, el compilador de Java emitirá un Signature
atributo que contiene la firma genérica totalmente parametrizado (s) de supertipo de la clase (s). He escrito sobre esto antes , pero la corta explicación es la siguiente: sin ellos, no sería posible consumir los tipos genéricos como tipos genéricos a menos que pasó a tener el código fuente. Debido al tipo de cancelación, la información sobre las variables de tipo se pierde en tiempo de compilación. Si esa información no se incluyeron como metadatos adicionales, ni el IDE ni su compilador sabrían que era un tipo genérico, y no se puede utilizar como tal. Tampoco podía el compilador emita los cheques de tiempo de ejecución necesarios para hacer cumplir la seguridad de tipos.
javac
emitirá metadatos firma genérica para cualquier tipo o método cuya firma contiene variables de tipo o de un tipo parametrizado, por lo que usted es capaz de obtener la información genérica supertipo original para sus tipos anónimos. Por ejemplo, el tipo anónimo creó aquí:
TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
... contiene este Signature
:
LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
A partir de esto, las java.lang.reflection
APIs pueden analizar la información genérica sobre su supertipo clase (anónimo).
Pero ya sabemos que esto funciona muy bien cuando la TypeToken
está parametrizado con tipos concretos. Echemos un vistazo a un ejemplo más relevante, donde su parámetro de tipo incluye una variable de tipo :
static <F> void test() {
TypeToken sup = new TypeToken<F[]>() {};
}
A continuación, se obtiene la siguiente firma:
LTypeToken<[TF;>;
Tiene sentido, ¿verdad? Ahora, Echemos un vistazo a cómo las java.lang.reflect
APIs son capaces de extraer información supertipo genérica de estas firmas. Si nos asomamos Class::getGenericSuperclass()
, vemos que lo primero que hace es llamada getGenericInfo()
. Si no hemos puesto en este método antes, una ClassRepository
consigue instanciado:
private ClassRepository getGenericInfo() {
ClassRepository genericInfo = this.genericInfo;
if (genericInfo == null) {
String signature = getGenericSignature0();
if (signature == null) {
genericInfo = ClassRepository.NONE;
} else {
// !!! RELEVANT LINE HERE: !!!
genericInfo = ClassRepository.make(signature, getFactory());
}
this.genericInfo = genericInfo;
}
return (genericInfo != ClassRepository.NONE) ? genericInfo : null;
}
La pieza fundamental aquí es la llamada a la getFactory()
que se expande a:
CoreReflectionFactory.make(this, ClassScope.make(this))
ClassScope
es la parte que nos importa: esto proporciona un marco para la resolución de las variables de tipo. Dado un nombre de variable de tipo, el alcance se buscó una variable de tipo juego. Si uno no se encuentra, el 'exterior' o encerrando alcance se busca:
public TypeVariable<?> lookup(String name) {
TypeVariable<?>[] tas = getRecvr().getTypeParameters();
for (TypeVariable<?> tv : tas) {
if (tv.getName().equals(name)) {return tv;}
}
return getEnclosingScope().lookup(name);
}
Y, por último, la clave de todo (desde ClassScope
):
protected Scope computeEnclosingScope() {
Class<?> receiver = getRecvr();
Method m = receiver.getEnclosingMethod();
if (m != null)
// Receiver is a local or anonymous class enclosed in a method.
return MethodScope.make(m);
// ...
}
Si (por ejemplo, una variable de tipo F
) no se encuentra en la clase en sí (por ejemplo, el anonimato TypeToken<F[]>
), entonces el siguiente paso es buscar el método de cerramiento . Si nos fijamos en la clase anónima desmontado, vemos este atributo:
EnclosingMethod: LambdaTest.test()V
La presencia de este atributo significa que computeEnclosingScope
producirá una MethodScope
para el método genérico static <F> void test()
. Dado que test
declara la variable tipo W
, lo encontramos cuando buscamos el ámbito de inclusión.
Así que, ¿por qué no funciona dentro de un lambda?
Para responder a esto, debemos entender cómo se compilan lambdas. El cuerpo de la lambda se movió en un método estático sintético. En el punto donde declaramos nuestra lambda, una invokedynamic
instrucción se emite, lo que provoca una TypeToken
clase de implementación que se genera la primera vez que llegamos a la instrucción.
En este ejemplo, el método estático generado por el cuerpo lambda sería algo como esto (si descompilada):
private static /* synthetic */ Object lambda$test$0() {
return new LambdaTest$1();
}
... dónde LambdaTest$1
está su clase anónima. Dissassemble de que e inspeccionar nuestros atributos Let:
Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
Al igual que el caso en el que crea una instancia de un tipo anónimo fuera de un lambda, la firma contiene la variable tipo W
. Pero EnclosingMethod
se refiere al método de síntesis .
El método sintético lambda$test$0()
no declara variable de tipo W
. Por otra parte, lambda$test$0()
no está encerrado por test()
, por lo que la declaración de W
que no es visible en su interior. Su clase anónima tiene un supertipo que contiene una variable de tipo de que su clase no sabe nada porque está fuera de alcance.
Cuando llamamos getGenericSuperclass()
, la jerarquía de posibilidades de LambdaTest$1
no contiene W
, por lo que el analizador no puede resolverlo. Debido a la forma en que el código está escrito, sin resolver este tipo de resultados variables en null
conseguir colocados en los parámetros de tipo de supertipo genérico.
Tenga en cuenta que, tenía su lambda había una instancia de un tipo que no no hace referencia a ningún tipo de variables (por ejemplo, TypeToken<String>
) entonces usted no se encuentra con este problema.
conclusiones
(i) Existe un error en javac
. La Especificación de la Máquina Virtual Java §4.7.7 ( "El EnclosingMethod
atributo") establece lo siguiente:
Es la responsabilidad de un compilador de Java para asegurar que el método identificado a través de la
method_index
es de hecho el más cercano léxico que encierra método de la clase que contiene esteEnclosingMethod
atributo. (énfasis mío)
Actualmente, javac
parece determinar el método que encierra después de la re-escritura lambda sigue su curso, y como resultado, el EnclosingMethod
atributo se refiere a un método que ni siquiera existía en el ámbito léxico. Si EnclosingMethod
informado de la actual método léxico que encierra, las variables de tipo de método que podrían ser resueltos por las clases lambda-incrustado, y su código producirían los resultados esperados.
Podría decirse que es también un error que el analizador de la firma / reifier silencio permite que un null
argumento de tipo que se propaga en un ParameterizedType
(que, como @ puntos tom-Hawtin-tackline a cabo, tiene efectos secundarios como toString()
tirar una NPE).
Mi informe de error para la EnclosingMethod
cuestión está ahora en línea.
(ii) Puede afirmarse que hay varios errores en java.lang.reflect
y su API de apoyo.
El método ParameterizedType::getActualTypeArguments()
se documenta como que lanza una TypeNotPresentException
cuando "cualquiera de los argumentos de tipo real se refiere a una declaración de tipo inexistente". Esa descripción podría decirse que cubre el caso en el que una variable de tipo no está en su alcance. GenericArrayType::getGenericComponentType()
debe lanzar una excepción similar al "tipo del tipo de matriz subyacente se refiere a una declaración de tipo inexistente". Actualmente, ni parece lanzar un TypeNotPresentException
bajo ninguna circunstancia.
También me gustaría argumentar que las diversas Type::toString
sustituciones deben simplemente rellenar el nombre canónico de cualquier tipo sin resolver en lugar de tirar una NPE o cualquier otra excepción.
He presentado un informe de error para estos temas relacionados con la reflexión-y voy a publicar el enlace una vez que es visible para el público.
Soluciones provisionales?
Si tiene que ser capaz de hacer referencia a un tipo de variable declarada por el método que encierra, entonces no se puede hacer eso con una lambda; que tendrá que recurrir a la sintaxis ya tipo anónimo. Sin embargo, la versión lambda debería funcionar en la mayoría de los otros casos. Usted debería ser capaz de variables de tipo referencia declarada por el cerramiento de clase . Por ejemplo, estos siempre deben trabajar:
class Test<X> {
void test() {
Supplier<TypeToken<X>> s1 = () -> new TypeToken<X>() {};
Supplier<TypeToken<String>> s2 = () -> new TypeToken<String>() {};
Supplier<TypeToken<List<String>>> s3 = () -> new TypeToken<List<String>>() {};
}
}
Desafortunadamente, dado que este error aparentemente ha existido desde lambdas se introdujo por primera vez, y no se ha solucionado en la última versión LTS, es posible que tenga que asumir los restos de errores en los JDK de sus clientes mucho después de que se fija, en el supuesto que se pone fijo en absoluto.