Зачем делать закрытый внутренний член класса общедоступным в Java?

В чем причина объявления члена частного внутреннего класса общедоступным в Java, если к нему по-прежнему нельзя получить доступ за пределами содержащего класса? Или может?

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

        public boolean hasNext() { // Why public?
            // ...
        }
    }
}

person vitaut    schedule 07.06.2011    source источник


Ответы (7)


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

Однако, если он расширяет или реализует любой другой не частный класс или интерфейс, это имеет смысл. Пример:

interface EvenIterator {
    public boolean hasNext();
}


public class DataStructure {
    // ...

    private class InnerEvenIterator implements EvenIterator{
        // ...

        public boolean hasNext() { // Why public?
            // ...
        }
    }

    InnerEvenIterator iterator;

    public EvenIterator getIterator(){
         return iterator;
    }     

}
person Gursel Koca    schedule 07.06.2011
comment
Таким образом, как правило, ни один модификатор доступа члена класса не может иметь приоритет над модификатором доступа класса члена. Это правильно? - person jdurston; 28.11.2014

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

Представьте, что во время рефакторинга вам нужно сделать этот внутренний класс верхним уровнем. Если этот метод private, как бы вы решили, следует ли сделать его public или следует использовать какой-то более ограничительный модификатор? Объявление метода как public сообщает читателю о намерениях исходного автора - этот метод не следует рассматривать как деталь реализации.

person axtavt    schedule 07.06.2011
comment
Именно так я использую public и private во вложенных классах! Зачем нам писать вложенные классы по-другому, используя приватные методы, которые можно вызывать извне? Обычно я считаю, что внешний класс не должен вызывать частные методы внутреннего класса, а только общедоступные методы, даже если компилятор не обеспечивает этого. В качестве бонуса вы можете позже преобразовать вложенный класс в класс верхнего уровня без необходимости менять частные методы на общедоступные. - person Henno Vermeulen; 27.11.2014

Это полезно при реализации любого interface.

class DataStructure implements Iterable<DataStructure> {

    @Override
    public Iterator<DataStructure> iterator() {
        return new InnerEvenIterator();
    }
    // ...        

    private class InnerEvenIterator implements Iterator<DataStructure> {
        // ...    
        public boolean hasNext() { // Why public?
            // ...
            return false;
        }

        @Override
        public DataStructure next() {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    }

    public static void main(String[] ex) {
        DataStructure ds = new DataStructure();
        Iterator<DataStructure> ids = ds.iterator();
        ids.hasNext(); // accessable            
    }
}
person Prince John Wesley    schedule 07.06.2011
comment
Что это может быть за шаблон проектирования? - person Java; 18.04.2020

Я думаю, что вам не хватает реализации части интерфейса Iterator в вашем примере кода. В этом случае вы не можете сделать так, чтобы метод hasNext() имел какой-либо другой идентификатор видимости, кроме общедоступного, поскольку это в конечном итоге уменьшит его видимость (методы интерфейса имеют общедоступную видимость), и он не будет компилироваться.

person Sanjay T. Sharma    schedule 07.06.2011

Существует множество бесполезных комбинаций модификаторов доступа. Открытый метод в частном внутреннем классе полезен только в том случае, если он реализует открытый метод в открытом классе/интерфейсе.

public class DataStructure {
    // ...

    private class InnerEvenIterator implements Iterator {
        // ...

        public boolean hasNext() { // Why public?
            // ...
        }
    }

    public Iterator iterator() {
        return new InnerEvenIterator();
    }
}

Кстати: абстрактные классы часто имеют public конструкторов, когда на самом деле они protected

person Peter Lawrey    schedule 07.06.2011
comment
Дело не в том, чтобы быть полезным; у вас просто нет выбора, когда дело доходит до реализации интерфейса. :-) - person Sanjay T. Sharma; 07.06.2011
comment
Это правда, но моя точка зрения такова; если вы не реализуете или не переопределяете общедоступный метод, этот метод не обязательно должен быть общедоступным и может иметь частную область действия для частного класса. Делать его public бесполезным, если к нему нельзя получить доступ publicly, и это может ввести в заблуждение или сбить с толку. - person Peter Lawrey; 07.06.2011

Если внутренний класс является закрытым, к нему нельзя получить доступ по имени вне внешнего класса. Внутренние и внешние классы имеют доступ к частным методам и частным переменным экземпляра друг друга. Пока вы находитесь во внутреннем или внешнем классе, модификаторы public и private имеют одинаковый эффект. В вашем примере кода:

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

        public boolean hasNext() { // Why public?
            // ...
        }
    }
}

Что касается класса DataStructure, это полностью эквивалентно:

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

        private boolean hasNext() {
            // ...
        }
    }
}

Это связано с тем, что только DataStructure может получить к нему доступ, поэтому не имеет значения, установите ли вы его как общедоступный или частный. В любом случае, DataStructure остается единственным классом, который может получить к нему доступ. Используйте любой модификатор, который вам нравится, это не имеет функционального значения. Единственный случай, когда вы не можете выбирать случайным образом, — это когда вы внедряете или расширяете, и в этом случае вы не можете уменьшить доступ, но можете увеличить его. Поэтому, если абстрактный метод имеет защищенный доступ, вы можете сделать его общедоступным. Конечно, ни один из них на самом деле не имеет никакого значения.

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

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

