Java内部类和静态嵌套类

2073

Java中内部类和静态嵌套类的主要区别是什么?在选择其中一种时,设计/实现是否起到作用?


99
Joshua Bloch在《Effective Java》一书的第22条建议中提到:优先使用静态成员类而不是非静态成员类。 - Raymond Chenon
24
记录一下,它是同一本书的第三版中的第24项。 - ZeroCool
28个回答

1906

来自Java教程

嵌套类分为两种类型:静态和非静态。声明为静态的嵌套类简称为静态嵌套类。非静态嵌套类称为内部类。

使用封闭类名称访问静态嵌套类:

OuterClass.StaticNestedClass

例如,要创建静态嵌套类的对象,请使用以下语法:
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();

内部类的实例化对象存在于外部类的实例中。考虑以下类:

class OuterClass {
    ...
    class InnerClass {
        ...
    }
}

InnerClass的实例只能存在于OuterClass的实例中,并且可以直接访问其封闭实例的方法和字段。

要实例化内部类,必须先实例化外部类。然后,使用以下语法在外部对象中创建内部对象:

OuterClass outerObject = new OuterClass()
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

参见:Java教程 - 嵌套类

需要完整性地指出,还有一种没有封闭实例的内部类

class A {
  int t() { return 1; }
  static A a =  new A() { int t() { return 2; } };
}

在这里,new A() { ... }是在静态上下文中定义的内部类,并且没有封闭实例。

159
请注意,您也可以直接导入嵌套的静态类,即您可以在文件顶部执行以下操作:import OuterClass.StaticNestedClass;然后仅将类引用为OuterClass。请注意,这不会改变原来的意思。 - Camilo Díaz Repka
1
那么,我可以说“静态嵌套类”只是放置在类中的顶层类,而外部类可以被视为“命名空间”。而“嵌套类”是外部类的成员,它需要一个外部类的封闭实例吗? - Ngọc Hy
1
很好如果有解释何时使用哪个,每个的目的是什么以及每个的优缺点。 - Giorgi Tsiklauri
一个静态内部类中的属性能在多线程环境下被改变吗? - Harshana

674

Java教程中提到:

术语: 嵌套类被分为两类:静态和非静态。声明为静态的嵌套类简称为静态嵌套类。非静态嵌套类被称为内部类。

在通俗的说法中,大多数程序员使用“嵌套”和“内部”这两个词是可以互换的,但我会使用正确的术语“嵌套类”,这个术语既包括内部类也包括静态嵌套类。

类可以进行无限嵌套,例如类A可以包含类B,类B又包含类C,类C又包含类D等等,但是超过一层类的嵌套很少出现,因为通常情况下这样做设计不佳。

有三个原因可能会创建嵌套类:

  • 组织:有时将一个类排序到另一个类的命名空间中似乎是最合理的,特别是当它不会在任何其他上下文中使用时
  • 访问:嵌套类对其包含类的变量/字段具有特殊的访问权限(哪些变量/字段取决于嵌套类的类型,无论是内部类还是静态嵌套类)。
  • 方便:为每种新类型创建新文件很麻烦,尤其是当该类型仅在一个上下文中使用时

Java中有四种嵌套类,它们分别是:

  • 静态类:声明为另一个类的静态成员
  • 内部类:声明为另一个类的实例成员
  • 局部内部类:在另一个类的实例方法中声明
  • 匿名内部类:像局部内部类一样编写,但编写为返回一个一次性对象的表达式

让我详细解释一下:


静态类

静态类最容易理解,因为它们与包含类的实例无关。

静态类是作为另一个类的静态成员声明的类。就像其他静态成员一样,这样的类实际上只是使用包含类作为其命名空间的吊挂物,例如,在包pizza中声明为类Rhino的静态成员的类Goat将称为pizza.Rhino.Goat

package pizza;

public class Rhino {

    ...

    public static class Goat {
        ...
    }
}

说实话,静态类是一个相当无用的功能,因为类已经根据包被划分为命名空间。创建静态类的唯一真正可行的理由是这样的类可以访问其包含类的私有静态成员,但我认为这不足以证明静态类功能存在的合理性。


内部类

内部类是在另一个类中声明为非静态成员的类:

package pizza;

public class Rhino {

    public class Goat {
        ...
    }

    private void jerry() {
        Goat g = new Goat();
    }
}

