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

Доступ к частному экземпляру из анонимного статического экземпляра

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

enum E {
    A { public int get() { return i; } },
    B { public int get() { return this.i; } },
    C { public int get() { return super.i; } },
    D { public int get() { return D.i; } };

    private int i = 0;
    E() { this.i = 1; }
    public abstract int get();
}

Я получаю ошибки времени компиляции в первых двух объявлениях констант континуума (A и B), но последние 2 компилируют штраф (C и D). Ошибки:

Ошибка 1 в строке A: нестатическая переменная я не может ссылаться на статический контекст
Ошибка 2 в строке B: у меня есть частный доступ в E

Так как get является методом экземпляра, я не понимаю, почему я не могу получить доступ к переменной экземпляра i так, как я хочу.

Примечание: удаление ключевого слова private из объявления i также делает компилируемый код, который я тоже не понимаю.

Использование Oracle JDK 7u9.

ИЗМЕНИТЬ

Как указано в комментариях, это не относится к перечислениям, а приведенный ниже код производит такое же поведение:

class E {
    static E a = new E() { public int get() { return i; } };
    static E b = new E() { public int get() { return this.i; } };
    static E c = new E() { public int get() { return super.i; } };
    static E d = new E() { public int get() { return d.i; } };

    private int i = 0;
}
4b9b3361

Ответ 1

Наблюдаемое поведение задается спецификацией языка Java, в частности неявным доступом к полям охватывающих типов, и правилом, что частные члены не наследуются.

доступ к неквалифицированному полю

A { public int get() { return i; } }

Спецификация mandates:

Необязательное тело класса константы enum неявно определяет анонимное объявление класса (§15.9.5), которое расширяет сразу включающий тип перечисления. Тело класса определяется обычными правилами анонимных классов; в частности, он не может содержать никаких конструкторов.

Это делает выражение i несколько неоднозначным: ссылаемся ли мы на поле охватывающего экземпляра или внутреннего экземпляра? Увы, внутренний экземпляр не наследует поле:

Члены класса, объявленные как private, не наследуются подклассами этого класса.

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

доступ к полю через this

B { public int get() { return this.i; } },

Спецификация мандатов:

При использовании в качестве основного выражения ключевое слово this обозначает значение, которое является ссылкой на объект, для которого был вызван метод экземпляра (§15.12), или для объекта, который создается.

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

Причина, по которой компилятор отклоняет выражение доступа к полю this.i ::

Члены класса, объявленные как private, не наследуются подклассами этого класса.

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

B { public int get() { return ((E)this).i; } },

просто отлично.

доступ через супер

Подобно этому, super ссылается на объект, на который был вызван метод (или объект, который был создан). Поэтому ясно, что мы имеем в виду внутренний экземпляр.

Кроме того, super имеет тип E, и поэтому объявление видимо.

доступ через другое поле

D { public int get() { return D.i; } };

Здесь D - это неквалифицированный доступ к статическому полю D, объявленному в E. Поскольку это статическое поле, вопрос о том, какой экземпляр использовать, является спорным, а доступ действителен.

Он, однако, довольно хрупкий, поскольку поле назначается только после полного создания объекта перечисления. Если кто-то вызовет get() во время построения, будет выброшен NullPointerException.

Рекомендация

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

При создании поля protected будет ослаблен контроль доступа (т.е. разрешить другим классам в пакете доступ к полю), это позволит избежать этих проблем.

Ответ 2

Взгляните на этот фрагмент кода:

public class E 
{
  final int i;
  private final int j;
  final E a;

  E() { i = j = 0; a = null; }

  E(int p_i) {
    this.i = this.j = p_i;
    a = new E() {
      int getI() { return i; }
      int getJ() { return j; }
    };
  }

  int getI() { throw new UnsupportedOperationException(); }
  int getJ() { throw new UnsupportedOperationException(); }

  public static void main(String[] args) {
    final E ea = new E(1).a;
    System.out.println(ea.getI());
    System.out.println(ea.getJ());
  }
}

это печатает

0
1

и единственное различие между i и j - это уровень доступа!

Это удивительно, но это правильное поведение.

Ответ 3

Обновление

Кажется, это потому, что он определен в статическом блоке. Посмотрите на следующее:

    private E works = new E("A", 0) {

        public int get() {
            return i; // Compiles
        }
    };

    static {
        A = new E("A", 0) {

            public int get() {
                return i; // Doesn't Compile
            }

        };
    }

Оригинал

Я скомпилировал перечисление, а затем декомпилировал его с помощью Jad, чтобы увидеть, как выглядит код:

static abstract class E extends Enum
{

    public static E[] values()
    {
        return (E[])$VALUES.clone();
    }

    public static E valueOf(String s)
    {
        return (E)Enum.valueOf(Foo$E, s);
    }

    public abstract int get();

    public static final E A;
    private int i;
    private static final E $VALUES[];

    static
    {
        A = new E("A", 0) {

            public int get()
            {
                return A.i;
            }

        }
;
        $VALUES = (new E[] {
            A
        });
    }


    private E(String s, int j)
    {
        super(s, j);
        i = 0;
        i = 1;
    }

}

Это упрощает мне, что A является анонимным внутренним классом, определенным в статическом блоке init типа E. Оглядываясь на видимость частного члена в анонимных внутренних классах, я нашел следующее в этом ответе (Почему только конечные переменные доступны в анонимном классе?):

Когда вы создаете экземпляр анонимного внутреннего класса, любые переменные, которые используются в этом классе, имеют свои значения, скопированные с помощью автогенерированного конструктора. Это позволяет компилятору не автогенерировать различные дополнительные типы для хранения логического состояния "локальных переменных", как, например, компилятор С# делает

Из этого я полагаю, что A.i ссылается на эту скопированную переменную в A, а не на i, объявленную в E. Единственный способ получить i в E - это либо статический, либо не закрытый.


Ответ 4

private методы могут быть доступны во вложенных классах, если они находятся в одном файле класса.

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

A { public int get() { return super.i; } };

скомпилировать, но

A { public int get() { return i; } };

дает

error: non-static variable i cannot be referenced from a static context

который явно неверен, данный super.i не имеет смысла, если бы это был статический контекст.

Как отмечает Марко

A { public int get() { return this.i; } };

выводит сообщение об ошибке

error: i has private access in E

который может быть более точным. То есть вы можете получить доступ к этому полю явно, но неявно.