comportamiento genérico diferente cuando se utiliza lambda en lugar de la clase interna anónima explícita

Quaffel:

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 TypeTokenbásicamente hace

Esta es una versión muy simplificada de TypeTokenlo 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, typees una GenericArrayTypecelebración de una TypeVariablecomo su tipo de componente. Esto es perfectamente normal.

La situación extraña cuando se utiliza lambdas

Sin embargo, cuando se inicializa un TypeTokeninterior de una expresión lambda, las cosas empiezan a cambiar. (La variable de tipo viene de la testfunción anterior)

Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};

En este caso, typesigue siendo una GenericArrayType, pero se mantiene nullcomo 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

  1. ¿Qué ocurre con la TypeVariable en el lambda-ejemplo? ¿Por qué la inferencia de tipos no respeta el tipo genérico?
  2. ¿Cuál es la diferencia entre el explícitamente declarado y el ejemplo declarada implícitamente-? Es la inferencia de tipos, la única diferencia?
  3. ¿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 Fi0significar 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 nullen 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 8u191antes). 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.

Mike Strobel:

tldr:

  1. Hay un error en el javacque 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.
  2. Puede afirmarse que hay dos tipos de errores en la java.lang.reflectimplementació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 propagan NullPointerExceptioncuando 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 Signatureatributo 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.

javacemitirá 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.reflectionAPIs pueden analizar la información genérica sobre su supertipo clase (anónimo).

Pero ya sabemos que esto funciona muy bien cuando la TypeTokenestá 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.reflectAPIs 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 ClassRepositoryconsigue 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))

ClassScopees 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 computeEnclosingScopeproducirá una MethodScopepara el método genérico static <F> void test(). Dado que testdeclara 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 invokedynamicinstrucción se emite, lo que provoca una TypeTokenclase 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$1está 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 EnclosingMethodse 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 Wque 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$1no 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 nullconseguir 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 EnclosingMethodatributo") establece lo siguiente:

Es la responsabilidad de un compilador de Java para asegurar que el método identificado a través de la method_indexes de hecho el más cercano léxico que encierra método de la clase que contiene este EnclosingMethodatributo. (énfasis mío)

Actualmente, javacparece determinar el método que encierra después de la re-escritura lambda sigue su curso, y como resultado, el EnclosingMethodatributo se refiere a un método que ni siquiera existía en el ámbito léxico. Si EnclosingMethodinformado 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 nullargumento 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 EnclosingMethodcuestión está ahora en línea.

(ii) Puede afirmarse que hay varios errores en java.lang.reflecty su API de apoyo.

El método ParameterizedType::getActualTypeArguments()se documenta como que lanza una TypeNotPresentExceptioncuando "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 TypeNotPresentExceptionbajo ninguna circunstancia.

También me gustaría argumentar que las diversas Type::toStringsustituciones 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.

Supongo que te gusta

Origin http://43.154.161.224:23101/article/api/json?id=37800&siteId=1
Recomendado
Clasificación