为什么在Java中将私有内部类成员变量变为公共的?

37

如果在Java中声明一个私有内部类的成员为公共的,而它仍然无法在包含类之外被访问,那么这样做的原因是什么?或者可以吗?

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

        public boolean hasNext() { // Why public?
            // ...
        }
    }
}
7个回答

37
如果`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;
    }     

}

那么,一般规则是,没有类成员访问修饰符可以优先于成员类的访问修饰符。这正确吗? - jdurston

19

为了表明这个方法在语义上是公共的,尽管编译器在这种情况下不强制执行可见性规则,因此可以将其设置为public

想象一下,在某些重构期间,您需要将此内部类转换为顶级类。如果此方法为private,那么您如何决定是否应将其设置为public或使用更严格的修饰符?将方法声明为public可以告诉读者原始作者的意图 - 这个方法不应被视为实现细节。


8
这正是我在嵌套类中使用公有和私有关键字的方式!我们为什么要使用可以从外部调用的私有方法来编写不同的嵌套类呢?虽然编译器没有强制执行,但我通常认为外部类不应该调用内部类的私有方法,只应该调用公有方法。如果以后需要将嵌套类重构为顶层类,这样做还可以避免将私有方法更改为公有方法。 - Henno Vermeulen

11

当你实现任何 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            
    }
}

这可能是哪种设计模式? - Java bee

4

我认为你在示例代码中漏掉了实现Iterator接口的部分。在这种情况下,你不能使hasNext()方法具有除public以外的任何其他可见性标识符,因为这将降低其可见性(接口方法具有public可见性),并且它将无法编译。


3

许多访问修饰符的组合是没有用处的。在私有内部类中定义公共方法只有在它实现了公共类/接口中的公共方法时才有用。

public class DataStructure {
    // ...

    private class InnerEvenIterator implements Iterator {
        // ...

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

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

顺便说一下:抽象类通常具有public构造函数,而实际上它们是protected


2
这不是关于“有用”的问题;当涉及到实现接口时,你没有选择。 :-) - Sanjay T. Sharma
1
这是正确的,但我的观点是:如果您没有实现或覆盖公共方法,则该方法不需要是公共的,并且可以具有私有类的私有范围。如果无法公开访问,则将其设置为“public”是没有用的,可能会产生误导或混淆。 - Peter Lawrey

2

如果内部类是私有的,则无法在外部类之外通过名称访问它。 内部和外部类可以访问彼此的私有方法和私有实例变量。 只要在内部或外部类中,public和private修饰符具有相同的效果。 在您的代码示例中:

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

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

就数据结构类而言,这与以下内容完全等效:

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

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

这是因为只有DataStructure可以访问它,所以将其设置为public或private并不重要。无论哪种修饰符,DataStructure仍然是唯一能访问它的类。使用任何一个修饰符都没有功能上的区别。唯一不能随意选择的时候是在实现或扩展时,此时不能降低访问级别,但可以增加访问级别。因此,如果抽象方法具有protected访问权限,则可以将其更改为public。尽管两者实际上没有任何区别。
如果您计划在其他类中使用内部类,从而使其公共可见,那么您可能不应该首先将其设置为内部类。
此外,我没有看到内部类需要扩展或实现其他类的要求。它们可能经常这样做,但肯定不是必需的。

在这种情况下,“private”和“public”之间有一点不同:非私有方法可以被其他内部类覆盖,而私有方法则不能。 - Dávid Horváth

1
这里需要考虑多个方面。以下将使用术语“嵌套类”,因为它涵盖了非静态(也称为“内部类”)和静态类(来源)。
与私有嵌套类无关,但JLS §8.2有一个有趣的示例,展示了在包私有或受保护类中使用公共成员可能很有用。

源代码

重写方法

当您的嵌套类实现接口或扩展类并覆盖其中一个方法时,按照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 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的推出,这不再是必需的(当使用JDK 11编译时,javap -v输出结果)。
class TopLevel$Nested
...
{
  private int i;
  ...

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

反射

另一个有趣的方面是反射。JLS在这方面不太详细,但§15.12.4.3中包含了一个有趣的提示:

如果T与D不在同一包中,但它们的包在同一模块中,并且T是publicprotected,那么T是可访问的。

[...]

如果T是protected,那么它必须是一个嵌套类型,因此在编译时,其可访问性受到包含其声明的类型的可访问性的影响。然而,在链接期间,它的可访问性不受包含其声明的类型的可访问性的影响。此外,在链接期间,protected T与public T一样可访问。

同样地,AccessibleObject.setAccessible(...)并没有提及封闭类型。实际上,在非公共的封闭类型中可以访问公共或受保护的嵌套类型的成员: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或包私有。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接