为什么Java禁止在内部类中使用静态字段?

91
class OuterClass {
 class InnerClass {
  static int i = 100; // compile error
  static void f() { } // compile error
 }
} 
尽管无法使用OuterClass.InnerClass.i访问静态字段,但如果我想记录应该是静态的东西,比如创建的InnerClass对象数量,将该字段设置为静态将会很有帮助。那么,为什么Java禁止在内部类中使用静态字段/方法呢?
编辑:我知道如何使用静态嵌套类(或静态内部类)使编译器满意,但我想知道的是,如果有人对语言设计和实现方面更了解,为什么Java禁止在内部类(或普通内部类)中使用静态字段/方法。

3
我最喜欢的例子是为内部类设置一个专门的Logger。它不能是静态的,因为所有其他Loggers都是这样的。 - Piotr Findeisen
8
自Java 16版本以后,since不再适用 - 请参阅此答案 - Nicolai Parlog
12个回答

60
我想知道为什么Java禁止在内部类中使用静态字段/方法?
因为这些内部类是“实例”内部类,就像封闭对象的一个实例属性一样。
由于它们是“实例”类,允许使用静态特性没有任何意义,因为静态本来就是指在没有实例的情况下工作。
这就像您同时尝试创建静态/实例属性一样。
看以下示例:
class Employee {
    public String name;
}
如果您创建两个员工实例:
Employee a = new Employee(); 
a.name = "Oscar";

Employee b = new Employee();
b.name = "jcyang";

很明显每个对象中的name属性都有它自己的值,对吧?

内部类也是如此,每个内部类实例都独立于其他内部类实例。

因此,如果您试图创建一个counter类属性,就没有办法在两个不同的实例之间共享该值。

class Employee {
    public String name;
    class InnerData {
        static count; // ??? count of which ? a or b? 
     }
}
当你在上面的例子中创建实例a和b时,静态变量count的正确值是什么?这是不可能确定的,因为InnerData类的存在完全取决于每个封闭对象。这就是为什么当类被声明为static时,它不再需要一个生存的实例来自我维持。既然没有依赖关系,你可以自由地声明一个静态属性。我认为这听起来有些重复,但如果你考虑实例vs.类属性之间的区别,它就会有意义。

4
我会接受你对内部类静态属性解释的说法,但正如@skaffman在我的答案评论中指出的那样,那么静态方法呢?看起来好像应该允许方法存在而不需要将它们与任何实例分离。实际上,在Java中,您可以在实例上调用静态方法(尽管这被认为是不好的风格)。顺便说一句:我让一个同事尝试将OP的代码编译为C#,它可以编译。因此,C#显然允许这样做,这证明了OP想要做的事情并没有违反一些基本的OO原则。 - Asaph
2
方法也是完全一样的。这里的重点不是属性方法是否为静态,而是内部类本身是实例"东西"的事实。我的意思是,这里的问题是,除非创建外部类,否则这样的内部类实例不存在。因此,如果没有任何东西,哪种方法将被调度。你只会碰到空气,因为你首先需要一个实例。 - OscarRyz
1
关于C#...嗯。仅仅因为C#允许它并不意味着它是面向对象有效的,我并不是说它是错误的,但是C#包含了几种范式来使开发更容易,即使以一致性为代价(你必须学习每个.NET版本的新东西),它也允许这种和其他类型的事情。我认为这是一件好事。如果社区觉得一个额外的功能足够酷,C#可能会在未来拥有它。 - OscarRyz
2
@OscarRyz 为什么需要内部类的实例来使用其静态方法/字段?内部类中有一个[有用的]静态方法的示例是私有帮助器方法。 - Leonid Semyonov
1
使用 final 关键字,Java 内部类允许使用静态字段。你如何解释这种情况? - Number945
显示剩余2条评论

36

内部类的设计初衷是在封闭实例(enclosing instance)的上下文中执行操作。但某种程度上,允许静态变量和方法会违背这一动机?

8.1.2 内部类与封闭实例

内部类是一个嵌套类,没有被显式或隐式地声明为静态的。内部类不允许声明静态初始化器(§8.7)或成员接口。除非它们是编译时常量字段(§15.28),否则内部类不允许声明静态成员。


21
也许就是这样决定的。 - Gregory Pakosz
3
如果没有父引用,您无法实例化非静态内部类,但仍然可以对其进行初始化。 - skaffman
1
如果类加载器保留了一个缓存,表示“类X已被初始化”,那么它们的逻辑就不能用于初始化多个表示X的类对象(当需要在几个不同的对象内实例化内部类的类对象时,这是必需的)。 - Erwin Smout
@skaffman 这仍然没有意义。内部类的静态属性只会初始化一次,那么问题是什么?现在,我有一个静态哈希映射表,我有大约4个方法仅操作此映射表,使将所有内容分组到内部类中更理想。但是,静态哈希映射表现在必须存在于外部,并且可能与其他相关事物一起,这就很愚蠢了。静态属性初始化会有什么问题? - mjs
4
自 Java 16 开始,情况已经改变 - 参见此答案 - Nicolai Parlog

34

InnerClass不能有static成员,因为它属于一个实例(OuterClass的实例)。如果你声明InnerClassstatic,使其与实例分离,那么你的代码将会编译。

class OuterClass {
    static class InnerClass {
        static int i = 100; // no compile error
        static void f() { } // no compile error
    }
}