就像静态类一样,内部类也是由其外部类名称(pizza.Rhino.Goat)限定的,但在包含类中,它可以用简单名称来表示。然而,每个内部类的实例都与其包含类的特定实例绑定在一起:在上面的例子中,jerry中创建的Goat隐式地与jerry中的this Rhino实例绑定在一起。否则,在实例化Goat时,我们需要明确关联的Rhino实例:

Rhino rhino = new Rhino();
Rhino.Goat goat = rhino.new Goat();
(注意在奇怪的新语法中,您将内部类型仅称为Goat:Java从rhino部分推断出包含类型。是的,new rhino.Goat() 对我来说也更有意义。) 这样做有什么好处呢?嗯,内部类实例可以访问包含类实例的实例成员。这些封闭实例成员在内部类中通过其简单名称引用,而不是通过this引用(在内部类中,this指的是内部类实例,而不是关联的包含类实例):
public class Rhino {

    private String barry;

    public class Goat {
        public void colin() {
            System.out.println(barry);
        }
    }
}
在内部类中,可以使用 Rhinoceros.this 来引用包含类的this,并且您可以使用 this 引用其成员,例如 Rhinoceros.this.barry 。
局部内部类是在方法体中声明的类。这样的类仅在其包含方法中已知,因此只能在其包含方法中实例化并访问其成员。获得的好处是局部内部类实例与其包含方法的最终本地变量联系在一起,并且可以访问这些变量。当实例使用包含方法的最终本地变量时,变量保留了在实例创建时它所持有的值,即使该变量已经超出了范围(这实际上是Java的简单,有限版本的闭包)。
由于局部内部类既不是类的成员也不是包的成员,因此不会具有访问级别。 (但请清楚,它自己的成员具有像正常类中一样的访问级别。)
如果局部内部类在实例方法中声明,则内部类的实例与包含方法的this所持有的实例在实例创建时绑定,因此可以像实例内部类那样访问包含类的实例成员。可以通过其名称简单地实例化局部内部类,例如局部内部类Cat 作为 new Cat() 实例化,而不是 this.Cat()
匿名内部类是编写局部内部类的语法上方便的方式。通常,在运行其包含方法时,局部内部类仅实例化一次。然后,我们可以将局部内部类定义和其单个实例化合并为一个方便的语法形式,并且我们也不必为类想出名称(代码中包含的无用名称越少,越好)。匿名内部类允许这两种事情。
new *ParentClassName*(*constructorArgs*) {*members*}

这是一个表达式,返回一个未命名的类的新实例,该类继承自ParentClassName。您不能提供自己的构造函数;相反,会隐含地提供一个构造函数,该构造函数仅调用超级构造函数,因此提供的参数必须适合超级构造函数。(如果父类包含多个构造函数,则调用“最简单”的构造函数,“最简单”由一组相当复杂的规则决定,不值得详细学习--只需注意NetBeans或Eclipse提示即可。)

另外,您可以指定要实现的接口:

new *InterfaceName*() {*members*}

这样的声明将创建一个继承自Object并实现InterfaceName接口的未命名类的新实例。同样,您不能提供自己的构造函数。在这种情况下,Java隐式地提供了一个无参、什么也不做的构造函数(因此,在这种情况下永远不会有构造函数参数)。

即使您不能给匿名内部类提供构造函数,您仍然可以使用初始化块(放置在任何方法外面的{}块)进行任何所需的设置。

请明确,匿名内部类只是一种创建具有一个实例的局部内部类的不太灵活的方式。如果您想要实现多个接口的局部内部类,或者实现继承自Object之外的某些类的接口,或者指定自己的构造函数,则必须创建常规的命名局部内部类。


48
好故事,谢谢。不过有一个错误。您可以通过Rhino.this.variableName从实例内部类访问外部类的字段。 - Thirler
1
你在注释中以“有两种类别”开始,然后在注释中间写道“有四种类型……”,这实在让我感到困惑。类别和“类型”不是一回事吗? - NoobCoder

170

我认为以上答案中并没有清楚地表明真正的区别。

首先要明确术语:

  • 嵌套类是指源代码层面上包含在另一个类中的类。
  • 如果你用 static 修饰符声明它,则它是静态的。
  • 非静态嵌套类被称为内部类。(我保持使用非静态嵌套类。)

Martin的答案到目前为止是正确的。然而,实际问题是:声明嵌套类为静态或非静态的目的是什么?

