为什么这个具有绑定的泛型方法可以返回任何类型?

为什么编译以下代码?该方法返回该类型或其子类的实例。类中的代码调用该方法。编译器允许将返回值存储到该类型的变量(显然不在 的层次结构中)。IElement.getX(String)IElementMaingetX(String)IntegerIElement

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;

编辑:始终替换为 。StringInteger


答案 1

这实际上是一个合法的类型推断*。

我们可以将其简化为以下示例(Ideone):

interface Foo {
    <F extends Foo> F bar();
    
    public static void main(String[] args) {
        Foo foo = null;
        String baz = foo.bar();
    }
}

允许编译器推断(实际上是荒谬的)交集类型,因为它是一个接口。对于问题中的示例,是推断出来的。String & FooFooInteger & IElement

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

// won't compile because Integer is final
Integer x = (Integer & IElement) element;

类型推断基本上适用于:

  • 每个方法的类型参数的一组推理变量
  • 必须符合的一组边界
  • 有时是约束,这些约束被简化为边界。

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

该过程从 8.1.3 开始

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

  • [...]

  • 否则,对于 TypeBound 中由 分隔的每个类型,绑定将显示在集合 [...] 中。T&αl <: T[P1:=α1, ..., Pp:=αp]

因此,这意味着首先编译器以 bound of 开头(这意味着 是 的子类型)。F <: FooFFoo

移动到 18.5.2,将考虑返回目标类型:

如果调用是 poly 表达式,则 [...] 设为 的返回类型 ,设为调用的目标类型,然后:RmT

  • [...]

  • 否则,约束公式将简化并与 [绑定集] 合并。‹R θ → T›

约束公式被简化为 的另一个边界,所以我们有 。‹R θ → T›R θ <: TF <: String

稍后根据 18.4 解决这些问题

[...]为每个实例定义了一个候选实例化:Tiαi

  • 否则,其中 具有适当的上限 , 。αiU1, ..., UkTi = glb(U1, ..., Uk)

边界与当前绑定集合并。α1 = T1, ..., αn = Tn

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

如果对于任何两个不是接口)和 不是 的子类,反之亦然,则这是一个编译时错误。ViVjViVj

最后:

如果解析成功,则使用推理变量的实例化,设为替换 。然后:T1, ..., Tpα1, ..., αpθ'[P1:=T1, ..., Pp:=Tp]

  • 如果不需要未经检查的转换即可使方法适用,则通过应用于 的类型来获得 的调用类型。mθ'm

因此,调用该方法时,其类型为 。我们当然可以将其分配给 a,从而不可能将 a 转换为 a 。String & FooFStringFooString

显然没有考虑 / 是最终类的事实。StringInteger


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

另外,虽然这也可以在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 {}

犯如下错误已经是完全有效的:

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()

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

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

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


答案 2

推荐