顺便说一下:您仍然可以创建InnerClass的实例。static在这种情况下允许在没有OuterClass的封闭实例的情况下发生。


6
InnerClass不属于OuterClass,属于它的是实例。这两个类本身没有这样的关系。为什么在InnerClass中不能有静态方法的问题仍然存在。 - skaffman

22

从Java 16开始,这种情况不再存在。引用自JEP 395(关于完成记录):

放宽长期以来的限制,即内部类不能声明显式或隐式静态成员的限制。这将变得合法,并且特别允许内部类声明一个记录类成员。

实际上,以下代码可以使用Java 16编译(已尝试使用16.ea.27):

public class NestingClasses {

    public class NestedClass {

        static final String CONSTANT = new String(
                "DOES NOT COMPILE WITH JAVA <16");

        static String constant() {
            return CONSTANT;
        }

    }

}

3
更多信息可在JDK-8254321中找到。 - Marcono1234
为什么要使用CONSTANT = **new** String(...)而不是CONSTANT = "..." - Lii
5
因为Java早就允许在非静态嵌套类中使用静态常量变量,比如字符串字面量。在Java < 15中,你可以通过CONSTANT = "..."来定义常量,而不会导致编译错误。 - Nicolai Parlog

11

实际上,如果静态字段是常量且在编译时编写,则可以声明它们。

class OuterClass {
    void foo() {
        class Inner{
            static final int a = 5; // fine
            static final String s = "hello"; // fine
            static final Object o = new Object(); // compile error, because cannot be written during compilation
        }
    }
}

9
  1. 类初始化顺序是一个关键原因。

由于内部类依赖于外围/封闭类的实例,所以在初始化内部类之前需要初始化外围类。
这是Java语言规范(JLS)中有关类初始化的内容。 我们需要注意的是,如果类T满足以下情况,则会进行初始化:

  • T声明的静态字段被使用且该字段不是常量变量。

因此,如果内部类具有访问静态字段的能力,则会导致初始化内部类,但这并不能确保外围类已经被初始化。

  1. 它会违反一些基本规则您可以跳到最后一节(到two cases)来避免新手问题

关于静态嵌套类,当某个嵌套类静态时,它将像普通类一样行事,并与外围类相关联。

但是内部类/ 非静态嵌套类的概念是,它将与外部/封闭类的实例相关联。请注意,与实例相关联意味着(从实例变量的概念出发),它将存在于一个实例中,并且在实例之间会有所不同。

现在,当我们使某些内容静态时,我们希望在加载类时初始化它,并且应该在所有实例之间共享。 但对于非静态的情况,即使是内部类本身(您现在肯定可以忘记内部类实例)也不与外部/封闭类的所有实例共享(至少在概念上是这样),那么我们如何期望内部类的某个变量将在内部类的所有实例之间共享?

因此,如果Java允许我们在非静态嵌套类中使用静态变量,则会有两种情况

  • 如果它与内部类的所有实例共享,则会违反实例上下文(实例变量)的概念。那么这是不行的。
  • 如果它未与所有实例共享,则会违反静态的概念。同样不行。

5

这里是与“限制”最相符的动机:

您可以将内部类的静态字段的行为实现为外部对象的实例字段; 因此,您不需要静态字段/方法。 我说的行为是指某个对象的所有内部类实例共享一个字段(或方法)。

因此,假设您要计算所有内部类实例的数量,您可以这样做:

public class Outer{
    int nofInner; //this will count the inner class 
                  //instances of this (Outer)object
                  //(you know, they "belong" to an object)
    static int totalNofInner; //this will count all 
                              //inner class instances of all Outer objects
    class Inner {
        public Inner(){
            nofInner++;
            totalNofInner++;
        }
    }
}

2
但问题是,当静态字段被声明为“final”时,允许它们的原因是什么? - Solace
如果您查看[https://dev59.com/e3I-5IYBdhLWcg3wPFx2#1954119](上面的OscarRyz答案):他的动机是无法将值与变量关联。当然,如果变量是final,您可以很容易地知道要分配什么值(您必须知道)。 - ianos

2
简单来说,非静态内部类是外部类的实例变量,只有在创建外部类并在运行时创建外部类对象时才会创建它们,而静态变量则是在类加载时创建的。 因此,非静态内部类是运行时的东西,所以静态不是非静态内部类的一部分。
注意:始终将内部类视为外部类的变量,它们可以像任何其他变量一样是静态或非静态的。

但是内部类可以拥有 static final 常量。 - VimNing
“非静态内部”是一个自我重复的说法。 - user207421

0
如果Inner类声明了一个静态字段并且该字段不是一个常量变量,那么当使用Inner的静态字段时,Inner类将被初始化。
class Outer{

    class Inner{

        static Inner obj = new Inner();
    }

    public static void main(String[] args){

        Inner i = Inner.obj; // It woulds violate the basic rule: without existing Outer class Object there is no chance of existing Inner class Object.
   }
}

0
因为这会导致“静态”含义上的模糊。

内部类不能声明静态成员,除了编译时常量之外。静态一词的含义会变得模糊。它是指在虚拟机中只有一个实例吗?还是每个外部对象只有一个实例?语言设计者决定不处理这个问题。

引用自Cay S. Horstmann的"Core Java SE 9 for the Impatient",第2.6.3章,第90页。


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