考虑以下一组表达式:
class T {{
/*1*/ super.toString(); // direct
/*2*/ T.super.toString(); // synthetic
Supplier<?> s;
/*3*/ s = super::toString; // synthetic
/*4*/ s = T.super::toString; // synthetic
}}
得出以下结果:
class T {
T();
0 aload_0 [this]
1 invokespecial java.lang.Object() [8]
4 aload_0 [this]
5 invokespecial java.lang.Object.toString() : java.lang.String [10]
8 pop // ^-- direct
9 aload_0 [this]
10 invokestatic T.access$0(T) : java.lang.String [14]
13 pop // ^-- synthetic
14 aload_0 [this]
15 invokedynamic 0 get(T) : java.util.function.Supplier [21]
20 astore_1 [s] // ^-- methodref to synthetic
21 aload_0 [this]
22 invokedynamic 1 get(T) : java.util.function.Supplier [22]
27 astore_1 // ^-- methodref to synthetic
28 return
static synthetic java.lang.String access$0(T arg0);
0 aload_0 [arg0]
1 invokespecial java.lang.Object.toString() : java.lang.String [10]
4 areturn
Bootstrap methods:
0 : # 40 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:...
#43 invokestatic T.access$0:(LT;)Ljava/lang/String;
1 : # 40 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:...
#46 invokestatic T.access$0:(LT;)Ljava/lang/String;
}
为什么java代码行/*2*/
、/*3*/
和/*4*/
生成并使用合成访问器方法access$0
?我期望线路/*2*/
以及线路/*3*/
和/*4*/
的引导方法也像线路/*1*/
那样使用invokespecial
。
特别是当方法Object::toString
可以直接从相关范围访问时,例如,以下方法引用没有包装对合成访问器方法的调用:
class F {{
Function<Object, ?> f = Object::toString; // direct
}}
然而,存在差异:
class O {{
super.toString(); // invokespecial -> "className@hashCode"
O.super.toString(); // invokespecial -> "className@hashCode"
Supplier<?> s;
s = super::toString; // invokespecial -> "className@hashCode"
s = O.super::toString; // invokespecial -> "className@hashCode"
Function<Object, ?> f = Object::toString;
f.apply(O.super); // invokeinterface -> "override"
}
public String toString() {return "override";}
}
这带来了另一个问题:有没有办法绕过((Function<Object, ?> Object::toString)::apply
中的覆盖?
对表单super.method()
的调用允许绕过同一类中的重写method()
,调用超类层次结构中最特定的method()
。由于在字节码级别上,只有声明类本身可以忽略其自己的重写方法(以及子类的潜在重写方法),因此如果这种调用应由不同的(但在概念上有标题的)类执行,如其内部类之一,则将生成合成访问器方法,格式为Outer.super.method(...)
,或者为方法引用生成的合成类。
请注意,即使一个类没有覆盖被调用的方法,而且似乎没有什么区别,调用也必须以这种方式编译,因为在运行时可能会有子类覆盖该方法,然后,它就会产生影响。
有趣的是,当T
实际上不是外部类而是包含语句的类时,使用T.super.method()
也会发生同样的事情。在这种情况下,helper方法并不是真正必要的,但编译器似乎统一地实现了形式identifier.super.method(...)
的所有调用。
附带说明一下,Oracle的JRE在为lambda表达式/方法引用生成类时能够绕过这种字节码限制,因此,super::methodName
类型的方法引用不需要访问器方法,如下所示:
import java.lang.invoke.*;
import java.util.function.Supplier;
public class LambdaSuper {
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup l=MethodHandles.lookup();
MethodType mt=MethodType.methodType(String.class);
MethodHandle target=l.findSpecial(Object.class, "toString", mt, LambdaSuper.class);
Supplier<String> s=(Supplier)LambdaMetafactory.metafactory(l, "get",
MethodType.methodType(Supplier.class, LambdaSuper.class),
mt.generic(), target, mt).getTarget().invokeExact(new LambdaSuper());
System.out.println(s.get());
}
@Override
public String toString() {
return "overridden method";
}
}
生成的Supplier
将返回类似的LambdaSuper@6b884d57
,表明它调用了重写的Object.toString()
方法而不是重写的LambdaSuper.toString()
。编译器供应商似乎对JRE功能的预期很谨慎,不幸的是,这一部分似乎有点未明确。
不过,对于真正的内部类场景,它们是必需的。
Holger已经解释了为什么会发生这种情况—super
引用仅限于直接子类。这里只是一个更详细的版本,说明那里真正发生的事情:
对封闭类型的超类方法的调用
class T {
class U {
class V {{
/*2*/ T.super.toString();
}}
}
}
它生成一系列合成访问器方法:
class T {
static synthetic String access$0(T t) { // executing accessor
return t.super.toString(); // only for the refered outer class
}
class U { // new U(T.this)
static synthetic T access$0(U u) { // relaying accessor
return T.this; // for every intermediate outer class
}
class V {{ // new V(U.this)
T.access$0(U.access$0(U.this)); // T.access$0(T.this)
}}
}
}
当T
是直接封闭类时,即没有中间外部类,在类T
中只生成"执行"访问器(即在其本身,这似乎是不必要的)。
注意:访问器链是由Eclipse生成的,但不是由OpenJDK产生的,请参阅下文。
对自己的超类方法的方法引用
class T {
class U {
class V {{
Supplier<?> s;
/*3*/ s = super::toString;
}}
}
}
这将生成一个合成访问器方法和一个委托给它的引导方法:
class T {
class U {
class V {
static synthetic String access$0(V v) {
return v.super.toString();
}
dynamic bootstrap Supplier get(V v) { // methodref
return () -> V.access$0(v); // toString() adapted to get()
}
{
get(V.this);
}
}
}
}
这是一个类似于前一个的奇异情况,因为super::toString
在这里等价于V.super::toString
,所以合成访问器是在类V
本身中生成的。这里的一个新元素是用于将CCD_ 30适配为CCD_。
注意:这里只有OracleJDK足够"智能"(正如Holger所指出的),可以通过将super
调用直接放入方法引用适配器来避免合成访问器。
对封闭类型的超类"方法的方法引用
class T {
class U {
class V {{
Supplier<?> s;
/*4*/ s = T.super::toString;
}}
}
}
正如您所料,这是前面两种情况的组合:
class T {
static synthetic String access$0(T t) { // executing accessor
return t.super.toString(); // only for the refered outer class
}
class U { // new U(T.this)
static synthetic T access$0(U u) { // relaying accessor
return T.this; // for every intermediate outer class
}
class V { // new V(U.this)
dynamic bootstrap Supplier get(T t) { // methodref
return () -> T.access$0(t); // toString() adapted to get()
}
{
get(U.access$0(U.this)); // get(T.this)
}
}
}
}
这里没有什么新鲜事,只需注意,内部类总是只接收直接外部类的实例,因此在类V
中,使用T.this
,它可能会经过整个中间合成访问器方法链,例如U.access$0(V.U_this)
(如在Eclipse中),或者利用这些合成字段(引用outer.this
)的包可见性,并将T.this
转换为V.U_this.T_this
(如在OpenJDK中)。
注意:以上翻译是根据Eclipse编译器进行的OpenJDK的不同之处在于为方法引用生成实例合成lambda方法,而不是像Eclipse那样生成static综合访问器方法,并且还避免了访问器链,因此在最后一种情况下OpenJDK发出:
class T {
static synthetic String access$0(T t) { // executing accessor
return t.super.toString(); // only for the refered outer class
}
class U { // new U(T.this)
class V { // new V(U.this)
instance synthetic Object lambda$0() {
return T.access$0(V.U_this.T_this); // direct relay
}
dynamic bootstrap Supplier get(V v) { // methodref
return () -> V.lambda$0(v); // lambda$0() adapted to get()
}
{
get(V.this);
}
}
}
}
总之,这在很大程度上取决于编译器供应商。