为什么在非静态内部类(Java 16 之前)中无法使用静态方法?

153

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

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

如果我把内部类设为静态,它就能正常工作。为什么?

public class Foo {
    static class Bar { // now static
        static void method() {}
    }
}
在Java 16+中,这两种写法都是有效的。

4
因为现在的 Java 就像旧的 COBOL 一样老了 :) - ses
1
底线是:因为他们还没有实现它。 - intrepidis
1
“非静态内部”是一个重言式。 - user207421
3
JDK16修复了这个问题,现在你可以在内部类中声明静态方法和字段。 - DuncG
请参考此链接:https://dev59.com/e3I-5IYBdhLWcg3wPFx2#65147661, JDK 16已经修复了这个问题。 - maplemaple
显示剩余2条评论
15个回答

114

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


9
我知道内部类与其外部类的实例相关联,也知道在内部类中声明静态成员有点没用,但我仍然想知道为什么内部类不能声明静态成员? - kzidane
16
在C++中,你可以这样做,所以这是Java语言中的一个错误。 - Industrial-antidepressant
44
“bug”这个词......我认为它的含义和你想的不一样。 - Seth Nelson
27
更合适的说法应该是“烦人如卡车司机”。不明白为什么Java不允许这样。有时候,我想让一个内部类使用父类的属性,但保留静态方法以更好地命名空间。这样做有什么本质上的问题吗?:( - Angad
6
没错。我想写一个内部工具类。它的一些方法会从访问外部类中受益,所以我不能将其设为静态,但其中一些方法只是实用函数。为什么我不能调用 A.B.sync(X) 或者(在 A 中)B.sync(x) - Edward Falk
显示剩余8条评论

49

在非静态内部类中允许静态方法没有太大的意义,因为你如何访问它呢?除非通过外部类实例访问非静态内部类实例,否则无法访问(至少最初)。没有纯粹的静态方法可以创建非静态内部类。

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

Outer.test();

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

Outer.Inner.innerTest();
然而,如果内部类Inner不是静态的,现在就没有纯静态的方法可以引用方法innertest。非静态内部类与其外部类的特定实例绑定在一起。函数与常量不同,在引用Outer.Inner.CONSTANT时保证是无歧义的,但调用Outer.Inner.staticFunction();则不然。假设您有一个调用getState()Inner.staticFunction(),后者在Outer中定义。如果尝试调用该静态函数,则现在对内部类存在一个模糊的引用。也就是说,在哪个内部类实例上调用静态函数很重要。这很重要。因为由于对外部对象的隐式引用,没有真正静态的方法可以引用那个静态方法。

Paul Bellora正确地指出,语言设计人员本可以允许这样做。然后他们必须小心地禁止访问非静态内部类的静态方法中的隐式外部类引用。此时,如果您不能除了静态方式以外引用外部类,那么内部类作为内部类的价值何在呢? 如果静态访问是可以接受的,那么为什么不将整个内部类声明为静态呢? 如果简单地将内部类本身设置为静态,则不再具有对外部类的隐式引用,也就不存在这种歧义。

如果您实际上需要在非静态内部类中使用静态方法,那么您可能需要重新考虑您的设计。


7
我不同意你在这里的观点。当然,我们可以引用内部类类型,例如Outer.Inner i = new Outer().new Inner(); 此外,根据 JLS §15.28 的规定,内部类允许声明静态常量 - Paul Bellora
2
是的,内部类可以声明静态常量。这与静态方法无关!虽然你可以非静态地引用静态方法,但这是不鼓励的。所有代码质量工具都会抱怨这种引用的方式,有充分的理由。而且你忽略了我的观点。我从未说过没有办法引用静态内部类。我说的是没有一种静态的方式来引用非静态外部类的静态内部类的静态方法。因此,没有适当的方式来引用它。 - Eddie
28
在非静态内部类中允许静态方法并没有什么意义;因为你如何访问它呢?你可以像访问Outer.Inner.CONSTANT一样调用Outer.Inner.staticMethod()。如果没有通过外部类实例,你无法访问非静态内部类的实例。但是为什么你需要这个实例呢?你不需要Outer类的实例来调用Outer.staticMethod()。我知道这有些苛刻,但我的观点是以这种方式来表述你的答案是没有意义的。我认为如果语言设计者希望的话是可以允许这一点的。 - Paul Bellora
1
Outer.Inner.CONSTANTOuter.Inner.staticMethod()之间的区别在于对常量的引用没有机会隐式地引用实例化InnerOuter实例。所有对Outer.staticMethod()的引用共享完全相同的状态。所有对Outer.Inner.CONSTANT的引用共享完全相同的状态。然而,对Outer.Inner.staticMethod()的引用是不明确的:由于每个Inner实例中都有对外部类的隐式引用,因此“静态”状态并不真正是静态的。没有真正明确、静态的方法来访问它。 - Eddie
3
在静态方法中无法引用实例字段,因此与无法引用隐式实例字段Outer.this相关的冲突不存在。我同意Java语言设计者的观点,不应该在内部类中允许静态方法或非最终静态字段,因为内部类中的所有内容都应该在封闭类的上下文中。 - Theodore Murdock
显示剩余3条评论

21

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

首先,你需要了解一些关于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字段(只要它们不引用封闭对象)。


7
但这只是一个普通的“试图从静态上下文访问非静态变量”的错误 - 与顶层静态方法尝试访问其所在类的实例变量没有什么不同。 - Lawrence Dol
2
我喜欢这个答案,因为它实际上解释了为什么从技术上讲不可能,尽管在语法上似乎是可能的。 - LoPoBo
@gustafc,我认为那是一个很好的解释。但正如Lawrence所指出的那样,它只是因为对foo的引用不是静态的才失败了。但是如果我想要在内部数学工具类中编写public static double sinDeg(double theta) { ... }呢? - Edward Falk

7
唯一的原因就是"没有必要",所以为什么要费事去支持呢?
从语法上来说,并没有理由禁止内部类拥有静态成员。虽然一个Inner实例与一个Outer实例相关联,但如果Java决定这样做,仍然可以使用Outer.Inner.myStatic来引用Inner的静态成员。
如果你需要在所有Inner实例之间共享某些东西,你可以将它们作为静态成员放入Outer中。这并不比在Inner中使用静态成员更糟糕,因为Outer仍然可以访问Inner的任何私有成员(并未提高封装性)。
如果你需要在由一个outer对象创建的所有Inner实例之间共享某些东西,那么将它们作为普通成员放入Outer类中就更有意义了。
我不同意“静态嵌套类实际上就是顶层类”的观点。我认为更好地把静态嵌套类/内部类视为外部类的一部分,因为它们可以访问外部类的私有成员。并且外部类的成员也是“内部类的成员”。因此,没有必要支持内部类中的静态成员,普通/静态外部类中的成员就足够了。

内部类也不是“必须”的。然而,由于语言确实提供了内部类,它应该提供一个完整和有意义的实现。 - intrepidis

4

来自:https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html

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

Oracle的解释过于肤浅和含糊。由于在内部类中没有技术或语法上的先发制人理由来阻止静态成员(其他语言如C#允许这样做),Java设计者的动机可能是概念品味和/或技术便利性问题。

以下是我的推测:

与顶级类不同,内部类是实例相关的:一个内部类实例与其外部类的每个实例相关联,并直接访问它们的成员。这是Java中拥有它们的主要动机。换句话说:内部类是为了在外部类实例的上下文中实例化而设计的。没有外部类实例,内部类就不应该比外部类的其他实例成员更可用。让我们把这称为内部类的实例相关精神。
静态成员的本质(它们不是面向对象的)与内部类的实例相关精神(它是面向对象的)相冲突,因为可以使用限定的内部类名称引用/调用内部类的静态成员而不需要外部类实例。
特别是静态变量可能会以另一种方式冒犯:与外部类的不同实例相关联的两个内部类实例将共享静态变量。由于变量是状态的一部分,这两个内部类实例实际上将独立于它们关联的外部类实例共享状态。并不是说静态变量以这种方式工作是不可接受的(我们在Java中接受它们作为面向对象编程纯粹性的合理折衷),但是在允许它们存在于其实例已经由设计与外部类实例耦合的内部类中时,可以认为存在更深层次的冒犯。禁止内部类中的静态成员,以支持“依赖于实例的精神”,还有一个额外的好处,即预防这种更深层次的面向对象编程冒犯。

另一方面,静态常量不涉及此类冒犯,因为它们没有实质性地构成状态,因此是允许的。为什么不禁止静态常量以实现最大的“依赖于实例的精神”一致性?也许是因为常量不需要占用比必要更多的内存(如果它们被强制为非静态,则将复制到每个内部类实例中,这可能是浪费的)。否则,我无法想象有例外的原因。

这可能不是牢固的推理,但在我看来,它对Oracle关于此问题的概略评论最有意义。


3

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

例如,在JDK16中,以下代码可以编译并运行,并且可以选择将哪个main()作为java Outerjava 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();
        }
    }
}