如果您只想将类组合在一起以便于管理,或者嵌套类仅在封闭类中使用,您可以使用静态嵌套类。静态嵌套类和其他类之间没有语义差异。

非静态嵌套类则是不同的类型。类似于匿名内部类,这种嵌套类实际上是闭包。这意味着它们捕获其周围的作用域和封闭实例,并使其可访问。也许一个示例会澄清这个问题。请参阅此容器桩:

public class Container {
    public class Item{
        Object data;
        public Container getContainer(){
            return Container.this;
        }
        public Item(Object data) {
            super();
            this.data = data;
        }

    }

    public static Item create(Object data){
        // does not compile since no instance of Container is available
        return new Item(data);
    }
    public Item createSubItem(Object data){
        // compiles, since 'this' Container is available
        return new Item(data);
    }
}

在这种情况下,您希望从子项引用父容器。使用非静态嵌套类,可以轻松实现此目标。使用语法Container.this可以访问Container的封闭实例。

更深入的解释如下:

如果查看Java字节码编译器为(非静态)嵌套类生成的代码,可能会变得更清晰:

// class version 49.0 (49)
// access flags 33
public class Container$Item {

  // compiled from: Container.java
  // access flags 1
  public INNERCLASS Container$Item Container Item

  // access flags 0
  Object data

  // access flags 4112
  final Container this$0

  // access flags 1
  public getContainer() : Container
   L0
    LINENUMBER 7 L0
    ALOAD 0: this
    GETFIELD Container$Item.this$0 : Container
    ARETURN
   L1
    LOCALVARIABLE this Container$Item L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 1
  public <init>(Container,Object) : void
   L0
    LINENUMBER 12 L0
    ALOAD 0: this
    ALOAD 1
    PUTFIELD Container$Item.this$0 : Container
   L1
    LINENUMBER 10 L1
    ALOAD 0: this
    INVOKESPECIAL Object.<init>() : void
   L2
    LINENUMBER 11 L2
    ALOAD 0: this
    ALOAD 2: data
    PUTFIELD Container$Item.data : Object
    RETURN
   L3
    LOCALVARIABLE this Container$Item L0 L3 0
    LOCALVARIABLE data Object L0 L3 2
    MAXSTACK = 2
    MAXLOCALS = 3
}

正如您所看到的,编译器会创建一个隐藏字段 Container this$0。构造函数中有一个额外的参数类型为 Container 来指定封闭实例。您在源代码中看不到这个参数,但编译器会隐式为嵌套类生成它。

Martin 的示例

OuterClass.InnerClass innerObject = outerObject.new InnerClass();

可以将其编译为类似以下字节码的调用

new InnerClass(outerObject)

为了完整起见:

匿名类是非静态嵌套类的典型例子,它没有与之关联的名称,并且无法在后面引用。


23
静态嵌套类和其他类之间没有语义差异,唯一的区别是嵌套类可以访问其父类的私有字段/方法,而父类也可以访问嵌套类的私有字段/方法。 - Brad Cupit
非静态内部类不会导致大量的内存泄漏吗?也就是说,每次创建监听器都会造成一次泄漏? - G_V
3
@G_V,由于内部类的实例保留了对外部类的引用,因此可能存在内存泄漏的潜在问题。这是否成为实际问题取决于对外部和内部类实例的引用在何处以及以何种方式被保留。 - jrudolph

107

我认为以上答案都没有真正解释嵌套类和静态嵌套类在应用设计方面的真正区别:

概述

嵌套类可以是非静态或静态的,在每种情况下都是在另一个类中定义的类。如果嵌套类仅为其封闭类服务,则应该存在于其中,如果嵌套类对其他类(不仅仅是封闭类)有用,则应将其声明为顶级类。

区别

非静态嵌套类:与包含类的封闭实例隐式关联,这意味着可以调用封闭实例的方法和访问变量。非静态嵌套类的一个常见用途是定义适配器类。

静态嵌套类:无法访问封闭类实例并调用其方法,因此应在嵌套类不需要访问封闭类实例时使用。 静态嵌套类的一个常见用途是实现外部对象的组件。

结论

所以从设计角度来看,两者之间的主要区别是:非静态嵌套类可以访问容器类的实例,而静态嵌套类则无法访问


