简体   繁体   English

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

[英]Why can this generic method with a bound return any type?

Why does the following code compile?为什么下面的代码可以编译? The method IElement.getX(String) returns an instance of the type IElement or of subclasses thereof. IElement.getX(String)方法返回IElement类型或其子类的实例。 The code in the Main class invokes the getX(String) method. Main class 中的代码调用getX(String)方法。 The compiler allows to store the return value to a variable of the type Integer (which obviously is not in the hierarchy of IElement ).编译器允许将返回值存储到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 .编辑:始终将String替换为Integer

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.允许编译器推断(无意义的,真的)交叉类型String & Foo因为Foo是一个接口。 For the example in the question, Integer & IElement is inferred.对于问题中的示例,推断为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 P 1 , ..., P p and associated inference variables α 1 , ..., α p .当推理开始时,通常从类型参数声明P 1 , ..., P p和关联的推理变量α 1 , ..., α p的列表中生成绑定集。 Such a bound set is constructed as follows.这样一个边界集的构造如下。 For each l (1 ≤ l ≤ p) :对于每个l (1 ≤ l ≤ p)

  • […] […]

  • Otherwise, for each type T delimited by & in a TypeBound , the bound α l <: T[P 1 :=α 1 , ..., P p :=α p ] appears in the set […].否则,对于TypeBound 中&分隔的每个类型T ,边界α l <: T[P 1 :=α 1 , ..., P p :=α p ]出现在集合 [...] 中。

So, this means first the compiler starts with a bound of F <: Foo (which means F is a subtype of Foo ).因此,这意味着首先编译器以F <: Foo的边界开始(这意味着FFoo的子类型)。

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:如果调用是一个 poly 表达式,[…] 让Rm的返回类型,让T是调用的目标类型,然后:

  • […] […]

  • Otherwise, the constraint formula ‹R θ → T› is reduced and incorporated with [the bound set].否则,约束公式‹R θ → T›会减少并与[边界集]合并。

The constraint formula ‹R θ → T› gets reduced to another bound of R θ <: T , so we have F <: String .约束公式‹R θ → T›被简化为R θ <: T另一个界限,所以我们有F <: String

Later on these get resolved according to 18.4 :稍后根据18.4解决这些问题:

[…] a candidate instantiation T i is defined for each α i : [...]的候选实例T i为每个定义的α i

  • Otherwise, where α i has proper upper bounds U 1 , ..., U k , T i = glb(U 1 , ..., U k ) .否则,其中α i具有适当的上限U 1 , ..., U k , T i = glb(U 1 , ..., U k )

The bounds α 1 = T 1 , ..., α n = T n are incorporated with the current bound set.界限α 1 = T 1 , ..., α n = T n与当前界限集合并。

Recall that our set of bounds is F <: Foo, F <: String .回想一下,我们的边界集是F <: Foo, F <: String glb(String, Foo) is defined as String & Foo . glb(String, Foo)被定义为String & Foo This is apparently a legitimate type for glb , which only requires that:这显然是glb的合法类型,它只需要:

It is a compile-time error if, for any two classes ( not interfaces ) V i and V j , V i is not a subclass of V j or vice versa.如果对于任何两个不是接口V iV jV i不是V j的子类,则这是编译时错误,反之亦然。

Finally:最后:

If resolution succeeds with instantiations T 1 , ..., T p for inference variables α 1 , ..., α p , let θ' be the substitution [P 1 :=T 1 , ..., P p :=T p ] .如果对推理变量α 1 , ..., α p实例化T 1 , ..., T p解析成功,则令θ'为替换[P 1 :=T 1 , ..., P p :=T p ] Then:然后:

  • 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 .因此,使用String & Foo作为F的类型调用该方法。 We can of course assign this to a String , thus impossibly converting a Foo to a String .我们当然可以将其分配给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 上编译,但我认为我们不必担心那里的规范是合理的。 Java 7's type inference was essentially a less sophisticated version of Java 8's. Java 7 的类型推断本质上是 Java 8 的一个不太复杂的版本。 It compiles for similar reasons.它出于类似的原因进行编译。


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");所以事实上我们也可以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.如果我们像这样编写代码,那么一开始就可能是不合理的。

Generally, a type parameter should only be solely inferred from the target type if there is no reason to bound it, eg 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM