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

Ссылка на метод частного интерфейса

Рассмотрим следующий код:

public class A {
    public static void main(String[] args) {
        Runnable test1 = ((I)(new I() {}))::test;  // compiles OK
        Runnable test2 = ((new I() {}))::test;     // won't compile 
    }

    interface I {
        private void test() {}
    }
}

Я действительно не понимаю... Я понимаю, что метод test() частный. Но что изменилось, если мы включили анонимный класс в свой интерфейс ((I)(new I() {}))? Точнее, я хотел бы видеть конкретную точку JLS, которая допускает этот трюк.

P.S. Я сообщил об этом как о компиляторе (ID: 9052217). Мне кажется, что Runnable test2 = ((new I() {}))::test; должен быть скомпилирован в этом конкретном случае.

P.P.S. До сих пор была создана ошибка, основанная на моем отчете: https://bugs.openjdk.java.net/browse/JDK-8194998. Возможно, он будет закрыт, поскольку "не исправит" или что-то еще.

4b9b3361

Ответ 1

Это не новая проблема и не имеет ничего общего с методами частного интерфейса или ссылками на методы.

Если вы изменяете код для расширения класса вместо реализации интерфейса и для вызова метода вместо ссылки на него, вы по-прежнему получаете ту же самую проблему.

class A {
    public static void main(String[] args) {
        ((I)(new I() {})).test();  // compiles OK
        ((new I() {})).test();     // won't compile 
    }

    class I {
        private void test() {}
    }
}

Однако этот код можно применить к старым версиям Java, и я пробовал Java 9, 8, 7, 6, 5 и 1.4. Все ведут себя одинаково!!

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

Когда вы добавили к I, теперь существует метод для компилятора, и поскольку I является внутренним классом, вам предоставляется доступ (через синтетический метод), хотя он является частным.

По-моему, это не ошибка. Это как частные методы работают в контексте наследования.

1) Как найденный Йорном Верни в JLS 6.6-5: "[Частный член класса] не наследуется подклассами.

Ответ 2

private методы не наследуются (ближе всего я нашел: JLS6.6-5: "[Частный член класса] не наследуется подклассами" ). Это означает, что вы не можете получить доступ к частному методу из подтипа (потому что он просто не имеет этого метода). Например:

public static void main(String[] args) {
    I1 i1 = null;
    I2 i2 = null;

    i1.test(); // works
    i2.test(); // method test is undefined
}

interface I1 {
    private void test() {}
}

interface I2 extends I1 {}

Это также означает, что вы не можете напрямую обращаться к методу test через тип анонимного подкласса. Тип выражения:

(new I() {})

Не I, но фактически не обозначаемый тип анонимного подкласса, поэтому вы не можете получить доступ к test через него.

Однако тип выражения:

((I) (new I() {}))

is I (поскольку вы явно передали его в I), поэтому вы можете получить доступ к этому методу test. (так же, как вы можете сделать ((I1) i2).test(); в моем примере выше)

Аналогичные правила применяются к методам static, так как они также не наследуются.

Ответ 3

Это противоречит интуиции. Сначала немного упростите это:

static interface Inter {
    private void test() {
        System.out.println("test");
    }
}


public static void main(String[] args) {
    ((Inter) new Inter() {
    }).hashCode();
}

Это имеет смысл, так как вы вызываете общедоступный метод hashCode, вот для него (байт) для него:

public static void main(java.lang.String[]);
Code:
   0: new           #2   // class com/test/DeleteMe$1
   3: dup
   4: invokespecial #3   // Method com/test/DeleteMe$1."<init>":()V
   7: invokevirtual #4   // Method java/lang/Object.hashCode:()I
  10: pop
  11: return

Мне очень нравится. Теперь измените это на вызов test():

public static void main(String[] args) {
    ((Inter) new Inter() {
    }).test();
}

Байт-код для этого:

 invokestatic  #4  // InterfaceMethod com/test/DeleteMe$Inter.access$000:(Lcom/test/DeleteMe$Inter;)V

Так как частные методы не наследуются, вы фактически "переходите" к этому методу с помощью статического синтетического метода access$n.

Ответ 4

Вызов метода private возможен только через выражение точно типа объявления независимо от сценария.

Давайте объясним это простейшим примером

public class A {
    public static void main(String[] args) {
        B b = new B();
        b.someMethod(); // does not compile
        A a = b;
        a.someMethod(); // no problem
    }
    private void someMethod() {}
}
class B extends A {
}

Вы можете ожидать, что это скомпилируется с помощью b.someMethod() для вызова A s someMethod(). Но что, если B было объявлено как

class B extends A {
    public void someMethod() {}
}

Это возможно, так как private void someMethod() не наследуется, поэтому public void someMethod() не переопределяет его. Но должно быть ясно, что теперь b.someMethod() должен вызывать метод B s.

Итак, если было разрешено, что b.someMethod() заканчивается с помощью private метода A, это будет зависеть от того, объявит ли B другой someMethod(), при котором фактический метод вызовет вызов. И это явно противоречит всему понятию методов private. private методы не наследуются и никогда не переопределяются, поэтому он не должен зависеть от подкласса, заканчивается ли вызов с помощью метода private или метода подкласса.

Ваш пример аналогичен. Анонимный внутренний класс, реализующий I, может объявить свой собственный метод test(), например. Runnable test2 = ((new I() {void test() {}}))::test;, поэтому он будет зависеть от этого анонимного внутреннего класса, будет ли вызван метод private I или метод этого анонимного внутреннего класса, что было бы неприемлемым. Конечно, с таким внутренним классом, непосредственно предшествующим обращению к вызову или методу, читатель может сразу сказать, при каком способе вызов будет завершен, но это было бы очень непоследовательно, если бы это было разрешено для анонимного внутреннего класса, но ничего иначе.

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