从你的结论中“而静态却不能”,甚至容器的静态实例也不行吗?确定吗? - VedantK
静态嵌套类的常见用途是在RecyclerView和ListView中使用ViewHolder设计模式。 - Hamzeh Soboh
2
在许多情况下,简短的答案更加清晰和优秀。这就是一个例子。 - Eric
静态嵌套类可以访问封闭类的静态字段。 - Artiom

40

以下是Java内部类和静态嵌套类之间的关键差异和相似之处。

希望能对您有所帮助!

内部类

  • 可以访问外部类中的实例和静态方法和字段
  • 与封闭类的实例相关联,因此要实例化它首先需要一个外部类的实例(请注意new关键字的位置):

    Outerclass.InnerClass innerObject = outerObject.new Innerclass();
    
  • 无法定义任何静态成员

  • 不能接口声明

静态嵌套类

  • 无法访问外部类的实例方法或字段

  • 没有与封闭类的任何实例相关联,因此要实例化它:

  • OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
    

相似之处

  • 内部类和外部类都能访问外部类的私有字段和方法。
  • 外部类也可以访问内部类的私有字段和方法。
  • 两种类都可以具有私有、受保护或公共的访问修饰符。

为什么要使用嵌套类?

根据 Oracle 文档(完整文档)提供的解释,有以下几个原因:

  • 它是一种逻辑上分组仅在一个位置使用的类的方式: 如果一个类仅对另一个类有用,则将其嵌入该类并将这两个类保持在一起是合理的。嵌套这样的“辅助类”使它们的包更加简洁。

  • 它增加了封装性:考虑两个顶层类 A 和 B,其中 B 需要访问 A 的成员,否则这些成员将被声明为 private。通过将类 B 隐藏在类 A 中,A 的成员可以被声明为 private,并且 B 可以访问它们。此外,B 本身也可以对外部隐藏。

  • 它可以带来更可读且易于维护的代码:将小类嵌套在顶层类中可以使代码更接近其使用位置。


我不认为那完全准确。一个内部类可以包含另一个内部类。 - Scratte

39

简单来说,我们需要嵌套类主要是因为Java不提供闭包。

嵌套类是在另一个封闭类的内部定义的类。它们分为两种类型 - 静态和非静态。

它们被视为封闭类的成员,因此您可以指定任何四个访问修饰符 - private, package, protected, public。我们无法使用顶层类来拥有这种豪华,因为顶层类只能声明为public或package-private。

内部类(也称为非堆栈类)可以访问顶级类的其他成员,即使它们被声明为私有,而静态嵌套类则无法访问顶级类的其他成员。

public class OuterClass {
    public static class Inner1 {
    }
    public class Inner2 {
    }
}

Inner1是我们的静态内部类,Inner2是我们的非静态内部类。它们之间的主要区别在于,你不能创建一个没有外部类的实例Inner2,而你可以独立地创建一个Inner1对象。

何时使用内部类呢?

想象这样一种情况,类A类B相关联,类B需要访问类A的成员,且类B只与类A相关。这时就可以使用内部类了。

要创建内部类的一个实例,你需要创建外部类的一个实例。

OuterClass outer = new OuterClass();
OuterClass.Inner2 inner = outer.new Inner2();
或者。
OuterClass.Inner2 inner = new OuterClass().new Inner2();

何时使用静态内部类?

当您知道静态内部类与包含类/顶级类的实例没有任何关系时,您将定义一个静态内部类。如果您的内部类不使用外部类的方法或字段,则它只是一种浪费空间的东西,因此请将其定义为静态。

例如,要创建静态嵌套类的对象,请使用以下语法:

OuterClass.Inner1 nestedObject = new OuterClass.Inner1();

静态嵌套类的优点是它不需要包含类/顶层类的对象来工作。这有助于您在运行时减少应用程序创建的对象数量。


3
你的意思是 OuterClass.Inner2 inner = outer.new Inner2(); 吗? - Erik Kaplun
4
"static inner" 这个词本身就是个自相矛盾的说法。 - user207421
内部类也不被称为“非堆栈类”。对于非代码文本不要使用代码格式,对于代码文本则需要使用代码格式。 - user207421
而且,“静态嵌套类无法访问顶层类的其他成员”是错误的。 - undefined

26
我认为通常遵循的惯例是这样的:
  • 在顶层类中的静态类是一个嵌套类
  • 在顶层类中的非静态类是一个内部类,它还有两种形式:
    • 局部类 - 命名类声明在块内,例如方法或构造函数体内
    • 匿名类 - 未命名的类其实例在表达式和语句中被创建

