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

"метод ___() в ___ определяется в недоступном классе или интерфейсе" ошибка компиляции

Я нашел странное ограничение компиляции, которое я не могу объяснить, и я не понимаю эту причину ограничения.

Пример-1:

Рассмотрим эти классы:

В package e1;:

public class C1 {
    enum E1 { A, B, C }
    public E1 x;
}

В package e2;:

import e1.C1;
public class C2 {
    public String test(C1 c1) {
        return c1.x.toString();    // here compilation error
    } 
}

Это приводит к следующей ошибке компиляции:

Ошибка: (5,20) java: toString() в java.lang.Enum определяется в недоступном классе или интерфейсе

Пример-2:

Рассмотрим эти классы:

В package i1;:

public interface I1 {
    int someMethod();
}

public class C1 {
    static class I2 implements I1 {
        public int someMethod() {
            return 1;
        }
    }
    public I2 x = new I2();
}

В package i2;:

import i1.C1;
import i1.I1;
public class C2 {
    public static void main(String[] args) {
        C1 c1 = new C1();
        System.out.println(c1.x.someMethod());  // compilation error
    }
}

Это также вызывает ту же ошибку компиляции, но если мы изменим строку нарушения на:

System.out.println(((I1)c1.x).someMethod());

Тогда это может быть скомпилировано и отлично работает.


Итак, вопрос:

Почему это ограничение доступности необходимо?

Да, я понимаю, что классы C1.E в примере-1) и C1.I2 в примере-2) являются закрытыми для пакета. Но в то же время ясно, что никто не может назначить более слабый доступ привилегии к методам базового интерфейса (I1 of Object), поэтому всегда будет безопасно делать прямое литье объекта в его базовый интерфейс и получать доступ к ограниченному методу.

Может ли кто-нибудь объяснить цели и причину этого ограничения?

UPDATE: assylias указал JLS §6.6 +0,1:

Элемент (класс, интерфейс, поле или метод) ссылочного типа (класса, интерфейса или массива) или конструктор типа класса доступен только в том случае, если тип доступен...

Похоже, это ограничение, но оно не объясняет почему это ограничение (в случае вышеупомянутых примеров) необходимо...

4b9b3361

Ответ 1

Для вызова методов экземпляра используется invokevirtual. Для вызова этого метода класс должен иметь разрешенную ссылку на этот метод

Из invokevirtual спецификации:

Связывание исключений

Во время разрешения символической ссылки на метод любой из могут быть выбраны исключения, относящиеся к разрешению метода (§5.4.3.3).

5.4.3.3. Разрешение метода:

Чтобы разрешить неразрешенную символическую ссылку от D к методу в класс C, символическая ссылка на C, заданная ссылкой метода сначала разрешено (§5.4.3.1).

5.4.3.1. Разрешение класса и интерфейса:

Если C не доступно (§5.4.4) до D, класса или разрешения интерфейса бросает IllegalAccessError.

5.4.4. Контроль доступа:

Класс или интерфейс C доступен для класса или интерфейса D, если и только если выполнено одно из следующих условий:

  • C является общедоступным.

  • C и D являются членами одного и того же пакета времени выполнения (§5.3).

C и D не из одного пакета. Поэтому, даже если java компилирует этот код для вас, он будет вызывать IllegalAccessError во время вызова. Компилятор достаточно умен, чтобы предотвратить такие очевидные ошибки. Эти ограничения исходят из требований процесса разрешения класса Java.

Для вызова метода экземпляра JVM требуется две вещи: ссылка на объект и описание объекта (класса или интерфейса). Описание доступно через процесс разрешения. Если это не удается, вызов завершается неудачно.

Если во время разрешения символьной ссылки возникает ошибка, тогда экземпляр IncompatibleClassChangeError (или подкласс) должен быть бросается в точку в программе, которая (прямо или косвенно) использует символическая ссылка.

В вашем случае C2 имеет доступ к I1. Таким образом, вызов интерфейса работает хорошо. Но C2 не имеет доступа к классу I2. И вот почему IllegalAccessError может быть запущен во время выполнения, если этот код компилируется.