person sage88    schedule 11.01.2013
comment
В этом случае между private и public есть небольшая разница: не приватные методы могут быть переопределены другим внутренним классом, а приватные - нет. - person Dávid Horváth; 06.06.2017

Здесь нужно учитывать несколько аспектов. Далее будет использоваться термин вложенный класс, поскольку он охватывает как не-1_ (также называемый внутренним классом), так и static классы (источник).

Не относится к private вложенным классам, но в JLS §8.2 есть интересный пример, который показывает, где могут быть полезны public члены в классах package-private или protected.

Исходный код

Методы переопределения

Когда ваш вложенный класс реализует интерфейс или расширяет класс и переопределяет один из его методов, то в соответствии с JLS §8.4.8.3:

Модификатор доступа переопределяющего или скрывающего метода должен предоставлять как минимум такой же доступ, как и переопределяемый или скрытый метод.

Например:

public class Outer {
  private static class Nested implements Iterator<String> {
    @Override
    public boolean hasNext() {
      ...
    }
    
    @Override
    public String next() {
      ...
    }
  }
}

Методы hasNext() и next(), переопределяющие методы Iterator, должны быть public, поскольку методы Iterator общедоступны.

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

Передача намерения

Ограничение доступа определяется в JLS. §6.6.1:

Член [...] ссылочного типа [...] доступен только в том случае, если тип доступен, а член или конструктор объявлен для разрешения доступа

[...]

В противном случае член или конструктор объявляется private, и доступ разрешается тогда и только тогда, когда он происходит в теле типа верхнего уровня (§7.6), который содержит объявление члена или конструктора.

Поэтому к членам вложенного класса private можно (с точки зрения исходного кода; см. также раздел «Отражение») получить доступ только из тела вложенного типа верхнего уровня. Интересно, что тело также охватывает другие вложенные классы:

public class TopLevel {
  private static class Nested1 {
    private int i;
  }

  void doSomething(Nested1 n) {
    // Can access private member of nested class
    n.i++;
  }

  private static class Nested2 {
    void doSomething(Nested1 n) {
      // Can access private member of other nested class
      n.i++;
    }
  }
}

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

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

public class Cache {
  private static class CacheEntry<T> {
    private final T value;
    private long lastAccessed;

    // Signify that enclosing class may use this constructor
    public CacheEntry(T value) {
      this.value = value;
      updateLastAccessed();
    }

    // Signify that enclosing class must NOT use this method
    private void updateLastAccessed() {
      lastAccessed = System.nanoTime();
    }

    // Signify that enclosing class may use this method
    public T getValue() {
      updateLastAccessed();
      return value;
    }
  }

  ...
}

Скомпилированные файлы классов

Также интересно отметить, как компилятор Java обрабатывает доступ к членам вложенных классов. До JEP 181: Nest-Based Access Control (добавлен в Java 11) компилятор должен был создавать синтетические методы доступа, потому что файл класса не мог выразить логику управления доступом, связанную с вложенными классами. Рассмотрим этот пример:

class TopLevel {
  private static class Nested {
    private int i;
  }
    
  void doSomething(Nested n) {
    n.i++;
  }
}

При компиляции с Java 8 и проверке с помощью javap -p ./TopLevel$Nested.class вы увидите, что был добавлен синтетический метод access$008:

class TopLevel$Nested {
  private int i;
  private TopLevel$Nested();
  static int access$008(TopLevel$Nested);
}

Это немного увеличило размер файлов классов и могло снизить производительность. Это одна из причин, по которой для членов вложенных классов часто выбирается закрытый для пакета (т.е. без модификатора доступа) доступ, чтобы предотвратить создание синтетических методов доступа.
С JEP 181 в этом больше нет необходимости (javap -v вывод при компиляции с СДК 11):

class TopLevel$Nested
...
{
  private int i;
  ...

  private TopLevel$Nested();
  ...
}
...
NestHost: class TopLevel
...

Отражение

Еще один интересный аспект — отражение. К сожалению, JLS не является конкретным в этом отношении, но §15.12.4.3 содержит интересную подсказку:

Если T находится в другом пакете, чем D, и их пакеты находятся в одном модуле, а T равен public или protected, то T доступен.

[...]

Если T равно protected, это обязательно вложенный тип, поэтому во время компиляции на его доступность влияет доступность типов, включающих его объявление. Однако во время компоновки на его доступность не влияет доступность типов, включающих его объявление. Более того, во время связывания protected T так же доступен, как и public T.

Аналогично AccessibleObject.setAccessible(...) вообще не упоминает объемлющий тип. И действительно, можно получить доступ к членам вложенного типа public или protected внутри включающего типа, отличного от public: test1/TopLevel1.java

package test1;

// package-private
class TopLevel1 {
  private static class Nested1_1 {
    protected static class Nested1_2 {
      public static int i;
    }
  }
}

test2/TopLevel2.java

package test2;

import java.lang.reflect.Field;

public class TopLevel2 {
  public static void main(String... args) throws Exception {
    Class<?> nested1_2 = Class.forName("test1.TopLevel1$Nested1_1$Nested1_2");
    Field f = nested1_2.getDeclaredField("i");
    f.set(null, 1);
  }
}

Здесь отражение может изменить поле test1.TopLevel1.Nested1_1.Nested1_2.i без необходимости делать его доступным, несмотря на то, что оно находится внутри private вложенного класса внутри закрытого от пакета класса.

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

person Marcono1234    schedule 25.10.2020