然而,还需要记住一些要点

  • 顶层类和静态嵌套类在语义上相同,唯一的区别是在静态嵌套类的情况下,它可以对其外部[父]类的私有静态字段/方法进行静态引用,反之亦然。

  • 内部类可以访问外部[父]类的实例变量。但是,并不是所有的内部类都有封闭实例,例如在静态上下文中的内部类,例如在静态初始化程序块中使用的匿名类,没有。

  • 匿名类默认扩展父类或实现父接口,并且没有进一步的子句来扩展任何其他类或实现更多接口。所以,

    • new YourClass(){}; 的意思是 class [Anonymous] extends YourClass {}
    • new YourInterface(){}; 的意思是 class [Anonymous] implements YourInterface {}

我认为仍然存在一个更大的问题,即何时使用哪个?这在很大程度上取决于您正在处理的情景,但阅读@jrudolph给出的回答可能有助于您做出一些决策。


15

嵌套类:在类内部定义的类

类型:

  1. 静态嵌套类
  2. 非静态嵌套类[内部类]

区别:

非静态嵌套类[内部类]

在非静态嵌套类中,内部类的对象存在于外部类的对象中。因此,外部类的数据成员可以被内部类访问。所以,要创建内部类的对象,我们必须先创建外部类的对象。

outerclass outerobject=new outerobject();
outerclass.innerclass innerobjcet=outerobject.new innerclass(); 

静态嵌套类

在静态嵌套类中,内部类的对象不需要外部类的对象,因为“static”关键字表明不需要创建对象。

class outerclass A {
    static class nestedclass B {
        static int x = 10;
    }
}

如果你想要访问x,那么在方法内部写入以下内容

  outerclass.nestedclass.x;  i.e. System.out.prinltn( outerclass.nestedclass.x);

13

内部类的实例在外部类的实例创建时同时创建。因此,内部类的成员和方法可以访问外部类实例(对象)的成员和方法。当外部类实例的作用域结束时,内部类实例也将不再存在。

静态嵌套类没有具体的实例。它们只是在第一次使用时加载(就像静态方法一样)。它是一个完全独立的实体,其方法和变量没有任何访问外部类实例的权限。

静态嵌套类与外部对象无关,速度更快,且不占用堆/栈内存,因为无需创建此类的实例。因此,经验法则是尝试定义静态嵌套类,并尽可能限制其范围(private >= class >= protected >= public),如果确实有必要,则将其转换为内部类(通过删除 "static" 标识符)并放宽范围。


3
第一句话是不正确的。不存在“内部类的唯一实例”这样的概念,它的实例可以在外部类被实例化后任何时候创建。第二句话并不是由第一句话推导出来的。 - user207421

11

使用嵌套静态类的一个微妙之处在于,在某些情况下可能会很有用。

与通过其构造函数实例化类之前实例化静态属性不同, 嵌套静态类内部的静态属性似乎直到调用类的构造函数,或者至少是在首次引用这些属性之后才被实例化,即使它们被标记为“final”。

考虑以下示例:

public class C0 {

    static C0 instance = null;

    // Uncomment the following line and a null pointer exception will be
    // generated before anything gets printed.
    //public static final String outerItem = instance.makeString(98.6);

    public C0() {
        instance = this;
    }

    public String makeString(int i) {
        return ((new Integer(i)).toString());
    }

    public String makeString(double d) {
        return ((new Double(d)).toString());
    }

    public static final class nested {
        public static final String innerItem = instance.makeString(42);
    }

    static public void main(String[] argv) {
        System.out.println("start");
        // Comment out this line and a null pointer exception will be
        // generated after "start" prints and before the following
        // try/catch block even gets entered.
        new C0();
        try {
            System.out.println("retrieve item: " + nested.innerItem);
        }
        catch (Exception e) {
            System.out.println("failed to retrieve item: " + e.toString());
        }
        System.out.println("finish");
    }
}
即使“嵌套”和“innerItem”都被声明为“static final”,但是设置nested.innerItem直到类被实例化(或至少在嵌套静态项首次被引用之后)才会发生,可以通过注释上面提到的行并取消注释来自己验证。对于“outerItem”则不适用。这至少是我在Java 6.0中看到的。

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