2
更多信息可以在JDK-8254321中找到。 - Marcono1234

3
简短回答:大多数程序员对作用域的心智模型并不是javac使用的模型。采用更直观的模型需要对javac进行重大改变。
内部类中静态成员变量最主要的原因是为了使代码更清晰——一个只被内部类使用的静态成员变量应该存在于内部类中,而不是放置在外部类中。请考虑以下示例:
class Outer {
   int outID;

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

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

考虑一下在getID()中使用不合格标识符“outID”时发生了什么。出现此标识符的范围应该是这样的:
Outer -> Inner -> getID()

这里再次强调,由于javac编译器的工作原理,"Outer"级别的作用域包括Outer类的静态和实例成员。这很令人困惑,因为通常我们会认为类的静态部分是作用域的另一个级别:

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

以这种思考方式,当然staticMethod()只能看到Outer的静态成员。但如果这就是javac的工作方式,那么在静态方法中引用实例变量将导致“名称无法解析”的错误。实际发生的是该名称在范围内被找到,但随后会出现额外的检查级别,并确定该名称是在实例上下文中声明的,并且正在从静态上下文中引用。
好的,这与内部类有什么关系呢?我们天真地认为内部类没有静态范围的原因是因为我们想象范围像这样工作:
Outer static -> Outer instance -> Inner instance -> getID()
         \------ Inner static ------^

换句话说,内部类中的静态声明和外部类中的实例声明都在内部类的实例上下文中范围内,但两者都不是相互嵌套的;而是都嵌套在Outer的静态范围中。
这就不是javac的工作方式 - 静态成员和实例成员有一个单独的作用域级别,并且作用域始终严格嵌套。即使继承也是通过将声明复制到子类中来实现的,而不是通过分支和搜索超类作用域来实现的。
为了支持内部类的静态成员,javac必须将静态和实例作用域拆分并支持分支和重新加入作用域层次结构,或者它必须扩展其简单的布尔“静态上下文”概念以更改跟踪当前作用域中所有嵌套类的上下文类型。

我认为允许非静态内部类具有非常量静态成员的更根本困难在于,声明这些成员的程序员可能意图将它们绑定到外部类的实例,或者使它们真正静态。在一个构造中,如果合法的话,可以明智地指定为两个不同含义之一,而且这两个含义都可以用其他明确的方式表达,那么将该构造指定为非法的比将其指定为具有任一含义要好。 - supercat

3
为什么非静态内部类中不能有静态方法?
注意:非静态嵌套类被称为内部类,因此没有所谓的“非静态内部类”。
内部类实例没有对应的外部类实例就不存在。内部类不能声明除编译时常量以外的静态成员。如果允许这样做,那么就会存在关于“静态”的含义的歧义。在这种情况下,就会出现一些混淆:
1. 这是否意味着VM中只有一个实例? 2. 或者是每个外部对象只有一个实例?
这就是设计者可能决定根本不处理这个问题的原因。
如果我将内部类设置为静态,则可以正常工作。为什么?
同样,您不能使内部类静态,而是可以声明一个嵌套的静态类。在这种情况下,这个嵌套的类实际上是外部类的一部分,并且可以拥有静态成员而没有任何问题。

3

这个话题引起了很多人的关注,我会尽最大努力用最简单的语言来解释。

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

  1. 因此,如果我们在内部类中放置一个静态成员,它将导致内部类的初始化,但不一定是外部/包含类。因此,我们破坏了类初始化顺序。

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

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


2

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


Benedikt,当你说“静态嵌套类只是一种分组方式”时,你是什么意思? - Ankur

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