【问题标题】:Why make private inner class member public in Java?为什么在 Java 中将私有内部类成员设为公有?
【发布时间】:2011-09-10 00:22:39
【问题描述】:

如果仍然无法在包含类之外访问,那么在 Java 中将私有内部类的成员声明为 public 的原因是什么?或者可以吗?

public class DataStructure {
    // ...

    private class InnerEvenIterator {
        // ...

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

【问题讨论】:

    标签: java private-members access-specifier public-members


    【解决方案1】:

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

    【讨论】:

    • 所以 - 作为一般规则 - 没有类成员访问修饰符可以优先于成员类的访问修饰符。对吗?
    【解决方案2】:

    可以将此方法设为public,以表明它在语义上是公开的,尽管编译器在这种特殊情况下不强制执行可见性规则。

    想象一下,在一些重构过程中,您需要使这个内部类成为顶级。如果这个方法是private,你将如何决定是应该使用public,还是应该使用更严格的修饰符?将方法声明为 public 告诉读者原作者的意图 - 此方法不应被视为实现细节。

    【讨论】:

    • 这正是我在嵌套类中使用公共和私有的方式!为什么我们要使用可以从外部调用的私有方法以不同的方式编写嵌套类?我通常认为外部类不应该调用内部类中的私有方法,而应该只调用公共方法,即使编译器不强制这样做。作为奖励,您可以稍后将嵌套类重构为顶级类,而无需将私有方法更改为公共。
    【解决方案3】:

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

    【讨论】:

    • 这可以是一种什么样的设计模式?
    【解决方案4】:

    我认为您在示例代码中缺少实现 Iterator 接口部分。在这种情况下,您不能使 hasNext() 方法具有除 public 之外的任何其他可见性标识符,因为这最终会降低其可见性(接口方法具有公共可见性)并且它不会编译.

    【讨论】:

      【解决方案5】:

      有许多无用的访问修饰符组合。私有内部类中的公共方法只有在实现公共类/接口中的公共方法时才有用。

      public class DataStructure {
          // ...
      
          private class InnerEvenIterator implements Iterator {
              // ...
      
              public boolean hasNext() { // Why public?
                  // ...
              }
          }
      
          public Iterator iterator() {
              return new InnerEvenIterator();
          }
      }
      

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

      【讨论】:

      • 这不是“有用”;在实现接口时,您别无选择。 :-)
      • 这是真的,但我的意思是;如果您没有实现或覆盖公共方法,则该方法不需要是公共的,并且可以具有私有类的私有范围。如果publicly 无法访问,则将其设为public 是没有用的,并且可能会产生误导或混淆。
      【解决方案6】:

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

      public class DataStructure {
          // ...
      
          private class InnerEvenIterator {
              // ...
      
              public boolean hasNext() { // Why public?
                  // ...
              }
          }
      }
      

      就DataStructure类而言,这完全等价于:

      public class DataStructure {
          // ...
      
          private class InnerEvenIterator {
              // ...
      
              private boolean hasNext() {
                  // ...
              }
          }
      }
      

      这是因为只有 DataStructure 可以访问它,所以设置为 public 或 private 都没有关系。无论哪种方式,DataStructure 仍然是唯一可以访问它的类。使用您喜欢的任何修饰符,它没有功能上的区别。唯一不能随意选择的时间是在实现或扩展时,这种情况下不能减少访问,但可以增加访问。因此,如果抽象方法具有受保护的访问权限,您可以将其更改为公共的。诚然,两者实际上都没有任何区别。

      如果您打算在其他类中使用内部类并因此将其公开,您可能一开始就不应该将其设为内部类。

      此外,我看不到内部类扩展或实现其他类的任何要求。他们这样做可能很常见,但肯定不是必需的。

      【讨论】:

      • 在这种情况下privatepublic 之间有一点区别:非私有方法可以被其他内部类覆盖,而私有方法不能。
      【解决方案7】:

      这里需要考虑多个方面。下面将使用术语“嵌套类”,因为它涵盖了非static(也称为“内部类”)和static 类(source)。

      private 嵌套类无关,但 JLS §8.2 有一个有趣的 example,它显示了 package-private 或 protected 类中的 public 成员可能有用的地方。

      源代码

      覆盖方法

      当您的嵌套类实现一个接口或扩展一个类并覆盖其方法之一时,则按照JLS §8.4.8.3

      覆盖或隐藏方法的访问修饰符必须至少提供与覆盖或隐藏方法一样多的访问权限

      例如:

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

      覆盖Iterator 方法的hasNext()next() 方法必须是public,因为Iterator 方法是公开的。

      附带说明:JLS §13.4.7 描述了一个类可以提高其方法之一的访问级别,即使子类覆盖它,也不会导致链接错误。

      传达意图

      访问限制定义在JLS §6.6.1:

      引用类型 [...] 的成员 [...] 仅在类型可访问且声明成员或构造函数允许访问时才可访问

      [...]

      否则,成员或构造函数被声明为private,并且当且仅当它出现在包含成员或构造函数声明的顶级类型 (§7.6) 的主体内时才允许访问。

      因此,private 嵌套类的成员只能从封闭的顶级类型的主体中访问(从源代码的角度来看;另请参见“反射”部分)。有趣的是,“body”还涵盖了其他嵌套类:

      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++;
          }
        }
      }
      

      因此,从编译器提供的访问限制的角度来看,在 private 嵌套类中使用 public 成员确实没有意义。

      但是,使用不同的访问级别对于传达意图可能很有用,尤其是(正如其他人指出的那样)嵌套类将来可能被重构为单独的顶级类时。考虑这个例子:

      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(...) 根本没有提到封闭类型。确实可以在非public 封闭类型中访问publicprotected 嵌套类型的成员: 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 或 package-private。

      【讨论】:

        猜你喜欢
        • 2014-06-03
        • 2014-09-22
        • 2016-07-31
        • 2017-12-22
        • 2011-02-13
        • 2010-10-22
        • 2015-05-03
        • 2013-06-08
        • 2017-01-12
        相关资源
        最近更新 更多