为什么这个泛型方法可以绑定 return 任何类型?
Why can this generic method with a bound return any type?
为什么下面的代码可以通过?
方法 IElement.getX(String)
return 是类型 IElement
或其子 class 类型的实例。 Main
class 中的代码调用 getX(String)
方法。编译器允许将 return 值存储到 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");
}
}
return 类型不应该仍然是 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
。
这实际上是一个合法的类型推断*。
我们可以将其简化为以下示例 (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:
When inference begins, a bound set is typically generated from a list of type parameter declarations P<sub>1</sub>, ..., P<sub>p</sub>
and associated inference variables α<sub>1</sub>, ..., α<sub>p</sub>
. Such a bound set is constructed as follows. For each l (1 ≤ l ≤ p):
[…]
Otherwise, for each type T
delimited by &
in a TypeBound, the bound α<sub>l</sub> <: T[P<sub>1</sub>:=α<sub>1</sub>, ..., P<sub>p</sub>:=α<sub>p</sub>]
appears in the set […].
因此,这意味着编译器首先以 F <: Foo
的边界开始(这意味着 F
是 Foo
的子类型)。
移动到 18.5.2,return 目标类型被考虑:
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:
[…]
Otherwise, the constraint formula ‹R θ → T›
is reduced and incorporated with [the bound set].
约束公式 ‹R θ → T›
被简化为 R θ <: T
的另一个边界,所以我们有 F <: String
.
稍后根据18.4解决这些问题:
[…] a candidate instantiation T<sub>i</sub>
is defined for each α<sub>i</sub>
:
- Otherwise, where
α<sub>i</sub>
has proper upper bounds U<sub>1</sub>, ..., U<sub>k</sub>
, T<sub>i</sub> = glb(U<sub>1</sub>, ..., U<sub>k</sub>)
.
The bounds α<sub>1</sub> = T<sub>1</sub>, ..., α<sub>n</sub> = T<sub>n</sub>
are incorporated with the current bound set.
回想一下,我们的界限集是 F <: Foo, F <: String
。 glb(String, Foo)
定义为 String & Foo
。这显然是 glb 的合法类型,它只需要:
It is a compile-time error if, for any two classes (not interfaces) V<sub>i</sub>
and V<sub>j</sub>
, V<sub>i</sub>
is not a subclass of V<sub>j</sub>
or vice versa.
最后:
If resolution succeeds with instantiations T<sub>1</sub>, ..., T<sub>p</sub>
for inference variables α<sub>1</sub>, ..., α<sub>p</sub>
, let θ'
be the substitution [P<sub>1</sub>:=T<sub>1</sub>, ..., P<sub>p</sub>:=T<sub>p</sub>]
. 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
.
因此调用该方法时使用 String & Foo
作为 F
的类型。我们当然可以将其分配给 String
,因此不可能将 Foo
转换为 String
.
String
/Integer
是最终的 类 这一事实显然没有被考虑。
* 注意:键入 erasure is/was 与问题完全无关。
此外,虽然这也可以在 Java 7 上编译,但我认为可以说我们不必担心那里的规范是合理的。 Java 7 的类型推断本质上是 Java 8 的一个不太复杂的版本。它出于类似的原因进行编译。
作为附录,虽然很奇怪,但它可能永远不会导致尚未出现的问题。编写其 return 类型仅从 return 目标推断的泛型方法很少有用,因为只有 null
可以从这样的方法中 returned 而无需强制转换。
假设我们有一些地图模拟,它存储特定接口的子类型:
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
。
为什么下面的代码可以通过?
方法 IElement.getX(String)
return 是类型 IElement
或其子 class 类型的实例。 Main
class 中的代码调用 getX(String)
方法。编译器允许将 return 值存储到 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");
}
}
return 类型不应该仍然是 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
。
这实际上是一个合法的类型推断*。
我们可以将其简化为以下示例 (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:
When inference begins, a bound set is typically generated from a list of type parameter declarations
P<sub>1</sub>, ..., P<sub>p</sub>
and associated inference variablesα<sub>1</sub>, ..., α<sub>p</sub>
. Such a bound set is constructed as follows. For each l (1 ≤ l ≤ p):
[…]
Otherwise, for each type
T
delimited by&
in a TypeBound, the boundα<sub>l</sub> <: T[P<sub>1</sub>:=α<sub>1</sub>, ..., P<sub>p</sub>:=α<sub>p</sub>]
appears in the set […].
因此,这意味着编译器首先以 F <: Foo
的边界开始(这意味着 F
是 Foo
的子类型)。
移动到 18.5.2,return 目标类型被考虑:
If the invocation is a poly expression, […] let
R
be the return type ofm
, letT
be the invocation's target type, and then:
[…]
Otherwise, the constraint formula
‹R θ → T›
is reduced and incorporated with [the bound set].
约束公式 ‹R θ → T›
被简化为 R θ <: T
的另一个边界,所以我们有 F <: String
.
稍后根据18.4解决这些问题:
[…] a candidate instantiation
T<sub>i</sub>
is defined for eachα<sub>i</sub>
:
- Otherwise, where
α<sub>i</sub>
has proper upper boundsU<sub>1</sub>, ..., U<sub>k</sub>
,T<sub>i</sub> = glb(U<sub>1</sub>, ..., U<sub>k</sub>)
.The bounds
α<sub>1</sub> = T<sub>1</sub>, ..., α<sub>n</sub> = T<sub>n</sub>
are incorporated with the current bound set.
回想一下,我们的界限集是 F <: Foo, F <: String
。 glb(String, Foo)
定义为 String & Foo
。这显然是 glb 的合法类型,它只需要:
It is a compile-time error if, for any two classes (not interfaces)
V<sub>i</sub>
andV<sub>j</sub>
,V<sub>i</sub>
is not a subclass ofV<sub>j</sub>
or vice versa.
最后:
If resolution succeeds with instantiations
T<sub>1</sub>, ..., T<sub>p</sub>
for inference variablesα<sub>1</sub>, ..., α<sub>p</sub>
, letθ'
be the substitution[P<sub>1</sub>:=T<sub>1</sub>, ..., P<sub>p</sub>:=T<sub>p</sub>]
. 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 ofm
.
因此调用该方法时使用 String & Foo
作为 F
的类型。我们当然可以将其分配给 String
,因此不可能将 Foo
转换为 String
.
String
/Integer
是最终的 类 这一事实显然没有被考虑。
* 注意:键入 erasure is/was 与问题完全无关。
此外,虽然这也可以在 Java 7 上编译,但我认为可以说我们不必担心那里的规范是合理的。 Java 7 的类型推断本质上是 Java 8 的一个不太复杂的版本。它出于类似的原因进行编译。
作为附录,虽然很奇怪,但它可能永远不会导致尚未出现的问题。编写其 return 类型仅从 return 目标推断的泛型方法很少有用,因为只有 null
可以从这样的方法中 returned 而无需强制转换。
假设我们有一些地图模拟,它存储特定接口的子类型:
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
。