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

nra*_*ner 33 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");
  }
}
Run Code Online (Sandbox Code Playgroud)

返回类型是否仍然是一个实例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;
Run Code Online (Sandbox Code Playgroud)

编辑:替换String与一致Integer.

Rad*_*def 21

这实际上是一种合法的类型推断*.

我们可以将其减少到以下示例(Ideone):

interface Foo {
    <F extends Foo> F bar();

    public static void main(String[] args) {
        Foo foo = null;
        String baz = foo.bar();
    }
}
Run Code Online (Sandbox Code Playgroud)

允许编译器推断(无意义的,真正的)交集类型,String & Foo因为它Foo是一个接口.对于问题中的示例,Integer & IElement推断出来.

这是荒谬的,因为转换是不可能的.我们不能自己做这样的演员:

// won't compile because Integer is final
Integer x = (Integer & IElement) element;
Run Code Online (Sandbox Code Playgroud)

类型推断基本上适用于:

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

在算法结束时,每个变量都根据绑定集解析为交集类型,如果它们有效,则调用将进行编译.

该过程从8.1.3开始:

推理开始时,通常从类型参数声明列表和关联的推理变量生成绑定集.这样的绑定集如下构造.对于每个l(1≤l≤p):P1, ..., Pp?1, ..., ?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

  • 否则,哪里有适当的上限,.?iU1, ..., UkTi = glb(U1, ..., Uk)

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

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

它是一个编译时间错误,如果,对于任何两个(未接口)和,不是的一个子类,或反之亦然.ViVjViVj

最后:

如果解析成功与推理变量的实例化,让我们替换.然后:T1, ..., Tp?1, ..., ?p?'[P1:=T1, ..., Pp:=Tp]

  • 如果不加以控制的转换是不必要的方法可应用于,那么该调用键入m通过施加获得?'到的类型m.

因此,该方法String & Foo作为类型调用F.我们当然可以将其分配给a String,因此不可能将a转换Foo为a String.

显然不考虑String/ Integer是最终类的事实.


*注意:类型擦除与问题完全无关.

此外,虽然这也编译在Java 7上,但我认为我们不必担心那里的规范是合理的.Java 7的类型推断本质上是Java 8的不太复杂的版本.它编译出于类似的原因.


作为附录,虽然很奇怪,但这可能永远不会导致一个尚未出现的问题.编写一个泛型方法很少有用,该泛型方法的返回类型只是从返回目标中推断出来的,因为只能null在没有强制转换的情况下从这样的方法返回.

例如,假设我们有一些地图模拟,它存储特定接口的子类型:

interface FooImplMap {
    void put(String key, Foo value);
    <F extends Foo> F get(String key);
}

class Bar implements Foo {}
class Biz implements Foo {}
Run Code Online (Sandbox Code Playgroud)

发出如下错误已经完全有效:

FooImplMap m = ...;
m.put("b", new Bar());
Biz b = m.get("b"); // casting Bar to Biz
Run Code Online (Sandbox Code Playgroud)

因此,我们也可以做的事实Integer i = m.get("b");不是错误的可能性.如果我们编写这样的代码,那么开始时它可能已经不健全了.

一般情况下,一个类型参数应该只单纯从目标类型推断,如果没有理由去约束它,比如Collections.emptyList()Optional.empty():

private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}
Run Code Online (Sandbox Code Playgroud)

这是A-OK,因为Optional.empty()既不能生产也不能消费T.

  • @OldCurmudgeon我可以向你保证,现在这个Q&A中没有任何原始类型.如果有的话,无论如何都会发生不同的事情.我们实际上得到一个错误.http://ideone.com/SeZ09H (2认同)