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

时间:2021-02-14 22:32:42

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

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

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

返回类型不应该是IElement的实例 - 即使在类型擦除之后?

The bytecode of the getX(String) method is:

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;

Edit: Replaced String consistently with Integer.

编辑:与Integer一致地替换字符串。

1 个解决方案

#1


21  

This is actually a legitimate type inference*.

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

We can reduce this to the following example (Ideone):

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

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

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

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.

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

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.

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

The process begins in 8.1.3:

该过程从8.1.3开始:

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

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

  • […]

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

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

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

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

Moving to 18.5.2, the return target type gets considered:

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

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为m的返回类型,让T为调用的目标类型,然后:

  • […]

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

    否则,约束公式 被减少并且与[绑定集合]合并。 θ→t>

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

约束公式 减小到Rθ<:T的另一个界限,所以我们有F <:String。 θ→t>

Later on these get resolved according to 18.4:

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

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

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

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

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

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

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:

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

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.

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

Finally:

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

如果分辨率成功,实例化T1,...,Tp用于推理变量α1,...,αp,则θ'为替换[P1:= T1,...,Pp:= Tp]。然后:

  • If unchecked conversion was not necessary for the method to be applicable, then the invocation type of m is obtained by applying θ' to the type of m.
  • 如果该方法不适用于未经检查的转换,则通过将θ'应用于m的类型来获得m的调用类型。

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&Foo作为F的类型调用该方法。我们当然可以将其赋值给String,从而不可能将Foo转换为String。

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

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


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

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

Also, while this compiles on Java 7 as well, I think it's reasonable to say we needn't worry about the specification there. Java 7's type inference was essentially a less sophisticated version of Java 8's. It compiles for similar reasons.

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


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.

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

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

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.

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

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

通常,如果没有理由限制类型参数,则应仅从目标类型推断出类型参数,例如, 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;
}

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

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

#1


21  

This is actually a legitimate type inference*.

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

We can reduce this to the following example (Ideone):

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

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

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

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.

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

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.

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

The process begins in 8.1.3:

该过程从8.1.3开始:

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

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

  • […]

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

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

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

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

Moving to 18.5.2, the return target type gets considered:

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

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为m的返回类型,让T为调用的目标类型,然后:

  • […]

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

    否则,约束公式 被减少并且与[绑定集合]合并。 θ→t>

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

约束公式 减小到Rθ<:T的另一个界限,所以我们有F <:String。 θ→t>

Later on these get resolved according to 18.4:

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

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

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

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

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

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

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:

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

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.

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

Finally:

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

如果分辨率成功,实例化T1,...,Tp用于推理变量α1,...,αp,则θ'为替换[P1:= T1,...,Pp:= Tp]。然后:

  • If unchecked conversion was not necessary for the method to be applicable, then the invocation type of m is obtained by applying θ' to the type of m.
  • 如果该方法不适用于未经检查的转换,则通过将θ'应用于m的类型来获得m的调用类型。

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&Foo作为F的类型调用该方法。我们当然可以将其赋值给String,从而不可能将Foo转换为String。

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

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


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

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

Also, while this compiles on Java 7 as well, I think it's reasonable to say we needn't worry about the specification there. Java 7's type inference was essentially a less sophisticated version of Java 8's. It compiles for similar reasons.

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


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.

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

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

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.

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

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

通常,如果没有理由限制类型参数,则应仅从目标类型推断出类型参数,例如, 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;
}

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

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