为什么这个带有绑定的泛型方法可以返回任何类型? [英] Why can this generic method with a bound return any type?

查看:15
本文介绍了为什么这个带有绑定的泛型方法可以返回任何类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Why does the following code compile? The method IElement.getX(String) returns an instance of the type IElement or of subclasses thereof. The code in the Main class invokes the getX(String) method. The compiler allows to store the return value to a variable of the type Integer (which obviously is not in the hierarchy of 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 的实例 - 即使在类型擦除之后?

Shouldn't the return type still be an instance of IElement - even after the type erasure?

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.

推荐答案

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

This is actually a legitimate type inference*.

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

We can reduce this to the following example (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 是推断出来的.

The compiler is allowed to infer a (nonsensical, really) intersection type String & Foo because Foo is an interface. For the example in the question, Integer & IElement is inferred.

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

It's nonsensical because the conversion is impossible. We can't do such a cast ourselves:

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

<小时>

类型推断基本上适用于:


Type inference basically works with:

  • 一组用于每个方法类型参数的推理变量.
  • 必须遵守的一组界限.
  • 有时约束减少到界限.
  • a set of inference variables for each of a method's type parameters.
  • a set of bounds that must be conformed to.
  • sometimes constraints, which are reduced to bounds.

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

At the end of the algorithm, each variable is resolved to an intersection type based on the bound set, and if they're valid, the invocation compiles.

流程开始于 8.1.3:

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

When inference begins, a bound set is typically generated from a list of type parameter declarations P1, ..., Pp and associated inference variables α1, ..., αp. Such a bound set is constructed as follows. For each l (1 ≤ l ≤ p):

  • […]

否则,对于 TypeBound,绑定αl<: T[P1:=α1, ..., Pp:=αp] 出现在集合 […] 中.

Otherwise, for each type T delimited by & in a TypeBound, the bound αl <: T[P1:=α1, ..., Pp:=αp] appears in the set […].

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

So, this means first the compiler starts with a bound of F <: Foo (which means F is a subtype of Foo).

移至 18.5.2,返回目标类型被考虑:

Moving to 18.5.2, the return target type gets considered:

如果调用是一个poly表达式,[…]让R作为m的返回类型,让T作为调用的目标输入,然后:

If the invocation is a poly expression, […] let R be the return type of m, let T be the invocation's target type, and then:

  • […]

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

Otherwise, the constraint formula ‹R θ → T› is reduced and incorporated with [the bound set].

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

The constraint formula ‹R θ → T› gets reduced to another bound of R θ <: T, so we have F <: String.

稍后根据 18.4:

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

[…] a candidate instantiation Ti is defined for each αi:

  • 否则,αi 有适当的上限 U1, ..., Uk, Ti = glb(U1, ..., Uk).
  • Otherwise, where αi has proper upper bounds U1, ..., Uk, Ti = glb(U1, ..., Uk).

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

The bounds α1 = T1, ..., αn = Tn are incorporated with the current bound set.

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

Recall that our set of bounds is F <: Foo, F <: String. glb(String, Foo) is defined as String & Foo. This is apparently a legitimate type for glb, which only requires that:

如果对于任何两个(非接口)Vi,这是一个编译时错误而VjVi不是Vj 反之亦然.

It is a compile-time error if, for any two classes (not interfaces) Vi and Vj, Vi is not a subclass of Vj or vice versa.

最后:

如果解析成功,实例化 T1, ..., Tp 用于推理变量 α1, ..., αp,让 θ' 成为替代 [P1:=T1, ..., Pp:=Tp].然后:

If resolution succeeds with instantiations T1, ..., Tp for inference variables α1, ..., αp, let θ' be the substitution [P1:=T1, ..., Pp:=Tp]. Then:

  • 如果非检查转换对于该方法的适用来说是不必要的,则通过将θ'应用于m的类型来获得m的调用类型.

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

The method is therefore invoked with String & Foo as the type of F. We can of course assign this to a String, thus impossibly converting a Foo to a String.

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

The fact that String/Integer are final classes is apparently not considered.

* 注意:输入 erasure 与问题完全无关.

* Note: type erasure is/was completely unrelated to the issue.

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

作为附录,虽然很奇怪,但这可能永远不会导致尚未出现的问题.编写返回类型仅从返回目标推断的泛型方法很少有用,因为只有 null 可以从这种方法返回而无需强制转换.

As an addendum, while strange, this will likely never cause a problem that was not already present. It's rarely useful to write a generic method whose return type is solely inferred from the return target, because only null can be returned from such a method without casting.

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

Suppose for example we have some map analog which stores subtypes of a particular interface:

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

class Bar implements Foo {}
class Biz implements Foo {}

犯这样的错误已经完全有效了:

It's already perfectly valid to make an error such as the following:

FooImplMap m = ...;
m.put("b", new Bar());
Biz b = m.get("b"); // casting Bar to Biz

所以我们可以Integer i = m.get("b");的事实并不是一个新的可能性为错误.如果我们像这样编写代码,那么一开始就可能是不合理的.

So the fact that we can also do Integer i = m.get("b"); is not a new possibility for error. If we were programming code like this, it was already potentially unsound to begin with.

通常,如果没有理由绑定类型参数,则只能从目标类型推断出类型参数,例如Collections.emptyList()Optional.empty():

Generally, a type parameter should only be solely inferred from the target type if there is no reason to bound it, e.g. Collections.emptyList() and 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.

This is A-OK because Optional.empty() can neither produce nor consume a T.

这篇关于为什么这个带有绑定的泛型方法可以返回任何类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