Je travaille sur un projet qui dépend fortement des types génériques. Un de ses composants clés est le soi-disant TypeToken
, qui fournit un moyen de représenter les types génériques au moment de l'exécution et de leur appliquer certaines fonctions utilitaires. Pour éviter l'effacement de type de Java, j'utilise la notation des accolades ({}
) pour créer une sous-classe générée automatiquement car cela rend le type réifiable.
TypeToken
fait essentiellementIl s'agit d'une version fortement simplifiée de TypeToken
qui est beaucoup plus clémente que l'implémentation d'origine. Cependant, j'utilise cette approche afin que je puisse m'assurer que le vrai problème ne réside pas dans l'une de ces fonctions utilitaires.
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];
// ...
}
Fondamentalement, cette implémentation fonctionne parfaitement dans presque toutes les situations. Il n'a aucun problème avec la gestion de la plupart des types. Les exemples suivants fonctionnent parfaitement:
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};
Comme elle ne vérifie pas les types, l'implémentation ci-dessus autorise tous les types autorisés par le compilateur, y compris TypeVariables.
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}
Dans ce cas, type
est un GenericArrayType
contenant un TypeVariable
comme type de composant. C'est parfaitement bien.
Cependant, lorsque vous initialisez un TypeToken
à l'intérieur d'une expression lambda, les choses commencent à changer. (La variable type provient de la fonction test
ci-dessus)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};
Dans ce cas, type
est toujours un GenericArrayType
, mais il contient null
comme type de composant.
Mais si vous créez une classe interne anonyme, les choses recommencent à changer:
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};
Dans ce cas, le type de composant contient à nouveau la valeur correcte (TypeVariable)
Pour clarifier un peu: ce n'est pas un problème qui est "pertinent" pour le programme car je n'autorise PAS du tout les types non résolvables, mais c'est quand même un phénomène intéressant que j'aimerais comprendre.
En attendant, j'ai fait quelques recherches sur ce sujet. Dans la spécification du langage Java §15.12.2.2 , j'ai trouvé une expression qui pourrait avoir quelque chose à voir avec cela - "pertinente pour l'applicabilité", mentionnant "l'expression lambda typée implicitement" comme exception. Évidemment, c'est le chapitre incorrect, mais l'expression est utilisée à d'autres endroits, y compris le chapitre sur l'inférence de type.
Mais pour être honnête: je n'ai pas encore vraiment compris ce que tous ces opérateurs aiment :=
ou Fi0
signifie ce qui rend vraiment difficile de le comprendre en détail. Je serais heureux si quelqu'un pouvait clarifier un peu cela et si cela pouvait être l'explication du comportement étrange.
J'ai repensé à cette approche et je suis arrivé à la conclusion que même si le compilateur supprimait le type car il n'est pas "pertinent pour l'applicabilité", il ne justifie pas de définir le type de composant sur null
à la place du type le plus généreux, Object. Je ne peux pas penser à une seule raison pour laquelle les concepteurs de langage ont décidé de le faire.
Je viens de retester le même code avec la dernière version de Java (J'ai utilisé 8u191
avant). À mon grand regret, cela n'a rien changé, bien que l'inférence de type de Java ait été améliorée ...
J'ai demandé une entrée dans l'officiel Java Bug Database/Tracker il y a quelques jours et cela vient d'être accepté. Comme les développeurs qui ont examiné mon rapport ont attribué la priorité P4 au bogue, il se peut prenez un certain temps jusqu'à ce qu'il soit corrigé. Vous pouvez trouver le rapport ici .
Un grand merci à Tom Hawtin - une ligne de mire pour avoir mentionné que cela pourrait être un bogue essentiel dans le Java SE lui-même. Cependant, un rapport de Mike Strobel serait probablement beaucoup plus détaillé que le mien en raison de son Cependant, lorsque j'ai rédigé le rapport, la réponse de Strobel n'était pas encore disponible.
tldr:
- Il y a un bogue dans
javac
qui enregistre la mauvaise méthode de fermeture pour les classes internes intégrées à lambda. Par conséquent, les variables de type de la méthode englobante actual ne peuvent pas être résolues par ces classes internes.- Il existe sans doute deux ensembles de bogues dans l'implémentation de l'API
Java.lang.reflect
:
- Certaines méthodes sont documentées comme levant des exceptions lorsque des types inexistants sont rencontrés, mais elles ne le font jamais. Au lieu de cela, ils permettent aux références nulles de se propager.
- Les diverses
Type::toString()
remplacent actuellement lancent ou propagent unNullPointerException
lorsqu'un type ne peut pas être résolu.
La réponse a à voir avec les signatures génériques qui sont généralement émises dans les fichiers de classe qui utilisent des génériques.
En règle générale, lorsque vous écrivez une classe qui possède un ou plusieurs supertypes génériques, le compilateur Java émet un attribut Signature
contenant la ou les signatures génériques entièrement paramétrées du supertype de la classe (s). J'ai écrit à ce sujet avant , mais la courte explication est la suivante: sans eux, il ne serait pas possible de consommer des types génériques en tant que types génériques sauf si vous aviez justement le code source. En raison de l'effacement de type, les informations sur les variables de type sont perdues au moment de la compilation. Si ces informations n'étaient pas incluses en tant que métadonnées supplémentaires, ni le IDE ni votre compilateur ne sauraient qu'un type était générique et vous ne pourriez pas l'utiliser comme tel. Le compilateur ne pourrait pas non plus émettre le runtime nécessaire vérifie pour assurer la sécurité du type.
javac
émettra des métadonnées de signature générique pour tout type ou méthode dont la signature contient des variables de type ou un type paramétré, c'est pourquoi vous pouvez obtenir les informations génériques de supertype d'origine pour vos types anonymes. Par exemple, le type anonyme créé ici:
TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
... contient ce Signature
:
LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
À partir de cela, les API Java.lang.reflection
Peuvent analyser les informations génériques de supertype sur votre classe (anonyme).
Mais nous savons déjà que cela fonctionne très bien lorsque le TypeToken
est paramétré avec des types concrets. Regardons un exemple plus pertinent, où son paramètre type inclut une variable type:
static <F> void test() {
TypeToken sup = new TypeToken<F[]>() {};
}
Ici, nous obtenons la signature suivante:
LTypeToken<[TF;>;
C'est logique, non? Voyons maintenant comment les API Java.lang.reflect
Sont capables d'extraire des informations génériques de supertype de ces signatures. Si nous regardons dans Class::getGenericSuperclass()
, nous voyons que la première chose qu'il fait est d'appeler getGenericInfo()
. Si nous n'avons pas encore appelé cette méthode, un ClassRepository
est instancié:
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 pièce critique ici est l'appel à getFactory()
, qui se développe en:
CoreReflectionFactory.make(this, ClassScope.make(this))
ClassScope
est le bit qui nous intéresse: cela fournit une portée de résolution pour les variables de type. Étant donné un nom de variable de type, la portée est recherchée pour une variable de type correspondante. Si aucun n'est trouvé, la portée 'externe' ou entourant est recherchée:
public TypeVariable<?> lookup(String name) {
TypeVariable<?>[] tas = getRecvr().getTypeParameters();
for (TypeVariable<?> tv : tas) {
if (tv.getName().equals(name)) {return tv;}
}
return getEnclosingScope().lookup(name);
}
Et, enfin, la clé de tout (de 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 une variable de type (par exemple, F
) n'est pas trouvée sur la classe elle-même (par exemple, l'anonyme TypeToken<F[]>
), Alors l'étape suivante consiste à rechercher le méthode englobante . Si nous regardons la classe anonyme désassemblée, nous voyons cet attribut:
EnclosingMethod: LambdaTest.test()V
La présence de cet attribut signifie que computeEnclosingScope
produira un MethodScope
pour la méthode générique static <F> void test()
. Puisque test
déclare la variable de type W
, nous la trouvons lorsque nous recherchons la portée englobante.
Pour répondre à cela, nous devons comprendre comment les lambdas sont compilés. Le corps du lambda se déplace dans une méthode statique synthétique. Au moment où nous déclarons notre lambda, une instruction invokedynamic
est émise, ce qui provoque la génération d'une classe d'implémentation TypeToken
la première fois que nous frappons cette instruction.
Dans cet exemple, la méthode statique générée pour le corps lambda ressemblerait à quelque chose comme ceci (si décompilée):
private static /* synthetic */ Object lambda$test$0() {
return new LambdaTest$1();
}
... où LambdaTest$1
est votre classe anonyme. Démontons cela et inspectons nos attributs:
Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
Tout comme dans le cas où nous avons instancié un type anonyme extérieur d'un lambda, la signature contient la variable de type W
. Mais EnclosingMethod
fait référence à la méthode synthétique.
La méthode synthétique lambda$test$0()
ne déclare pas la variable de type W
. De plus, lambda$test$0()
n'est pas entouré par test()
, donc la déclaration de W
n'est pas visible à l'intérieur. Votre classe anonyme a un sur-type contenant une variable de type que votre classe ne connaît pas car elle est hors de portée.
Lorsque nous appelons getGenericSuperclass()
, la hiérarchie de portée pour LambdaTest$1
Ne contient pas W
, donc l'analyseur ne peut pas le résoudre. En raison de la façon dont le code est écrit, cette variable de type non résolue entraîne le placement de null
dans les paramètres de type du supertype générique.
Notez que si votre lambda avait instancié un type qui ne faisait référence à aucune variable de type (par exemple, TypeToken<String>
), Vous ne le feriez pas rencontrer ce problème.
(i) Il y a un bogue dans javac
. Le Java Virtual Machine Specification §4.7.7 ("L'attribut EnclosingMethod
") indique:
Il est de la responsabilité d'un compilateur Java Java de s'assurer que la méthode identifiée via le
method_index
Est bien la plus proche lexicalement englobant méthode de la classe qui contient cet attributEnclosingMethod
. (c'est moi qui souligne)
Actuellement, javac
semble déterminer la méthode englobante après le réécriveur lambda suit son cours, et par conséquent, l'attribut EnclosingMethod
fait référence à une méthode qui ne existait même dans le champ lexical. Si EnclosingMethod
a signalé la méthode réelle englobant lexicalement, les variables de type sur cette méthode pourraient être résolues par les classes intégrées à lambda, et votre code produirait les résultats attendus.
C'est sans doute aussi un bogue que l'analyseur/réificateur de signature permet à un argument de type null
de se propager silencieusement dans un ParameterizedType
(qui, comme le souligne @ tom-hawtin-tackline, a des effets auxiliaires comme toString()
lancer un NPE).
Mon rapport de bogue pour le problème EnclosingMethod
est maintenant en ligne.
(ii) Il existe sans doute plusieurs bogues dans Java.lang.reflect
et ses API de prise en charge.
La méthode ParameterizedType::getActualTypeArguments()
est documentée comme lançant un TypeNotPresentException
lorsque "l'un des arguments de type réels fait référence à une déclaration de type inexistante". Cette description couvre sans doute le cas où une variable de type n'est pas dans la portée. GenericArrayType::getGenericComponentType()
devrait lever une exception similaire lorsque "le type du type de tableau sous-jacent fait référence à une déclaration de type inexistante". Actuellement, aucun ne semble lancer un TypeNotPresentException
en aucune circonstance.
Je dirais également que les différentes substitutions de Type::toString
Devraient simplement remplir le nom canonique de tout type non résolu plutôt que de lancer un NPE ou toute autre exception.
J'ai soumis un rapport de bogue pour ces problèmes liés à la réflexion et je publierai le lien une fois qu'il sera publiquement visible.
Si vous devez pouvoir référencer une variable de type déclarée par la méthode englobante, vous ne pouvez pas le faire avec un lambda; vous devrez revenir à la syntaxe de type anonyme plus longue. Cependant, la version lambda devrait fonctionner dans la plupart des autres cas. Vous devriez même être en mesure de référencer les variables de type déclarées par le class. Par exemple, ceux-ci devraient toujours fonctionner:
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>>() {};
}
}
Malheureusement, étant donné que ce bogue existe apparemment depuis la première introduction de lambdas et qu'il n'a pas été corrigé dans la version la plus récente de LTS, vous devrez peut-être supposer que le bogue reste dans les JDK de vos clients longtemps après qu'il a été corrigé, en supposant qu'il le soit fixe du tout.
Pour contourner ce problème, vous pouvez déplacer la création de TypeToken hors de lambda vers une méthode distincte, tout en utilisant lambda au lieu de la classe entièrement déclarée:
static<T> TypeToken<T[]> createTypeToken() {
return new TypeToken<T[]>() {};
}
Supplier<TypeToken<T[]>> sup = () -> createTypeToken();
Je n'ai pas trouvé la partie pertinente de la spécification, mais voici une réponse partielle.
Il y a certainement un bogue avec le type de composant étant null
. Pour être clair, c'est TypeToken.type
de la conversion ci-dessus en GenericArrayType
(beurk!) avec la méthode getGenericComponentType
invoquée. Les documents API ne mentionnent pas explicitement si le null
retourné est valide ou non. Cependant, la méthode toString
lance NullPointerException
, il y a donc certainement un bogue (au moins dans la version aléatoire de Java que j'utilise).
Je n'ai pas de compte bugs.Java.com, je ne peux donc pas le signaler. Quelqu'un devrait.
Jetons un coup d'œil aux fichiers de classe générés.
javap -private YourClass
Cela devrait produire une liste contenant quelque chose comme:
static <T> void test();
private static TypeToken lambda$test$0();
Notez que notre méthode explicite test
a son paramètre type, mais pas la méthode lambda synthétique. Vous pourriez vous attendre à quelque chose comme:
static <T> void test();
private static <T> TypeToken<T[]> lambda$test$0(); /*** DOES NOT HAPPEN ***/
// ^ name copied from `test`
// ^^^ `Object[]` would not make sense
Pourquoi cela ne se produit-il pas? Vraisemblablement parce que ce serait un paramètre de type de méthode dans un contexte où un paramètre de type de type est requis, et ce sont des choses étonnamment différentes. Il existe également une restriction sur les lambdas ne leur permettant pas d'avoir des paramètres de type de méthode, apparemment parce qu'il n'y a pas de notation explicite (certaines personnes peuvent suggérer que cela semble être une mauvaise excuse).
Conclusion: il y a au moins un bogue JDK non signalé ici. L'API reflect
et cette partie lambda + génériques du langage ne sont pas à mon goût.