为什么这个带有限制的通用方法可以返回任何类型?

36
为什么下面的代码可以编译? 方法 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
Java版本为:1.7.0_45 32位 - nrainer
CharSequence可以赋值给String,有什么问题吗? - Muhammad Hamed
1
刚刚用Java 1.8.0_31测试了原始版本(带有“Integer”),确实编译通过。 - Ray
1
它也可以编译Java 6。奇怪的是,它似乎接受任何返回类型。String str = element.getX("Y") 也可以工作。当你在eclipse中突出显示该方法时,它显示<? extend Integer> ? extends Integer IElement.getX(String)。 - ramp
2
似乎涉及使用接口作为上限。改为“T extends List”,仍然可以编译。 - Tobb
1个回答

28
这实际上是一种合法的类型推断。我们可以将其简化为以下示例(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(这意味着FFoo的子类型)。
转到 18.5.2,考虑返回目标类型:
如果调用是多态表达式,[...]让R成为m的返回类型,让T成为调用的目标类型,然后:
- [...] - 否则,约束公式‹R θ → T›被简化并与[绑定集]合并。
约束公式‹R θ → T›被简化为另一个绑定R θ <: T,因此我们有F <: String

后来根据18.4解决了这些问题:

为每个αi定义一个候选实例化Ti

  • 否则,当αi具有适当的上界U1, ..., Uk时,Ti = glb(U1, ..., Uk)

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

请注意,我们的边界集是F <: Foo, F <: Stringglb(String, Foo)被定义为String & Foo。这显然是glb的一个合法类型,它只需要满足以下条件:

如果对于任何两个(而不是接口)ViVjVi不是Vj的子类或反之,则会在编译时出现错误。

最后:

如果实例化变量α1, …, αp的T1, ..., Tp成功,则让θ'成为替换[P1:=T1, ..., Pp:=Tp]。然后:
- 如果未检查转换不是必需的,则可以通过将θ'应用于m的类型来获得m的调用类型。 因此,使用String & Foo作为F的类型调用方法。当然,我们可以将其分配给String,从而将Foo转换为String,这是不可能的。
显然,没有考虑String / Integer是final类的事实。
* 注意:类型擦除与此问题完全无关。
此外,虽然这也可以在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;
}

这很好,因为Optional.empty()既不能生成也不能消费T

问题可能在于 T extends [interface] 隐式地意味着 T extends Object & [Interface]。由于 String 是一个 Object,编译器没有报告这个问题。 - Clashsoft
@Radiodef - 原始类型!!那就是我一直在寻找的短语! - OldCurmudgeon
2
@OldCurmudgeon,我可以向你保证,目前在这个问答环节中没有使用原始类型。而且如果确实有这样的情况发生,会得到不同的处理结果。我们实际上会收到一个错误提示。http://ideone.com/SeZ09H - Radiodef
1
非常感谢您对JLS中这个神秘部分的描述,以便解释正在发生的事情。 - benzonico
1
然而,你可以从Object转换为(Integer&IElement) :) 或者从Number等类型进行转换。 - ZhongYu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接