Подтвердить что ты не робот

Почему "T.super.toString()" и "super:: toString" используют синтетический метод доступа?

Рассмотрим следующий набор выражений:

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 code /*2*/, /*3*/ и /*4*/ производят и используют синтетический метод доступа access$0? Я бы ожидал, что строки /*2*/ и методы бутстрапа для строк /*3*/ и /*4*/ также будут использовать invokespecial, как это делает строка /*1*/.

Особенно, когда метод Object::toString доступен непосредственно из соответствующей области, например. следующая ссылка метода не переносит вызов синтетического метода доступа:

class F {{
    Function<Object, ?> f = Object::toString; // direct
}}

Однако там есть разница:

class O {{
        super.toString();      // invokespecial -> "[email protected]"
        O.super.toString();    // invokespecial -> "[email protected]"
        Supplier<?> s;
        s = super::toString;   // invokespecial -> "[email protected]"
        s = O.super::toString; // invokespecial -> "[email protected]"
        Function<Object, ?> f = Object::toString;
        f.apply(O.super); // invokeinterface -> "override"
    }
    public String toString() {return "override";}
}

Что вызывает другой вопрос: существует ли способ обхода переопределения в ((Function<Object, ?> Object::toString)::apply?

4b9b3361

Ответ 1

Вызов формы super.method() позволяет обойти переопределение method() в том же классе, вызывая наиболее специфическую method() иерархии суперклассов. Поскольку на уровне байтового кода только сам класс объявления может игнорировать свой собственный метод переопределения (и возможные методы переопределения подклассов), синтетический метод доступа будет создан, если этот вид вызова должен выполняться другим (но концептуально озаглавленным), как один из его внутренних классов, используя форму Outer.super.method(...) или синтетический класс, сгенерированный для ссылки на метод.

Обратите внимание, что даже если класс не переопределяет вызываемый метод и, похоже, не имеет значения, вызов должен быть скомпилирован таким образом, поскольку во время выполнения могут быть подклассы, переопределяющие этот метод, и тогда это будет иметь значение.

Интересно, что то же самое происходит при использовании T.super.method(), когда T фактически не является внешним классом, а классом, содержащим инструкцию. В этом случае вспомогательный метод на самом деле не нужен, но кажется, что компилятор реализует все вызовы формы identifier.super.method(...) равномерно.


В качестве побочного примечания Oracle JRE способен обойти это ограничение байтового кода при создании классов для лямбда-выражений/ссылок на методы, поэтому методы доступа не нужны для ссылок на методы типа 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 возвращает что-то похожее [email protected], показывающее, что он вызывает переопределенный метод Object.toString(), а не переопределяющий LambdaSuper.toString(). Похоже, что поставщики компилятора внимательно следят за тем, что ожидать от возможностей JRE, и, к сожалению, эта часть немного отстала.

Тем не менее, для реальных сценариев внутреннего класса они требуются.

Ответ 2

Хольгер уже объяснил , почему это происходит — 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 создается только "исполняющий" аксессор (т.е. сам по себе, что кажется ненужным).

N.B.: Цепочка доступа генерируется 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.

NB: здесь только 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 отличается тем, что генерирует синтетические лямбда-методы экземпляра для ссылок на методы, вместо синтетических методов доступа static, как это делает Eclipse, а также избегает цепи доступа, поэтому в последнем случае 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);
            }
        }
    }
}


Подводя итог, он вполне зависит от поставщика компилятора.