Как воспроизвести IllegalAccessError:

  • Сделать внутренний класс общедоступным и скомпилировать все в среде IDE, например
  • Сделать внутренний пакет класса приватным и скомпилировать его с помощью javac из командной строки.
  • Замените классы, сгенерированные IDE, сгенерированными с помощью cmd
  • И вы увидите что-то вроде:

Исключение в потоке "main" java.lang.IllegalAccessError: попытался класс доступа     qq.Test1 $I2 из класса Test             в Test.main(Test.java:30)

Ответ 2

Элементы управления доступом (private, protected, public, package) хорошо определены и дают программисту большую гибкость для контроля доступа к классам, переменным, методам и т.д. Ваш вопрос просто иллюстрирует конкретный пример этих применение контроля доступа. Как вы закодировали свой пакет i1, вы сказали, что всем хорошо видеть интерфейс i1, но это не нормально для всех, чтобы увидеть класс I2. Это позволяет добавлять функциональные возможности класса I2, который не является общедоступным, но при этом позволяет получить доступ к бит I2, который реализует i1. Например, если я добавляю локальное целое пакет в класс I2

public class C1 {
    static class I2 implements I1 {
        int i;
        public int xxx() {
            return 1;
        }
    }
    public I2 x = new I2();
}

то у вас есть доступ к int внутри вашего пакета, но не за его пределами, а вне вашего пакета вы не можете обойти это, выполнив: -).

Ответ 3

E1 из примера 1 и I2 из примера 2 оба не видны вызывающим в этих примерах. Поэтому мне ясно, что компилятор говорит, что не знает, как с этим справиться. Ваше намерение неоднозначно. Или наоборот: как JVM обрабатывает такой вызов невидимым классам? Передача в интерфейс? Какой из них, как класс, может реализовать несколько.

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

Если вы хотите скрыть внутренности, вы предпочитаете использовать (общедоступные) интерфейсы вместо пакетов-частных реализаций.

Ответ 4

Я попробую объяснить это, используя ваш пример 2 и немного изменив его.

Предположим, что класс C1 был таким

public class C1 {
    static class I2 implements I1 {
        public int xxx() {
            return 1;
        }

        public int hello(){
            return 1;
        }
    }
    public I2 x = new I2();
}

Я добавил еще один общедоступный метод hello() в класс I2.

В

package i2;:

import i1.C1;
import i1.I1;
public class C2 {
    public static void main(String[] args) {
        C1 c1 = new C1();

        c1.x.hello(); // Will not compile.
        c1.x.toString(); //Will not compile.             

        Object mObject=(Object)c1.x;
        mObject.toString(); //Will compile.
    }
}

Теперь, чтобы ответить на ваш вопрос о том, почему ограничение. Ограничение позволяет программисту запретить доступ к общедоступным методам класса I2. Поскольку спецификатор доступа I2 является спецификатором доступа по умолчанию, ни один из его методов не должен быть доступен вне пакета, следовательно, это ограничение. Если бы ограничение не было, мы могли бы получить доступ к методу hello() из C2 (который находится в другом пакете), хотя I2 имеет спецификатор доступа по умолчанию.

Выполнение c1.x до Object работает, потому что теперь компилятор рассматривает его как объект класса Object. Следовательно, мы можем получить доступ к его методу toString(). Следовательно, mObject.toString() работает тогда c1.x.toString() не потому, что класс I2 недоступен из класса C2.

Ответ 5

В обоих примерах вы используете внутренние классы/интерфейсы. Внутренние классы не могут быть доступны ни одним объектом за пределами класса, в котором они определены.

Следовательно, вы должны отнести их к объектам (или любому другому супертипу вашего внутреннего класса). Вы должны сделать это вручную, потому что JDK не знает о каких-либо неявных отбросах.

Способ справиться с этим - превратить ваши внутренние классы в обычные классы. Внутренние классы очень полезны, но только для очень конкретных задач.