为什么我们不能在(非静态的)内部类(Java16之前)中使用静态方法?

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

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

如果我让内部类是静态的,它会工作。为什么?

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

在 Java16 + 中,这两者都是有效的。

75159 次浏览

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

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

允许对静态嵌套类使用静态方法

public class Outer {


public static class Inner {


public static void method() {


}
}
}

我有个理论,也许正确,也许不正确。

首先,你应该知道一些内部类是如何在 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 字段 允许(只要它们不引用封闭的对象)。

在非静态内部类中允许静态方法没有多大意义; 如何访问它?如果不通过外部类实例,则无法访问(至少在初始阶段)非静态内部类实例。创建非静态内部类没有纯静态的方法。

对于外部类 Outer,您可以访问静态方法 test(),如下所示:

Outer.test();

对于静态内部类 Inner,您可以像下面这样访问它的静态方法 innerTest():

Outer.Inner.innerTest();

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

语言设计者允许这样做是正确的。然后,它们必须小心地禁止对非静态内部类的静态方法中的外部类的隐式引用的任何访问。此时,如果除静态外不能引用外部类,那么作为内部类的值是什么?如果静态访问是可行的,那么为什么不声明整个内部类是静态的呢?如果只是使内部类本身是静态的,那么就没有对外部类的隐式引用,并且不再具有这种模糊性。

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

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

简短的回答: 大多数程序员关于作用域如何工作的心智模型并不是 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 ()当然只能看到外部的静态成员。但是如果 javac 就是这样工作的,那么在静态方法中引用实例变量会导致“名称无法解析”错误。实际发生的情况是,在作用域中找到了名称,但随后进行了额外的检查,并发现名称是在实例上下文中声明的,并且是从静态上下文引用的。

好吧,这和内部课程有什么关系?我们天真地认为,内部类没有理由不能有一个静态范围,因为我们想象的范围是这样工作的:

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

换句话说,内部类中的静态声明和外部类中的实例声明都位于内部类的实例上下文中的作用域中,但是这两者实际上都没有嵌套在另一个实例上下文中; 相反,它们都嵌套在外部类的静态作用域中。

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

为了支持内部类的静态成员,javac 将不得不分离静态和实例作用域 还有支持分支和重新加入作用域层次结构,或者它将不得不扩展其简单的布尔型“静态上下文”思想来改变以跟踪当前作用域中所有级别的嵌套类的上下文类型。

唯一的理由是“不是必须的”,那么为什么还要费心去支持它呢?

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

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

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

我不同意“静态嵌套类基本上只是一个顶级类”的观点。我认为最好将静态嵌套类/内部类视为外部类的一部分,因为它们可以访问外部类的私有成员。外部阶级成员也是“内部阶级成员”。因此在内部类中不需要支持静态成员。外部类中的普通/静态成员就足够了。

将内部类成员作为静态成员是无用的,因为您首先无法访问它们。

想想看,要访问一个静态成员,您使用 className.memName,在我们的示例中,它应该类似于 outerclassName.innerclassName.memName,,现在您明白为什么 innerclass 必须是静态的了吧... 。

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

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

没有外部类的对应实例,内部类实例就不存在。内部类不能声明编译时常量以外的静态成员。如果允许这样做,那么 static的含义就会含糊不清。在这种情况下,会出现某些混乱:

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

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

如果我让内部类是静态的,它会工作。为什么?

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

首先,为什么有人想要在非静态内部类中定义静态成员?答案是,这样外部类成员可以使用那些只有内部类名称的静态成员,对吗?

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

比如下面的代码,

public class Outer {


class Inner {


public static void method() {


}


}


}

可以这样写

public class Outer {


void method() {


}


class Inner {




}


}

因此,在我看来,Java 设计者不允许这个功能,这样做不会使代码复杂化,或者我们可以在将来的版本中看到这个功能带有更多的特性。

试着把这个类当作一个普通的字段,那么你就会明白。

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

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

首先,引用 http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.1,在第一次发生/调用任何成员之前(之前是 static 关键字) ,立即初始化一个类或接口。

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

  2. 还要考虑这样一个事实,即非静态内部类与封闭/外部类的实例相关联。因此,与实例关联将意味着,内部类将存在于外部类实例中,并且在实例之间是不同的。

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

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

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

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

我的推测是:

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

静态成员(不是面向对象的)的本质与内部类(是面向对象的)的 视乎实例而定精神相冲突,因为你可以通过使用限定的内部类名来引用/调用内部类的静态成员,而不需要外部类实例。

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

另一方面,静态常量不会造成这样的冒犯,因为静态常量不能有意义地构成状态,所以这些静态常量是允许的。为什么不禁止静态常量与 依赖实例的精神的最大一致性?也许是因为 常数不需要占用过多的内存(如果它们被迫成为非静态的,那么它们就会被复制到每个内部类实例中,这可能会造成浪费)。否则我无法想象出现异常的原因。

这可能不是一个坚如磐石的推理,但是 IMO 认为甲骨文在这个问题上草率的评论是最有意义的。

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();
}
}
}