关于 Java 的内部类

内部类(Inner Class)为定义在类或方法内部的类,其的特点,用途及使用原因如下——

  • 内部类中的方法可以访问该类定义所在作用域(也就是它的外部类的作用域)的成员,包括私有成员。
  • 内部类可以对包内其它类不可见(普通的 class 的修饰符只能是 public 和默认,其在同一个包内都是可见的)。
  • 内部类可利用于 lambda 表达式,匿名实现类等特性,可以实现闭包等功能。

内部类还可分为内部类和静态内部类,其在语义上和功能上都是有差异的。

我们接触到的内部类最典型的就是 Map 的 Entry,lombok 的 builder。

一般定义的内部类,其在语义上来说是和实例对应的,不过这种对应关系不是 1 对 1,是 n 对 1,因为一个实例可以创建无数个对应其的内部类实例。

类对内部类的实例化和对普通对象的代码是一样的,但是编译器在背后会做不同的工作——内部类实例化时,当前类实例的指针也会被隐式地赋予给该实例,这就使内部类的实例能够访问外部类的属性。一个示例代码见下——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InnerTest {
private int outerData = 100; // 外部类的成员
private class Inner { // 使用 private 定义,保证其在外界不可见
public void getData() {
System.out.println(++outerData); // 引用了外部类的成员!
// 完全的引用是 ++InnerTest.this.outerData
}
}
public void doSomething() {
Inner inner = new Inner(); // 初始化内部类实例,当前实例的指针(this)会被隐式赋予其。这个内部类的实例也可以作为 Solution 的成员,其将达到同样效果
// 同 this.new Inner()
inner.getData();
}
public static void main(String[] args) {
InnerTest tst = new InnerTest();
tst.doSomething(); // 101
tst.doSomething(); // 102
tst.doSomething(); // 103
// 可见每次创建新内部类实例时,其绑定的类实例都为当前实例
}
}

一个有趣的事实是,不能在其它类中直接创建 Inner 实例,不能在 static 方法中直接创建 Inner 实例——没有和它绑定的外部类对象,自然不能创建!但是可以先创建外部对象,再创建其内部对象,示例见下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InnerTest {
private int outerData = 100;
private class Inner {
public void getData() {
System.out.println(++outerData);
}
}
public static void main(String[] args) {
InnerTest tst = new InnerTest();
InnerTest.Inner inner = tst.new Inner(); // 这个语法就离谱
inner.getData(); // 101
inner.getData(); // 102
inner.getData(); // 103
}
}

使用 static 定义的内部类并非意味着这个类是单例的(Java 没有类似 Kotlin 那样的 object 修饰符)——其在语义上是和类对应的,在使用上的限制仅限于其访问修饰符了。但是也可以采用取巧的方法——在构造器中将它的外部类实例作为自己的一个指针,这样就也能够获取和普通的内部类一样的效果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class InnerTest {
private int outerData = 100;
private static class Inner { // 使用 private 定义,保证其在外界不可见
final InnerTest outer;
Inner(InnerTest outer) {
this.outer = outer;
}
void getData() {
System.out.println(++outer.outerData); // 引用了外部类的成员!
}
}
public void doSomething() {
Inner inner = new Inner(this); // 初始化内部类实例,这次是显式地把当前实例指针传进去了
inner.getData();
}
public static void main(String[] args) {
InnerTest tst = new InnerTest();
tst.doSomething(); // 101
tst.doSomething(); // 102
tst.doSomething(); // 103
}
}

静态内部类(也叫嵌套类)和常规内部类的差别在于——

  • 常规内部类保存对外围类对象的引用,静态内部类则无。
  • 静态内部类可以有 static 的成员。
  • 常规内部类需要先生成外围对象,静态内部类可以直接被生成(实际上这也让静态内部类也被称为嵌套类——只是将这个类的定义隐藏在另一个类里罢了)。

内部类可以定义在方法中,这会导致其作用域仅限于该方法作用域中。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int a = 1;
class Tmp { // 只在 main 方法内可见
void fun() {
System.out.println(a); // 可以引用上层作用域的变量,如果在实例方法中,可以引用实例的成员
System.out.println("fun!");
}
}
Tmp tmp = new Tmp();
tmp.fun(); // 如果在这里对 a 进行操作,Java 会抱怨其只能引用“实际上 final”的对象,见之前对 Java 闭包的解释
//引用实例成员的话不受此约束——这两个性质和操作都是完全不同的,可以认为引用实例成员时实际只引用了实例,对其成员的操作不会改变实例的指针
}

至于匿名内部类,lambda 表达式,函数式接口等概念已经无需再提,这里再提一点——关于{{}}语法(DBI),这其实是建立了一个匿名子类,同时通过初始化块来进行操作。如在使用 SpringMVC 构造返回结果时,可以这样写——

1
2
3
4
5
6
7
@GetMapping("/hello")
Map<String, Object> hello() {
return new HashMap<String, Object>(){{
put("hello", "world");
put("answer", 42);
}};
}

这形式其实就是构造了一个 HashMap 的匿名子类,定义一个新的初始化块并进行一些操作(这里的执行顺序是父类初始化块->父类构造器->子类初始化块)。这种形式的缺点之一是,最后生成的对象的 class 并非是 HashMap,在某些时候会出现问题,比如在 equals 方法里,比较对象类型是否相同时;比如使用 gson 进行序列化时,如此构造的对象将返回 null(这可让我吃了些苦头!)。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!