为什么以下代码会编译?
方法IElement.getX(String)
返回类型IElement
或其子类的实例。 Main
类中的代码调用getX(String)
方法。编译器允许将返回值存储到Integer
类型的变量中(显然不在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");
}
}
返回类型是否仍然是IElement
的实例 - 即使在类型擦除之后?
getX(String)
方法的字节码是:
public abstract <T extends IElement> T getX(java.lang.String);
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #7 // <T::LIElement;>(Ljava/lang/String;)TT;
修改:与String
一致地替换Integer
。
答案 0 :(得分:21)
这实际上是一种合法的类型推断*。
我们可以将其减少到以下示例(Ideone):
interface Foo {
<F extends Foo> F bar();
public static void main(String[] args) {
Foo foo = null;
String baz = foo.bar();
}
}
允许编译器推断(无意义的,真正的)交集类型String & Foo
,因为Foo
是一个接口。对于问题中的示例,推断出Integer & IElement
。
这是荒谬的,因为转换是不可能的。我们不能自己做这样的演员:
// won't compile because Integer is final
Integer x = (Integer & IElement) element;
类型推断基本上适用于:
在算法结束时,每个变量已解析为基于绑定集的交集类型,如果它们有效,则调用将进行编译。
该过程从8.1.3开始:
当推理开始时,通常从类型参数声明
P1, ..., Pp
列表和关联的推理变量α1, ..., αp
生成绑定集。这样的绑定集如下构造。对于每个 l(1≤l≤p):
[...]
否则,对于TypeBound中
T
分隔的每个&
类型,绑定的αl <: T[P1:=α1, ..., Pp:=αp]
会出现在集合中[...]。
所以,这意味着首先编译器以F <: Foo
的边界开头(这意味着F
是Foo
的子类型。)
转到18.5.2,会考虑返回目标类型:
如果调用是多面运算,[...]让
R
成为m
的返回类型,让T
成为调用的目标类型,然后:
[...]
否则,约束公式
‹R θ → T›
会减少并与[绑定集]合并。
约束公式‹R θ → T›
被缩减为R θ <: T
的另一个边界,因此我们有F <: String
。
稍后根据18.4解决这些问题:
[...]为每个
Ti
定义候选实例αi
:
- 否则,
αi
有适当的上限U1, ..., Uk
,Ti = glb(U1, ..., Uk)
。边界
α1 = T1, ..., αn = Tn
与当前边界集合在一起。
回想一下,我们的界限是F <: Foo, F <: String
。 glb(String, Foo)
定义为String & Foo
。这显然是glb的合法类型,只需要:
对于任何两个类(非接口)
Vi
和Vj
,{{1},如果是编译时错误}不是Vi
的子类,反之亦然。
最后:
如果对推理变量
Vj
的实例化T1, ..., Tp
解析成功,请α1, ..., αp
为替换θ'
。然后:
- 如果方法不适用于未经检查的转换,则
[P1:=T1, ..., Pp:=Tp]
的调用类型是通过将m
应用于θ'
的类型获得的。
因此,使用m
作为String & Foo
的类型调用该方法。我们当然可以将其分配给F
,从而无法将String
转换为Foo
。
显然不考虑String
/ String
是最终类的事实。
*注意:类型 erasure 与问题完全无关。
此外,虽然这也在Java 7上编译,但我认为我们不必担心那里的规范是合理的。 Java 7的类型推断本质上是Java 8的不太复杂的版本。它的编译原因类似。
作为附录,虽然很奇怪,但这可能永远不会导致一个尚未出现的问题。编写一个泛型方法很少有用,它的返回类型只是从返回目标中推断出来的,因为只有Integer
可以从这样的方法中返回而不进行强制转换。
假设我们有一些地图模拟,它存储特定界面的子类型:
null
出现如下错误已完全有效:
interface FooImplMap {
void put(String key, Foo value);
<F extends Foo> F get(String key);
}
class Bar implements Foo {}
class Biz implements Foo {}
因此,我们也做FooImplMap m = ...;
m.put("b", new Bar());
Biz b = m.get("b"); // casting Bar to Biz
的事实不是新错误的可能性。如果我们编写这样的代码,那么从一开始就可能是不健全的。
通常,如果没有理由限制类型参数,则只能从目标类型推断出类型参数,例如: Integer i = m.get("b");
和Collections.emptyList()
:
Optional.empty()
这是A-OK,因为private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
既不能生成也不会消费Optional.empty()
。