Почему «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 /*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 -> "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?


person charlie    schedule 08.01.2016    source источник
comment
Обратите внимание, что поведение лямбда-выражения фиксировано и не может быть изменено вызывающей стороной. Следовательно, недопустимый (но принятый Eclipse) синтаксис f.apply(O.super); не может иметь значения для f.apply(O.this);, поскольку это тот же объект, а поведение вызова фиксировано для этой функции. Вы не можете создать Function<Object, ?>, который игнорирует переопределения (с допустимыми конструкциями Java), но вы можете создать Function<O, ?>, который игнорирует переопределения, используя вспомогательный метод, подобный этим синтетическим access$n методам.   -  person Holger    schedule 08.01.2016
comment
Протестировано. ((Function<O, ?>) O::helper).apply(this), где private String helper() {return super.toString();} работает нормально. Однако он работает только на 1 уровень выше, и если вы не создадите цепочку помощников вверх по иерархии, вы никогда не получите настоящий Object::toString, верно? Спасибо, в любом случае.   -  person charlie    schedule 08.01.2016
comment
Для внутренних классов, когда помощником является private String helper() {return OuterMostClass.super.toString();}, он все равно будет вызывать только родителя OuterMostClass, а не Object, поэтому кажется, что простого способа Java не существует.   -  person charlie    schedule 08.01.2016
comment
... там, где подойдет простой invokespecial...   -  person charlie    schedule 08.01.2016
comment
Нет, это было давно, когда invokespecial разрешалось пропускать/нацеливаться на произвольные классы (Java 1.0). В настоящее время для не-2_ и не-3_ методов целевой тип должен быть прямым суперклассом содержащего класса. В противном случае проверяющий имеет право отклонить его. Было время, когда отсутствие флага ACC_SUPER могло усилить старое поведение, но самые последние JVM обрабатывают все классы так, как если бы флаг присутствовал.   -  person Holger    schedule 08.01.2016


Ответы (2)


Вызов формы 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 вернет что-то похожее на LambdaSuper@6b884d57, показывая, что он вызвал переопределенный метод Object.toString(), а не переопределяющий LambdaSuper.toString(). Кажется, что поставщики компиляторов осторожно относятся к возможностям JRE, и, к сожалению, эта часть немного недоопределена.

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

person Holger    schedule 08.01.2016
comment
Интересно, что мой тест с javac показал, что он даже генерирует два идентичных метода доступа для T.super.toString(), один из которых совершенно не используется. - person Holger; 08.01.2016
comment
Ну, Eclipse и javac дают совершенно разные результаты. Eclipse выдает 1 метод доступа и 2 ссылки на метод для этого метода доступа. В то время как javac выдает 2 метода доступа (один вызывается напрямую, другой из лямбда) и 2 лямбда-метода, из которых один вызывает метод доступа, а другой выполняет invokespecial на Object::toString напрямую, без метода доступа. - person charlie; 08.01.2016
comment
На ваш ответ: я могу принять необходимость в аксессоре в суперклассе (чтобы обойти переопределение), но почему он здесь, в самом классе? Поскольку super.toString() использует invokespecial, а не средство доступа, почему все остальные не делают то же самое? - person charlie; 08.01.2016
comment
Ну, javac генерирует несколько методов доступа, потому что добавляет к нему отладочную информацию строки. По той причине, почему он не обрабатывает T.super как super, когда T является текущим классом, неудобная правда заключается в том, что компиляторы не всегда генерируют наиболее эффективный код. - person Holger; 08.01.2016
comment
Другие примеры неоптимального вывода компилятора см. в разделе Зачем включать компиляцию строк в два переключателя или попробуйте с ресурсами ввести недостижимый байт-код - person Holger; 08.01.2016
comment
Относительно 1-го абзаца: я думаю, что в последнем предложении следует подчеркнуть, что оно применяется только для вызовов внешнего класса, то есть в форме T.super.method(). - person charlie; 08.01.2016
comment
Re T.super.method() при вызове в T: кажется, что цепочка методов доступа генерируется до указанного типа. В самом внешнем методе доступа типа, в T, содержится фактический вызов метода, в то время как промежуточные методы доступа служат для передачи ссылки непосредственно внешнему классу. Поэтому, если T является текущим классом, это вырожденный случай, когда будет создан только окончательный метод доступа с вызовом метода. - person charlie; 08.01.2016
comment
Для формы super::toString метод доступа создается по совершенно другой причине — я полагаю, что это замыкание, захватывающее экземпляр super, так как это ссылка на метод экземпляра. Так что этот случай не имеет ничего общего с обходом переопределения. - person charlie; 08.01.2016
comment
И последний случай, T.super::toString, представляет собой комбинацию двух. :о) - person charlie; 08.01.2016
comment
Спасибо за редактирование, теперь это действительно более понятно, поэтому я проголосовал за. Однако, если мои недавние выводы неверны, ваш ответ объясняет только 1,5 из 3 случаев. Я сам составлю ответ и буду ждать реакции других читателей. - person charlie; 08.01.2016
comment
Не существует такой вещи, как экземпляр super. Напишите ли вы this::toString или super::toString, в обоих случаях захватывается один и тот же экземпляр, меняется только тип вызова. Но this::toString не нужен метод доступа. Обратите внимание, что пример кода в моем ответе делает то же самое, что и super::toString, захватывая экземпляр, но без использования метода доступа. - person Holger; 08.01.2016
comment
Какой случай вы считаете безответным? - person Holger; 08.01.2016
comment
Но, как вы написали, разница в типе вызова (invokevirtual против invokespecial). Под экземпляром super я имел в виду invokespecial в родительском типе (с this в качестве цели, согласен). Итак, транслируется ли этот пример кода Oracle в invokespecial непосредственно внутри метода начальной загрузки? - person charlie; 08.01.2016
comment
Eclipse переводит super::toString в ссылку на метод для метода доступа, который просто принимает this в качестве аргумента, а () -> super.toString() преобразует в лямбда-метод, который напрямую использует this. OpenJDK использует в обоих случаях последний. - person charlie; 08.01.2016
comment
Да, OpenJDK предпочитает всегда использовать методы static для лямбда-выражений, даже если this захвачено. Поскольку методы, содержащие код явных лямбда-выражений, имеют номер private, это незначительная разница, так как это всегда будет невиртуальный вызов. И это должно ответить на другой вопрос: да, сгенерированные классы способны выполнять невиртуальные вызовы, включая методы private, и в этом примере Oracle создает класс, который выполняет invokespecial с предполагаемой семантикой супервызова напрямую. - person Holger; 08.01.2016
comment
О том, что осталось без ответа: объяснение строк /*3*/ и /*4*/ как замыканий, что является причиной синтетического метода, особенно в случае super::toString, который не является случаем цепочки доступа к внешнему классу. Тем не менее, я хотел бы услышать, является ли invokespecial законным в методе начальной загрузки (как это предлагается в примере с Oracle). - person charlie; 08.01.2016
comment
На самом деле все наоборот: OpenJDK не использует статические методы (я имел в виду использует this напрямую), поэтому он виртуальный. Это Eclipse, который использует статический метод, но только в случае methodref, а не для лямбда-выражения. - person charlie; 08.01.2016
comment
Замыкание не причина. Как было сказано, this::method также является замыканием и не нуждается в таком методе. Все дело в доступности и типе вызова. invokespecial всегда допустимо, если вы вызываете метод private своего собственного класса, поскольку все лямбда-выражения компилируются в private синтетические методы. Что касается super вызовов, как уже было сказано, это немного занижено. - person Holger; 08.01.2016
comment
Хорошо, так хотя бы часть причины? То, что this::method не требует синтетического метода, очевидно. А super.method() идет и без. И для Oracle тоже super::method не обойтись. Таким образом, invokespecial может перейти непосредственно к методу начальной загрузки (лямбда-метафабрика), и как Eclipse, так и OpenJDK также могут работать без доступа, верно? Таким образом, причиной будет Eclipse, OpenJDK, а не закрытие. - person charlie; 08.01.2016
comment
Надеюсь, последний вопрос: как Oracle действует на () -> this.method()? Наконец-то он должен создать лямбда-метод, не так ли? - person charlie; 08.01.2016
comment
() -> this.method() всегда компилируется в лямбда-метод. Можно было бы распознать этот шаблон и заменить его эквивалентной ссылкой на метод, но 1.) кто-то должен реализовать его и создать тестовые примеры и т. д. и 2.) эта гипотетическая оптимизация не должна применяться, когда атрибуты отладки строки включены. (они есть по умолчанию), так как номера строк исходного кода лямбда-выражения прикрепляются к этому синтетическому лямбда-методу. - person Holger; 08.01.2016

Хольгер уже объяснил, почему это происходит 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.

N.B.: Здесь только 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 отличается созданием синтетических лямбда-методов экземпляра для ссылок на методы вместо статических синтетических методов доступа, как это делает 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);
            }
        }
    }
}


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

person charlie    schedule 08.01.2016