为啥我们不能在(非静态)内部类(Java 16 之前)中使用静态方法?

Posted

技术标签:

【中文标题】为啥我们不能在(非静态)内部类(Java 16 之前)中使用静态方法?【英文标题】:Why can't we have static method in a (non-static) inner class (pre-Java 16)?为什么我们不能在(非静态)内部类(Java 16 之前)中使用静态方法? 【发布时间】:2010-11-01 18:53:11 【问题描述】:

为什么我们不能在非静态内部类中拥有静态方法?

public class Foo 
    class Bar 
        static void method()  // Compiler error
    

如果我将内部类设为静态,它就可以工作。为什么?

public class Foo 
    static class Bar  // now static
        static void method() 
    

在 Java 16+ 中,这两个都是有效的。

【问题讨论】:

因为现在 Java 是旧的 COBOL :) 底线是:因为他们还没有实现它。 '非静态内部'是一个重言式。 如果你不想把你的内部类暴露给别人,又希望它包含静态方法,你可以把修饰符“private”和“static”都放在内部类上。 JDK16修复了这个问题,现在你可以在内部类上声明静态方法和字段了。 【参考方案1】:

因为内部类的实例隐式关联到其外部类的实例,所以它本身不能定义任何静态方法。由于静态嵌套类不能直接引用在其封闭类中定义的实例变量或方法,它只能通过对象引用来使用它们,因此在静态嵌套类中声明静态方法是安全的。

【讨论】:

