Я хочу напечатать привет, дедушка, но, кажется, напечатает привет, отец

Я хочу распечатать hi GrandFather

Но, кажется, печатает привет, отец. И я не понимаю, как различать использование между findSpecial и findVirtual

Я хочу, чтобы кто-нибудь помог мне. Спасибо

class GrandFather{
    void thinking(){
        System.out.println("hi GrandFather");

    }
}
class Father extends GrandFather{
    void thinking(){
        System.out.println("hi Father");
    }
}
class Son extends Father{
    void thinking(){
            MethodType mt=MethodType.methodType(void.class);

            //MethodHandle mh=MethodHandles.lookup().findVirtual(GrandFather.class,"thinking",mt).bindTo(this);
            MethodHandle mh;
                mh = MethodHandles.lookup().findSpecial(GrandFather.class,"thinking",mt,getClass());
                mh.invoke(this);
    }
}
public static void main(String[] args){
    (new MethodHandleTest().new Son()).thinking();
}

скриншот консоли, на котором напечатан привет, отец


person Chronos    schedule 20.02.2020    source источник
comment
Там вы можете найти дополнительную информацию: stackoverflow.com/questions/586363/. В Java вы не можете вызывать super.super.method() (как в Python), поэтому вы НЕ можете получить доступ к более чем одному классу выше.   -  person Boken    schedule 20.02.2020


Ответы (2)


Последний аргумент findSpecial указывает класс контекста для вызова. Вы указали getClass(), что приведет к Son.class. Вызов super из Son может закончиться только Father, как и обычный вызов super.thinking() в исходном коде.

Вам нужно указать Father.class в качестве класса контекста, так как Father разрешено выполнять super вызов GrandFather. Если вы сделаете это без дальнейших изменений, вы получите исключение вроде java.lang.IllegalAccessException: no private access for invokespecial…. Вы должны изменить контекст вашего объекта lookup(), чтобы получить доступ к private функциям Father:

MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = MethodHandles.lookup().in(Father.class)
    .findSpecial(GrandFather.class, "thinking", mt, Father.class);
mh.invoke(this);

Это работает, потому что Son и Father являются внутренними классами одного и того же класса верхнего уровня, поэтому разрешен доступ к закрытым членам друг друга. Если бы они не были классами в одном и том же классе верхнего уровня, in(…) изменил бы класс контекста, но очистил бы разрешение на частный доступ. В этом случае официальное решение есть только для Java 9 и новее:

MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = MethodHandles.privateLookupIn(Father.class, MethodHandles.lookup())
    .findSpecial(GrandFather.class, "thinking", mt, Father.class);
mh.invoke(this);

Это работает, когда Father и Son находятся в одном модуле или модуль Father открыл пакет Father для модуля Son для отражения.

person Holger    schedule 20.02.2020
comment
могу я спросить, зачем нужен аргумент первого класса? Если вы говорите, что сборка invokeSpecial основана на Son.class - она ​​может только перейти к Farther.class, так в чем смысл первого аргумента? Я знаю, что что-то упускаю, но этот API никогда не имел для меня смысла. - person Eugene; 21.02.2020
comment
@Eugene, семантика должна соответствовать инструкции байт-кода invokespecial, следовательно, тип получателя имеет значение, когда вы хотите вызвать методы default интерфейсов. Кроме того, вы можете использовать его для вызова private методов в вашем собственном классе. Вопрос должен быть наоборот, зачем нам нужен последний аргумент, когда единственное разумное значение для параметра caller-class — это класс поиска, уже инкапсулированный в объекте Lookup. - person Holger; 21.02.2020

Хольгер как всегда прав. На самом деле мне потребовалось некоторое время, чтобы понять, что происходит, пожалуйста, относитесь к этому как к поправке к другому очень хорошему ответу. Чтобы понять это, мне пришлось сделать 3 разных примера.

public class FindSpecialFailure {

