学习笔记 - Java 中的 Nested Classes


1. 静态嵌套类与非静态嵌套类

写过 Java 的话想必对嵌套类(nested class)并不陌生,其中包括两类,一类是被static修饰的,称为静态嵌套类;另一类则没有被static修饰,一般称作内部类(inner class)。

对于静态嵌套类来说,就和一般的静态方法一样,可以理解成为了封装的需要,作为嵌套类出现。静态嵌套类可以单独实例化,即不需要外部类的实例。因此从语义上讲,把静态嵌套类写成一个完全独立的类是完全可以的。一个典型的例子是在实现链表类的时候需要一个节点类作为静态嵌套类。

但是反过来,对于非静态嵌套类,即内部类来说,其存在的初衷即为了能够在该内部类的内部对外部类的变量或者方法进行直接访问,即这个内部类的状态与外部类的状态息息相关。并且内部类不能单独实例化。一个典型的例子就是在容器类中通常有一个迭代器类作为内部类。

2. 局部类与匿名类

在内部类中有两个特殊的情形——局部类(local class)和匿名类(anonymous class)。

2.1 局部类

局部类可以在任意代码块中声明,例如方法体、循环体、分支等等。局部类同样可以访问外部类的变量和方法。并且,局部类还可以访问在当前代码块中被声明为final的局部变量。到了Java SE 8,该限制放宽为了当前代码块中被声明为final或者effectively final的变量和参数。这里的effectively final的意思是一个变量或参数的值在初始化后不再被改变。因此,本质上effectively finalfinal是一样的。

为什么局部类所能访问的局部变量和参数需要有这样的限制呢?因为局部类的实例在其所在的函数体返回后仍然存在,因此为了保证这样的访问是有效的,在实例化的时候会对所需要访问的变量或者参数进行一份拷贝。所以局部类所谓的访问局部变量和参数,实际上是访问他们的一个拷贝。也因此,如果不对可以访问的局部变量和参数加以final的限制的话,就会出现数据不一致的情况。

需要注意的是,局部类作为非静态嵌套类的一种,能够声明和定义的静态事物仅限于被final修饰的原生类型和字符串,并且被编译时就可以确定的常量初始化。类似于一般的静态变量和静态方法都不能够声明和定义。特别的,也不能在局部类中声明接口(interface),因为接口本质上也是静态的。

2.2 匿名类

匿名类可以说是局部类的一个特例。相较于局部类,匿名类的作用完全是为了让你的代码更简洁。匿名类可以同时对一个局部类进行声明和实例化。当你只需要实例化这个局部类一次的时候,用匿名类可以使得你的代码变得更简洁,但是如果你需要实例化不止一次,那么匿名类反而会显得很冗余。

匿名类的一个基本用法如下所示,其中Typename可以是类名也可以是接口名,分别表示该匿名类继承了一个类或实现了一个接口。

Typename t = new Typename() {
    // like normal local class definition
};

一个典型的使用场景是多线程编程时需要实现一个简单的Runnable接口。

Runnable r = new Runnable() {
    @Override
    void run { ... }
}
new Thread(r).start(); // 新线程

需要注意的是,匿名类和局部类一样在静态事物的声明和定义上受限。此外,匿名类不能定义任何构造器(constructor)。从语法上也可以看出,由于匿名类的定义和实例化是同时的,即使能够定义构造函数也无法进行调用。如果匿名类实现了一个接口,那么调用的是一个编译器给的默认空构造器,因为确实也没有任何需要运行时构造的变量。如果匿名类是继承了一个类,那么调用的就是相对应的父类构造器。看了下面这个例子就很清晰了。

public class AnonyClass {
    public int id = 1;

    AnonyClass() { }

    AnonyClass(int id) {
        this.id = id;
    }

    public void print() {
        System.out.println("father");
    }

    public static void main(String[] args) {
        AnonyClass s1 = new AnonyClass() {
            public void print() {
                System.out.println("son1");
            }
        };

        AnonyClass s2 = new AnonyClass(2) {
            public void print() {
                System.out.println("son2");
            }
        };

        s1.print();
        System.out.println(s1.id);
        s2.print();
        System.out.println(s2.id);
    }
}

// stdout:
// > son1
// > 1
// > son2
// > 2

最后需要注意的是,虽然匿名类可以创建新的变量(非静态或静态常量)以及非静态函数,但是由于我们在声明该实例时使用的是其父类或者接口的符号,因此不能通过该实例变量访问新增的变量或函数(否则会编译错误)。如果一定要访问,那么只能通过如下形式。

public class AnonyClass {

    public static void main(String[] args) {
        new AnonyClass() {
            int id = 1;
            public void print() {
                System.out.println(id);
            }
        }.print();

        System.out.println(new AnonyClass() {
            int id = 1;
        }.id);
    }
}

但是这样就会显得很僵硬,所以个人感觉如果是新增的变量和函数,都应该只在匿名类内部访问。

参考


文章作者: Shun Zhang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Shun Zhang !
  目录