【问题标题】:Why 'T.super.toString()' and 'super::toString' use a synthetic accessor method?为什么 'T.super.toString()' 和 'super::toString' 使用合成访问器方法?
【发布时间】:2016-04-13 00:22:49
【问题描述】:

考虑以下一组表达式:

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&lt;Object, ?&gt; Object::toString)::apply 中的覆盖?

【问题讨论】:

  • 请注意,lambda 表达式的行为是固定的,不能由调用者更改。因此,无效(但被 Eclipse 接受)语法 f.apply(O.super); 不能对 f.apply(O.this); 产生影响,因为它是同一个对象,并且此函数的调用行为是固定的。您不能创建一个忽略覆盖的Function&lt;Object, ?&gt;(使用合法的Java 结构),但您可以使用一个辅助方法创建一个忽略覆盖的Function&lt;O, ?&gt;,类似于这些合成的access$n 方法。
  • 经过测试。 ((Function&lt;O, ?&gt;) O::helper).apply(this) 其中private String helper() {return super.toString();} 工作正常。但是,它只能向上工作 1 级,除非您在层次结构上创建一个助手链,否则您将永远无法获得真正的 Object::toString,对吗?还是谢谢。
  • 对于内部类,当helper为private String helper() {return OuterMostClass.super.toString();}时,它仍然只会调用OuterMostClass的父类,而不是Object,所以似乎没有普通的java方式。
  • ...一个简单的invokespecial 就可以了...
  • 不,那是很久以前的事了,当时invokespecial 允许跳过/定位任意类(Java 1.0)。如今,对于非private、非interface 方法,目标类型必须是包含类的直接超类。否则,验证者有权拒绝它。曾经有一段时间,ACC_SUPER 标志的缺失可能会强化旧的行为,但最近的 JVM 将所有类视为是否存在标志。

标签: java java-8 super method-reference synthetic


【解决方案1】:

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本身中生成的。这里的一个新元素是用于将Object::toString 适配为Supplier::get 的引导方法。

注意:这里只有 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 )。


N.B.:以上翻译是根据 Eclipse 编译器。 OpenJDK 的不同之处在于为方法引用生成 instance 合成 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);
            }
        }
    }
}


总而言之,它非常依赖于编译器供应商。

【讨论】:

    【解决方案2】:

    super.method() 形式的调用允许绕过同一类中的覆盖method(),调用超类层次结构中最具体的method()。因为,在字节码级别,只有声明类本身可以忽略它自己的覆盖方法(以及子类的潜在覆盖方法),如果这种调用应该由不同的(但概念上称为) 类,就像它的内部类之一一样,使用Outer.super.method(...) 形式,或为方法引用生成的合成类。

    请注意,即使一个类没有覆盖被调用的方法并且似乎没有任何区别,调用也必须以这种方式编译,因为在运行时可能有子类覆盖该方法,然后,它会有所作为.

    有趣的是,当T 实际上不是外部类而是包含语句的类时,使用T.super.method() 时会发生同样的事情。在这种情况下,辅助方法并不是必需的,但编译器似乎统一实现了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 功能的期望很谨慎,不幸的是,这部分似乎有点未指定。

    不过,对于真正的内部类场景,它们必需的。

    【讨论】:

    • 有趣的是,我对javac 的测试表明,它甚至为T.super.toString() 生成了两个 相同的访问器方法,其中一个完全未使用。
    • 嗯,javac 生成多个访问器方法,因为它向其中添加了行调试信息。原因是,当T 是当前类时,为什么它不将T.super 视为super,令人不安的事实是编译器并不总是生成最有效的代码。
    • 对于其他低于最佳编译器输出的示例,请参阅Why switch on String compiles into two switchestry with resources introduce unreachable bytecode
    • 被关闭是不是的原因。如前所述,this::method 也是一个闭包,不需要这样的方法。这都是关于可访问性和调用类型的。 invokespecial 在调用自己类的 private 方法时始终是合法的,因为所有 lambda 表达式都编译为 private 合成方法。关于super 调用,如上所述,它有点未指定。
    • () -&gt; this.method() 总是被编译成一个 lambda 方法。可以识别这种模式并用等效的方法引用替换它,但是 1.) 必须有人实现它并创建测试用例等,以及 2.) 当线路调试属性打开时,这种假设的优化不应该应用(默认情况下它们是)作为 lambda 表达式的源代码行号附加到该合成 lambda 方法。
    猜你喜欢
    • 2015-01-31
    • 1970-01-01
    • 2013-04-03
    • 2011-05-28
    • 2010-11-09
    • 2011-09-01
    • 2012-08-30
    • 2021-01-26
    • 1970-01-01
    相关资源
    最近更新 更多