[英]Spring can't determine generic types when lambda expression is used instead of anonymous inner class
[英]Different generic behaviour when using lambda instead of explicit anonymous inner class
我正在开发一个严重依赖泛型类型的项目。 它的一个关键组件是所谓的TypeToken
,它提供了一种在运行时表示泛型类型并在其上应用一些实用程序函数的方法。 为了避免Java的类型擦除,我使用大括号表示法( {}
)来创建一个自动生成的子类,因为这使得类型可以恢复。
TypeToken
基本上做什么 这是TypeToken
一个非常简化的版本,它比原始实现更宽松。 但是,我正在使用这种方法,所以我可以确保真正的问题不在于其中一个效用函数。
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];
// ...
}
基本上,这种实现几乎适用于所有情况。 处理大多数类型没有问题。 以下示例完美运行:
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};
由于它不检查类型,上面的实现允许编译器允许的每种类型,包括TypeVariables。
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}
在这种情况下, type
是一个GenericArrayType
,它将TypeVariable
作为其组件类型。 这很好。
但是,当您在lambda表达式中初始化TypeToken
时,事情开始发生变化。 (类型变量来自上面的test
函数)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};
在这种情况下, type
仍然是GenericArrayType
,但它保持null
作为其组件类型。
但是如果你正在创建一个匿名内部类,事情就会再次发生变化:
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};
在这种情况下,组件类型再次保持正确的值(TypeVariable)
稍微澄清一点:这不是一个与程序“相关”的问题,因为我根本不允许不可解析的类型,但它仍然是一个我想要了解的有趣现象。
同时,我对这个话题做了一些研究。 在Java语言规范§15.12.2.2中,我发现了一个可能与它有关的表达式 - “与适用性相关”,提到“隐式类型的lambda表达式”作为例外。 显然,这是不正确的章节,但表达式在其他地方使用,包括关于类型推断的章节。
但说实话:我还没有真正弄清楚所有这些运营商都喜欢:=
或Fi0
意味着什么使它真的很难理解它。 如果有人能够澄清这一点,我会很高兴,如果这可能是对奇怪行为的解释。
我再次想到了这种方法并得出结论,即使编译器会删除该类型,因为它不是“与适用性相关”,因此将组件类型设置为null
而不是最慷慨的类型是不合理的,对象。 我想不出语言设计师决定这么做的一个原因。
我刚刚用最新版本的Java重新测试了相同的代码(之前我使用过8u191
)。 令我遗憾的是,尽管Java的类型推断得到了改进,但这并没有改变任何东西......
几天前我已经要求在官方Java Bug数据库/跟踪器中输入一个条目并且它已被接受。 由于审阅我的报告的开发人员将优先级P4分配给该错误,因此可能需要一段时间才能修复。 你可以在这里找到报告。
汤姆·霍金(Tom Hawtin)的巨大呐喊 - 提到这可能是Java SE本身的一个重要错误。 然而,由于他令人印象深刻的背景知识,Mike Strobel的报告可能比我的更详细。 但是,当我撰写报告时,斯特罗贝尔的答案尚未公布。
tldr:
javac
中存在一个错误,它记录了嵌入lambda内部类的错误封闭方法。 因此,这些内部类无法解析实际封闭方法上的类型变量。java.lang.reflect
API实现中可以说有两组错误:
- 有些方法被记录为在遇到不存在的类型时抛出异常,但它们永远不会。 相反,它们允许空引用传播。
- 当无法解析类型时,各种
Type::toString()
覆盖当前抛出或传播NullPointerException
。
答案与通常在使用泛型的类文件中发出的通用签名有关。
通常,当您编写具有一个或多个通用超类型的类时,Java编译器将发出一个Signature
属性,该属性包含类的超类型的完全参数化的通用签名。 我以前写过这些 ,但简短的解释是:如果没有它们,除非你碰巧有源代码,否则不可能将泛型类型作为泛型类型使用 。 由于类型擦除,有关类型变量的信息在编译时会丢失。 如果该信息未作为额外元数据包含在内,则IDE和您的编译器都不会知道某个类型是通用的,并且您无法使用它。 编译器也不能发出必要的运行时检查来强制类型安全。
javac
将为其签名包含类型变量或参数化类型的任何类型或方法发出通用签名元数据,这就是您能够获取匿名类型的原始通用超类型信息的原因。 例如,此处创建的匿名类型:
TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
...包含此Signature
:
LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
由此, java.lang.reflection
API可以解析有关您的(匿名)类的通用超类型信息。
但是我们已经知道,当使用具体类型对TypeToken
进行参数化时,这种方法可以正常工作。 让我们看一个更相关的例子,其类型参数包括一个类型变量 :
static <F> void test() {
TypeToken sup = new TypeToken<F[]>() {};
}
在这里,我们得到以下签名:
LTypeToken<[TF;>;
有道理,对吗? 现在,让我们看看java.lang.reflect
API如何从这些签名中提取通用超类型信息。 如果我们进入Class::getGenericSuperclass()
,我们会发现它做的第一件事就是调用getGenericInfo()
。 如果我们之前没有调用过这个方法,那么ClassRepository
会被实例化:
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;
}
这里的关键部分是对getFactory()
的调用,它扩展为:
CoreReflectionFactory.make(this, ClassScope.make(this))
ClassScope
是我们关心的:它提供了类型变量的解析范围。 给定类型变量名称,搜索范围以查找匹配的类型变量。 如果找不到,则搜索“外部”或封闭范围:
public TypeVariable<?> lookup(String name) {
TypeVariable<?>[] tas = getRecvr().getTypeParameters();
for (TypeVariable<?> tv : tas) {
if (tv.getName().equals(name)) {return tv;}
}
return getEnclosingScope().lookup(name);
}
最后,这一切的关键(来自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);
// ...
}
如果在类本身上没有找到类型变量(例如, F
)(例如,匿名的TypeToken<F[]>
),则下一步是搜索封闭方法 。 如果我们查看反汇编的匿名类,我们会看到这个属性:
EnclosingMethod: LambdaTest.test()V
此属性的存在意味着computeEnclosingScope
将为泛型方法static <F> void test()
生成MethodScope
。 由于test
声明了类型变量W
,因此我们在搜索封闭范围时会找到它。
要回答这个问题,我们必须了解如何编译lambdas。 lambda的主体被移动到合成静态方法中。 在我们声明lambda的时候,会发出一个invokedynamic
指令,这会导致在我们第一次点击该指令时生成一个TypeToken
实现类。
在这个例子中,为lambda体生成的静态方法看起来像这样(如果反编译):
private static /* synthetic */ Object lambda$test$0() {
return new LambdaTest$1();
}
... LambdaTest$1
是你的匿名类。 让我们解散它并检查我们的属性:
Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
就像我们在lambda 之外实例化匿名类型的情况一样,签名包含类型变量W
但EnclosingMethod
指的是合成方法 。
合成方法lambda$test$0()
不声明类型变量W
此外, lambda$test$0()
不包含在test()
,因此W
的声明在其中不可见。 您的匿名类有一个超类型,其中包含您的类不知道的类型变量,因为它超出了范围。
当我们调用getGenericSuperclass()
, LambdaTest$1
的范围层次结构不包含W
,因此解析器无法解析它。 由于代码的编写方式,这个未解析的类型变量会导致null
被置于泛型超类型的类型参数中。
请注意,如果你的lambda实例化了一个没有引用任何类型变量的类型(例如, TypeToken<String>
),那么你就不会遇到这个问题。
(i) javac
有一个错误。 Java虚拟机规范§4.7.7 (以下简称“ EnclosingMethod
属性”)规定:
Java编译器负责确保通过
method_index
标识的方法确实是包含此EnclosingMethod
属性的类的最接近的词法封闭方法。 (强调我的)
目前, javac
似乎确定了lambda重写器运行之后的封闭方法,因此, EnclosingMethod
属性引用了一个在词法范围内从未存在过的方法。 如果EnclosingMethod
报告了实际的词法封闭方法,那么该方法的类型变量可以通过lambda嵌入类来解析,并且您的代码将产生预期的结果。
可以说这也是一个错误,签名解析器/ reifier默默地允许将null
类型参数传播到ParameterizedType
(正如@ tom-hawtin-tackline指出的那样,它具有类似toString()
抛出NPE的辅助效果)。
我关于 EnclosingMethod
问题的错误报告现已在线。
(ii) java.lang.reflect
及其支持API中存在多个错误。
当“任何实际类型参数引用不存在的类型声明”时,方法ParameterizedType::getActualTypeArguments()
被记录为抛出TypeNotPresentException
。 该描述可以说涵盖了类型变量不在范围内的情况。 当“基础数组类型的类型引用不存在的类型声明”时, GenericArrayType::getGenericComponentType()
应该抛出类似的异常。 目前,在任何情况下都不会抛出TypeNotPresentException
。
我还认为各种Type::toString
覆盖只应填写任何未解析类型的规范名称,而不是抛出NPE或任何其他异常。
我已经针对这些与反思相关的问题提交了错误报告,一旦公开显示,我就会发布链接。
如果你需要能够引用封闭方法声明的类型变量,那么你就不能用lambda做到这一点; 你将不得不回到更长的匿名类型语法。 但是,lambda版本应该适用于大多数其他情况。 您甚至应该能够引用封闭类声明的类型变量。 例如,这些应始终有效:
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>>() {};
}
}
不幸的是,鉴于自从lambdas首次引入以来已经存在这个bug,并且在最近的LTS版本中没有修复它,你可能不得不假设它在修复后很长时间内仍然存在于客户端的JDK中,假设它得到了完全固定。
作为一种解决方法,您可以将lambda的TypeToken创建移动到单独的方法,并仍然使用lambda而不是完全声明的类:
static<T> TypeToken<T[]> createTypeToken() {
return new TypeToken<T[]>() {};
}
Supplier<TypeToken<T[]>> sup = () -> createTypeToken();
我没有找到规范的相关部分,但这是一个部分答案。
当然组件类型为null
的错误。 为了清楚TypeToken.type
,这是从上面转换为GenericArrayType
(yuck!)的TypeToken.type
,并调用了getGenericComponentType
方法。 API文档未明确提及返回的null
是否有效。 但是, toString
方法抛出NullPointerException
,所以肯定存在一个错误(至少在我使用的随机Java版本中)。
我没有bugs.java.com帐户,因此无法报告此情况。 有人应该。
我们来看看生成的类文件。
javap -private YourClass
这应该会产生一个包含以下内容的列表:
static <T> void test();
private static TypeToken lambda$test$0();
请注意,我们的显式test
方法有它的类型参数,但合成的lambda方法没有。 您可能会期待如下:
static <T> void test();
private static <T> TypeToken<T[]> lambda$test$0(); /*** DOES NOT HAPPEN ***/
// ^ name copied from `test`
// ^^^ `Object[]` would not make sense
为什么不这样呢? 大概是因为这将是需要类型类型参数的上下文中的方法类型参数,并且它们是令人惊讶的不同的东西。 对lambdas也有一个限制,不允许他们有方法类型参数,显然是因为没有明确的符号(有些人可能会认为这似乎是一个糟糕的借口)。
结论:这里至少有一个未报告的JDK错误。 reflect
API和这种语言的lambda +泛型部分不符合我的口味。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.