为什么带绑定的泛型方法可以返回任何类型?

时间:2015-04-16 09:02:42

标签: java generics type-inference generic-method

为什么以下代码会编译? 方法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

1 个答案:

答案 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;

类型推断基本上适用于:

  • 每个方法类型参数的一组推理变量
  • 必须符合的一组 bounds
  • 有时约束减少到边界。

在算法结束时,每个变量已解析为基于绑定集的交集类型,如果它们有效,则调用将进行编译。

该过程从8.1.3开始:

  

当推理开始时,通常从类型参数声明P1, ..., Pp列表和关联的推理变量α1, ..., αp生成绑定集。这样的绑定集如下构造。对于每个 l(1≤l≤p)

     
      
  • [...]

  •   
  • 否则,对于TypeBoundT分隔的每个&类型,绑定的αl <: T[P1:=α1, ..., Pp:=αp]会出现在集合中[...]。

  •   

所以,这意味着首先编译器以F <: Foo的边界开头(这意味着FFoo的子类型。)

转到18.5.2,会考虑返回目标类型:

  

如果调用是多面运算,[...]让R成为m的返回类型,让T成为调用的目标类型,然后:

     
      
  • [...]

  •   
  • 否则,约束公式‹R θ → T›会减少并与[绑定集]合并。

  •   

约束公式‹R θ → T›被缩减为R θ <: T的另一个边界,因此我们有F <: String

稍后根据18.4解决这些问题:

  

[...]为每个Ti定义候选实例αi

     
      
  • 否则,αi有适当的上限U1, ..., UkTi = glb(U1, ..., Uk)
  •   
     

边界α1 = T1, ..., αn = Tn与当前边界集合在一起。

回想一下,我们的界限是F <: Foo, F <: Stringglb(String, Foo)定义为String & Foo。这显然是glb的合法类型,只需要:

  

对于任何两个非接口ViVj,{{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()