    public static void main(String[] args) {
        try {
            MethodType mt = MethodType.methodType(void.class);
            Lookup l = MethodHandles.lookup();
            MethodHandle mh = l.findSpecial(Son.class, "go", mt, Son.class);
            mh.invoke(new Son());
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    static class Parent {
        void go() {
            System.out.println("parent");
        }
    }

    static class Son extends Parent {
        void go() {
            System.out.println("son");
        }
    }
}

Если вы запустите это, произойдет сбой с java.lang.IllegalAccessException: no private access for invokespecial.... Документация дает правильное направления, почему это происходит:

В общем, условия, при которых можно искать дескриптор метода для метода M, не более ограничительны, чем условия, при которых класс поиска мог бы скомпилировать, проверить и разрешить вызов M.

В той же документации это также объясняется:

Класс вызывающего объекта, к которому применяются эти ограничения, называется классом поиска.

В нашем случае lookup class – это FindSpecialFailure, и, таким образом, это будет использоваться, чтобы определить, можно ли компилировать, проверить и разрешить метод go из Son.class.

Вы можете думать об этом проще, немного. Сможете ли вы (теоретически) создать инструкцию invokeSpecial байтового кода в FindSpecialFailure::main и вызвать ее? Опять же, в теории. Вы можете создать его там:

invokeSpecial Son.go:()V

Вы можете вызвать его, хотя? Нет; конкретно, в моем понимании, это правило было бы нарушено:

Если символическая ссылка называет класс (не интерфейс), то этот класс является суперклассом текущего класса.

Очевидно, что Son не является суперклассом FindSpecialFailure.

Чтобы доказать мою точку зрения, вы можете изменить приведенный выше код на (второй пример):

// example 1
public class FindSpecialInterface {

    public static void main(String[] args) {
        try {
            MethodType mt = MethodType.methodType(void.class);
            Lookup l = MethodHandles.lookup();
            // <---- Parent.class
            MethodHandle mh = l.findSpecial(Parent.class, "go", mt, Son.class);
            mh.invoke(new Son());
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    // <---- this is now an interface
    interface Parent {
        default void go() {
            System.out.println("parent");
        }
    }

    static class Son implements Parent {
        public void go() {
            System.out.println("son");
        }
    }
}

На этот раз все будет работать нормально, потому что (из той же спецификации):

В противном случае, если C является интерфейсом, а объект класса содержит объявление общедоступного метода экземпляра с тем же именем и дескриптором, что и разрешенный метод, то вызывается именно этот метод.

Как мне исправить первый пример? (FindSpecialFailure) Нужно добавить интересную опцию:

public static void main(String[] args) {
    try {
        MethodType mt = MethodType.methodType(void.class);
        // <--- Notice the .in(Son.class) --> 
        Lookup l = MethodHandles.lookup().in(Son.class);
        MethodHandle mh = l.findSpecial(Son.class, "go", mt, Son.class);
        mh.invoke(new Son());
    } catch (Throwable t) {
        t.printStackTrace();
    }
} 

Если вы перейдете к ту же документацию вы найдете:

В некоторых случаях доступ между вложенными классами обеспечивается компилятором Java путем создания метода-оболочки для доступа к закрытому методу другого класса в том же объявлении верхнего уровня. Например, вложенный класс C.D может обращаться к закрытым членам других связанных классов, таких как C, C.D.E или C.B, но компилятору Java может потребоваться сгенерировать методы-оболочки в этих связанных классах. В таких случаях объект Lookup в C.E не сможет получить доступ к этим закрытым членам. Обходным путем для этого ограничения является метод Lookup.in, который может преобразовать поиск в CE в любой из этих других классов без специального повышения привилегий.

Третий пример почему-то начинает больше походить на ваши примеры:

public class FinSpecialMoveIntoSon {

    public static void main(String[] args) {
        new Son().invokeMe();
    }

    static class Parent {
        public void go() {
            System.out.println("parent");
        }
    }

    static class Son extends Parent {

        void invokeMe() {
            try {
                MethodType mt = MethodType.methodType(void.class);
                Lookup l = MethodHandles.lookup();
                MethodHandle mh = l.findSpecial(Son.class, "go", mt, Son.class);
                mh.invoke(new Son());
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        public void go() {
            System.out.println("son");
        }
    }
}

Дело в том, что в документации findSpecial говорится о первом параметре:

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

Вот почему он напечатает Son, а не Parent.


Вооружившись этим, ваш пример становится легче понять:

static class Son extends Father {

    void thinking() {
        try {
            MethodType mt = MethodType.methodType(void.class);
            MethodHandle mh = MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
            mh.invoke(this);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

класс поиска равен Son.class, а разрешение метода и refc (здесь класс или интерфейс, из которого осуществляется доступ к методу) равен GranFather. Таким образом, разрешение действительно начинается с GrandFather::thinking, но, поскольку вы не можете вызывать методы super.super в java, оно "понижено" до Father::thinking.

Все, что я мог предложить здесь, это использовать .in для решения этой проблемы, я не знал о privateLookupIn.

person Eugene    schedule 25.02.2020