我知道一个内部类与其外部类的一个实例相关联,我知道我们能够在内部类中声明静态成员有点没用,但我仍然在问为什么不是内部类可以声明静态成员吗​​? 在 C++ 中可以有,所以这是 Java 语言中的一个错误。 bug 这个词...我不认为这个词的意思是你认为的意思。 一个更合适的短语是“烦人的卡车司机”。不明白为什么 Java 不允许这样做。有时,我希望内部类使用父类的属性,但保留静态方法以获得更好的命名空间。这有什么本质上的问题吗? :( 没错。我想写一个实用程序内部类。它的一些 方法将受益于对外部类的访问,所以我不能将其设为静态,但它的一些 方法只是实用函数。为什么我不能打电话给A.B.sync(X) 甚至(从A 内部)B.sync(x)【参考方案2】:

在非静态内部类中允许使用静态方法没有多大意义;你将如何访问它?如果不通过外部类实例,您就无法访问(至少最初是)非静态内部类实例。没有纯粹的静态方法来创建非静态内部类。

对于外部类Outer,您可以像这样访问静态方法test()

Outer.test();

对于一个静态内部类Inner,你可以像这样访问它的静态方法innerTest()

Outer.Inner.innerTest();

但是,如果Inner 不是静态的,则现在没有纯静态的方式来引用方法innertest。非静态内部类与其外部类的特定实例相关联。函数与常量不同,因为对Outer.Inner.CONSTANT 的引用保证是明确的,而函数调用Outer.Inner.staticFunction(); 则不然。假设您有调用getState()Inner.staticFunction(),它在Outer 中定义。如果您尝试调用该静态函数,您现在对 Inner 类的引用不明确。也就是说,您在内部类的哪个实例上调用静态函数?这很重要。看,由于对外部对象的隐式引用,没有真正的静态方法来引用该静态方法。

Paul Bellora 是正确的,语言设计者本可以允许这样做。然后,他们将不得不小心地禁止对非静态内部类的静态方法中对外部类的隐式引用进行任何访问。在这一点上,如果你不能引用外部类,那么作为一个内部类的价值是什么,除了静态的?如果静态访问没问题,那么为什么不将整个内部类声明为静态呢?如果您只是将内部类本身设为静态,那么您就没有对外部类的隐式引用,并且您不再有这种歧义。

如果您确实需要非静态内部类的静态方法,那么您可能需要重新考虑您的设计。

【讨论】:

-1 我不同意你在这里采取的角度。当然我们可以引用一个内部类类型,例如Outer.Inner i = new Outer().new Inner(); 另外,根据JLS §15.28 允许内部类声明静态constants 是的,内部类可以声明静态常量。这与静态方法无关!虽然您可以以非静态方式引用静态方法,但不鼓励这样做。所有代码质量工具都抱怨这种参考,并且有充分的理由。你错过了我的观点。我从来没有说过没有办法引用静态内部类。我说没有静态方法来引用非静态外部类的内部类的静态方法。因此,没有正确的方法来引用它。 “在非静态内部类中允许静态方法没有多大意义;你将如何访问它?”你可以打电话给Outer.Inner.staticMethod(),就像你可以访问Outer.Inner.CONSTANT一样。 “如果不通过外部类实例,您就无法访问......非静态内部类实例。”为什么需要实例?您不需要Outer 的实例来调用Outer.staticMethod()。我知道这很挑剔,但我的观点是,以这种方式构建你的答案是没有意义的。恕我直言,如果语言设计者愿意,他们可以允许这样做。 Outer.Inner.CONSTANTOuter.Inner.staticMethod() 之间的区别在于,对常量的引用没有机会隐式引用 Outer 的实例,其中 Inner 被实例化。所有对Outer.staticMethod() 的引用共享相同的确切状态。所有对Outer.Inner.CONSTANT 的引用共享相同的确切状态。然而,对Outer.Inner.staticMethod() 的引用是模棱两可的:“静态”状态并不是真正的静态,因为在Inner 的每个实例中都隐含了对外部类的引用。没有一种真正明确、静态的方式来访问它。 @Eddie 不能在静态方法中引用实例字段,因此不存在与无法引用隐式实例字段Outer.this 相关的冲突。我同意 Java 语言设计者的观点,即没有理由在内部类中允许静态方法或非最终静态字段,因为内部类中的所有内容都应该在封闭类的上下文中。【参考方案3】:

我有一个理论,可能正确也可能不正确。

首先,您应该了解一些有关 Java 内部类如何实现的知识。假设你有这个类:

class Outer 
    private int foo = 0;
    class Inner implements Runnable 
        public void run() foo++; 
    
    public Runnable newFooIncrementer() return new Inner(); 

当你编译它时,生成的字节码看起来就像你写了这样的东西:

class Outer 
    private int foo = 0;
    static class Inner implements Runnable 
        private final Outer this$0;
        public Inner(Outer outer)
            this$0 = outer;
        
        public void run() this$0.foo++; 
    
    public Runnable newFooIncrementer() return new Inner(this); 

现在,如果我们确实允许在非静态内部类中使用静态方法,您可能想要做这样的事情。

class Outer 
    private int foo = 0;
    class Inner 
        public static void incrFoo() foo++; 
    

...这看起来相当合理,因为Inner 类似乎每个Outer 实例都有一个化身。但正如我们在上面看到的,非静态内部类实际上只是静态“内部”类的语法糖,所以最后一个示例大致相当于:

class Outer 
    private int foo = 0;
    static class Inner 
        private final Outer this$0;
        public Inner(Outer outer)
            this$0 = outer;
        
        public static void incrFoo() this$0.foo++; 
    

... 这显然行不通,因为this$0 是非静态的。这类解释了为什么不允许使用静态方法(尽管您可以提出可以允许静态方法的论点,只要它们不引用封闭对象),以及为什么不能有非最终静态字段(如果来自不同对象的非静态内部类的实例共享“静态状态”,那将是违反直觉的)。它还解释了为什么允许 final 字段(只要它们不引用封闭对象)。

【讨论】:

但这只是一个正常的“尝试从静态上下文访问非静态变量”类型错误 - 与***静态方法尝试访问它自己的类的实例变量没有什么不同。 我喜欢这个答案,因为它实际上解释了为什么它在技术上是不可能的,尽管它在语法上看起来是可能的。 @gustafc,我认为这是一个很好的解释。但正如劳伦斯指出的那样,这只是一个失败,因为对 foo 的引用不是静态的。但是,如果我想编写 public static double sinDeg(double theta) ... 一个内部数学实用程序类怎么办?【参考方案4】:

唯一的理由是“非必须”,何必支持呢?

从语法上讲,没有理由禁止内部类拥有静态成员。尽管Inner 的实例与Outer 的实例相关联,但如果java 决定这样做,仍然可以使用Outer.Inner.myStatic 来引用Inner 的静态成员。

如果您需要在Inner 的所有实例之间共享某些内容,您可以将它们作为静态成员放入Outer。这并不比您在Inner 中使用静态成员差,其中Outer 仍然可以访问Inner 的任何私有成员(不改进封装)。

如果您需要在由一个outer 对象创建的所有Inner 实例之间共享某些内容,则将它们作为普通成员放入Outer 类更有意义。

我不同意“静态嵌套类几乎只是***类”的观点。我认为最好将静态嵌套类/内部类真正视为外部类的一部分,因为它们可以访问外部类的私有成员。外部类的成员也是“内部类的成员”。所以内部类不需要支持静态成员。外部类中的普通/静态成员就足够了。

【讨论】:

内部类也不是“必须的”。然而,由于语言确实提供了内部类,它应该提供它们的完整且有意义的实现。【参考方案5】:

发件人:https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html

与实例方法和变量一样,内部类与其封闭类的实例相关联,并且可以直接访问该对象的方法和字段。此外,由于内部类与实例相关联,它本身不能定义任何静态成员。

甲骨文的解释是肤浅的和手摇的。由于在内部类中抢占静态成员没有技术或语法上的理由(在其他语言如 C# 中是允许的),Java 设计者的动机可能是概念品味和/或技术方便问题。

这是我的猜测:

与***类不同,内部类是实例相关的:内部类实例与其每个外部类的实例相关联,并且可以直接访问它们的成员。这是在 Java 中使用它们的主要动机。用另一种方式表达:内部类意味着在外部类实例的上下文中实例化。如果没有外部类实例,内部类不应该比外部类的其他实例成员更可用。我们将其称为内部类的实例依赖精神

静态成员(非面向对象)的本质与内部类(面向对象)的实例相关精神相冲突,因为您可以引用/调用静态成员使用限定的内部类名称来定义没有外部类实例的内部类。

特别是静态变量可能会以另一种方式冒犯:与外部类的不同实例相关联的内部类的两个实例将共享静态变量。由于变量是状态的组成部分,因此两个内部类实例实际上将独立于它们关联的外部类实例共享状态。并不是说静态变量以这种方式工作是不可接受的(我们在 Java 中接受它们作为对 OOP 纯度的明智妥协),但可以说,允许它们在其实例已经与外部类实例耦合的内部类中存在更深层次的冒犯按设计。禁止内部类中的静态成员以支持依赖实例的精神,可以获得先发制人这种更深层次的 OOP 攻击的额外好处。

另一方面,静态常量不会带来这样的冒犯,静态常量不会有意义地构成状态,因此这些都是允许的。为什么不禁止静态常量以最大限度地与实例依赖精神保持一致?可能是因为 constants 不需要占用比必要更多的内存(如果它们被强制为非静态,那么它们会被复制到每个内部类实例中,这可能是浪费的)。否则我无法想象异常的原因。

这可能不是坚如磐石的推理,但 IMO 最能理解甲骨文对此事的粗略评论。

【讨论】:

【参考方案6】:

简短的回答:大多数程序员对作用域如何工作的心智模型不是 javac 使用的模型。匹配更直观的模型需要对 javac 的工作方式进行重大更改。

需要内部类中的静态成员的主要原因是为了代码的简洁性 - 仅由内部类使用的静态成员应该存在于内部类中,而不是必须放置在外部类中。考虑:

class Outer 
   int outID;

   class Inner 
      static int nextID;
      int id = nextID++;

      String getID() 
         return outID + ":" + id;
      
   

考虑一下当我使用非限定标识符“outID”时 getID() 中发生了什么。此标识符出现的范围类似于:

Outer -> Inner -> getID()

这里,同样因为这正是 javac 的工作方式,作用域的“外部”级别包括外部的静态成员和实例成员。这很令人困惑,因为我们通常被告知将类的静态部分视为范围的另一个级别:

Outer static -> Outer instance -> instanceMethod()
         \----> staticMethod()

这样想,staticMethod()当然只能看到Outer的静态成员。但是,如果 javac 是这样工作的,那么在静态方法中引用实例变量将导致“名称无法解析”错误。真正发生的是名称是在作用域中找到的,但随后会进行额外级别的检查,并确定该名称是在实例上下文中声明的,并且是从静态上下文中引用的。

好的,这与内部类有什么关系?天真地,我们认为内部类没有理由不能拥有静态作用域,因为我们正在想象作用域是这样工作的:

Outer static -> Outer instance -> Inner instance -> getID()
         \------ Inner static ------^

换句话说,内部类中的静态声明和外部类中的实例声明都在内部类的实例上下文的范围内,但实际上它们都没有嵌套在另一个中;两者都嵌套在 Outer 的静态范围内。

这不是 javac 的工作方式——静态成员和实例成员都有一个单一级别的范围,并且范围总是严格嵌套。甚至继承也是通过将声明复制到子类而不是分支和搜索超类范围来实现的。

为了支持内部类的静态成员,javac 必须拆分静态和实例范围并且支持分支和重新加入范围层次结构,或者它必须将其简单的布尔“静态上下文”理念扩展到更改以跟踪当前范围内所有级别的嵌套类的上下文类型。

【讨论】:

我认为允许非静态内部类具有非常量静态成员的一个更根本的困难是,声明此类成员的程序员可能打算将它们绑定到外部类的实例,或者它们是真正静态的。如果一个构造——如果合法的话——可以被合理地指定为两种不同事物中的任何一种,并且这两种事物都可以以其他明确的方式表达,那么将构造指定为非法通常比将其指定为更好具有任何一种含义。【参考方案7】:

为什么我们不能在非静态内部类中拥有静态方法?

注意:非静态嵌套类称为内部类,因此您没有 non-static inner class 本身。

如果没有相应的外部类实例,内部类实例就不存在。内部类不能声明除了编译时常量之外的静态成员。如果允许,那么static 的含义就会有歧义。在那种情况下,会有一些混淆:

    这是否意味着VM中只有一个实例? 或者每个外部对象只有一个实例?

这就是为什么设计师可能决定根本不处理这个问题。

如果我将内部类设为静态,它就可以工作。为什么?

同样,您不能将内部类设为静态,而是可以将静态类声明为嵌套类。在这种情况下,这个嵌套类实际上是外部类的一部分,并且可以毫无问题地拥有静态成员。

【讨论】:

【参考方案8】:

这个话题已经引起了很多人的关注,我还是会尽量用最简单的术语来解释。

首先,参考http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.1,类或接口在第一次出现/调用任何以static 关键字开头的成员之前立即初始化。

    因此,如果我们容忍内部类中的静态成员,它将导致内部类的初始化,而不一定是外部/封闭类。因此,我们阻碍了类初始化序列。

    还要考虑非静态内部类与封闭/外部类的实例相关联的事实。因此,与实例关联意味着内部类将存在于外部类实例中,并且在实例之间会有所不同。

简化一点,为了访问静态成员,我们需要一个外部类的实例,我们将再次需要从中创建一个非静态内部类的实例。静态成员不应该绑定到实例,因此您会收到编译错误。

【讨论】:

【参考方案9】:

向JDK16 添加记录的工作还提到静态方法和字段现在可以与内部类一起使用,甚至允许main() 启动该类。

例如在JDK16中编译运行,可以选择main()作为java Outer或者java Outer$Inner运行:

public class Outer 
    public static void main(String[] args) 
        System.out.println("Outer class main xxx="+Inner.xxx+" nnn="+(++Inner.nnn)+" iii="+(--iii));
        aaa();
        Inner.zzz();
    
    public static void aaa() 
        System.out.println("aaa() nnn="+(++Inner.nnn)+" iii="+(--iii));
    
    public static int iii = 100;

    class Inner 
        public static final String xxx= "yyy";
        public static int nnn = 0;

        public static void zzz() 
            System.out.println("zzz() "+" nnn="+(++nnn)+" iii="+(--iii));
        
        public static void main(String[] args) 
            System.out.println("Inner class main xxx="+xxx+" nnn="+(++nnn)+" iii="+(--iii));
            zzz();
            aaa();
        
    

【讨论】:

更多信息可以在JDK-8254321找到。【参考方案10】:

内部类与静态嵌套类完全不同,尽管两者在语法上相似。静态嵌套类只是分组的一种方式,而内部类具有强关联 - 并且可以访问它们的外部类的所有值。您应该确定为什么要使用内部类,然后应该很自然地使用哪个。如果你需要声明一个静态方法,它可能是你想要的静态嵌套类。

【讨论】:

Benedikt,你说“静态嵌套类只是分组的一种手段”是什么意思?【参考方案11】:

首先为什么有人要在非静态内部类中定义静态成员?答案是,外部类成员只能使用内部类名称的静态成员,对吧?

但是对于这种情况,我们可以直接在外部类中定义成员。它将与外部类实例中的内部类的所有对象相关联。

如下代码,

public class Outer 

  class Inner 

    public static void method() 

    

  


可以这样写

public class Outer 

  void method() 

   

   class Inner 


  


因此,在我看来,为了不使代码复杂化,Java 设计器不允许此功能,否则我们可能会在未来的版本中看到此功能以及更多特性。

【讨论】:

【参考方案12】:

假设有两个外部类的实例并且它们都实例化了内部类。现在如果内部类有一个静态成员,那么它将只在堆区域中保留该成员的一个副本。在这种情况下,外部类的两个对象都会引用这个单一的副本&他们可以一起改变它。这可能导致“脏读”的情况,因此为了防止这个 Java 应用了这个限制。支持这个论点的另一个强点是 java 在这里允许最终的静态成员,那些值可以'不能从任何一个外部类对象中改变。 如果我错了,请让我。

【讨论】:

【参考方案13】:

试着把class当作一个普通的field,你就会明白。

//something must be static. Suppose something is an inner class, then it has static keyword which means it's a static class
Outer.something 

【讨论】:

【参考方案14】:

您可以在静态嵌套类上使用静态方法。例如

public class Outer 

  public static class Inner 

    public static void method() 

    
  

【讨论】:

【参考方案15】:

将内部类成员设为静态是没有用的,因为您一开始就无法访问它们。

想一想,要访问一个静态成员,你使用 className.memberName ,在我们的例子中,它应该是类似于 externalclassName.innerclassName.memberName 的东西,现在你明白为什么内部类必须是静态的了......

【讨论】:

以上是关于为啥我们不能在(非静态)内部类(Java 16 之前)中使用静态方法?的主要内容,如果未能解决你的问题,请参考以下文章

深入浅析Java中Static Class及静态内部类和非静态内部类的不同

Java内部类:静态内部类&接口内部类

java为什么非静态内部类中不能有static修饰的属性,但却可以有常量?

内部类之静态内部类

Java面试题|静态内部类和非静态内部类有什么区别?

5.7 内部类