Java内部类、匿名内部类、嵌套类详解

  1. 1. 创建内部类
  2. 2. 内部类到外部类的连接
  3. 3. 在内部类中生成外部类对象的引用
  4. 4. 匿名内部类
  5. 5. 嵌套类
  6. 6. 接口中的类

本文介绍了 Java 中的内部类、匿名内部类以及嵌套类机制。

1. 创建内部类

创建内部类的方式就是把类定义放在一个包围它的类之中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.yyj;

public class Parcel1 {
class Contests {
private int x = 1;
public int value() { return x; }
}

class Destination {
private String dest;

Destination(String dest) {
this.dest = dest;
}

String getDest() { return dest; }
}

public void ship(String dest) {
Contests c = new Contests();
Destination d = new Destination(dest);
System.out.println(d.getDest());
}

public static void main(String[] args) {
// Contests c = new Contests(); // 不能这么定义
Parcel1 p = new Parcel1();
p.ship("XDU"); // XDU
}
}

2. 内部类到外部类的连接

到目前为止,内部类看上去就是一种名称隐藏和代码组织机制。当创建一个内部类时,这个内部类的对象中会隐含一个链接,指向用于创建该对象的外围对象。通过该链接,无须任何特殊条件,内部类对象就可以访问外围对象的成员。此外,内部类拥有対外围对象所有元素的访问权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.yyj;

interface Selector {
boolean end(); // 是否到尾部
Object current(); // 返回当前对象
void next(); // 指向下一个对象
}

public class Sequence {
private Object[] objs;
private int now = 0;

public Sequence(int size) {
objs = new Object[size];
}

public void add(Object x) { // 添加对象x
if (now < objs.length)
objs[now++] = x;
}

private class SequenceSelector implements Selector { // 用于移动和选择每个元素
private int i = 0;

@Override
public boolean end() { return i == objs.length; }

@Override
public Object current() { return objs[i]; }

@Override
public void next() {
if (i < objs.length) i++;
}
}

public Selector getSelector() {
return new SequenceSelector();
}

public static void main(String[] args) {
Sequence seq = new Sequence(10);
for (int i = 0; i < 10; i++)
seq.add(Integer.toString(i)); // 向Sequence中加入String对象

Selector s = seq.getSelector();
while (!s.end()) {
System.out.print(s.current() + " ");
s.next();
}
}
}

Sequence 是以类的形式包装起来的定长 Object 数组,可以调用 add() 向序列末尾增加一个新的 Object(如果还有空间)。要取得 Sequence 中的每一个对象,可以使用名为 Selector 的接口,这是迭代器(Iterator)设计模式的一个例子。

SequenceSelector 中的 end()current()next() 这些方法中的每一个都用到了外部类的字段 objs,这个引用并不是 SequenceSelector 的一部分,而是外围对象的一个 private 字段。然而,内部类可以访问外围对象的所有方法和字段,就好像拥有它们一样。

这是怎么做到的呢?对于负责创建内部类对象的特定外围类对象而言,内部类对象偷偷地获取了一个指向它的引用。然后,当你要访问外围类的成员时,该引用会被用于选择相应的外围类成员。幸运的是,编译器会为你处理所有的这些细节。

3. 在内部类中生成外部类对象的引用

要生成外部类对象的引用,可以使用外部类的名字,后面加上 .this。这样生成的引用会自动具有正确的类型,而且是可以在编译时确定并检查的,所以没有任何运行时开销,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.yyj;

public class DotThis {
void f() {
System.out.println("DotThis.f()");
}

public class Inner {
public DotThis outer() {
return DotThis.this; // 如果直接写this,引用的是Inner的this
}
}

public Inner getInner() { return new Inner(); }

public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.getInner(); // 使用外部类的对象来创建内部类
dti.outer().f(); // DotThis.f()
}
}

有时我们想让其他某个对象来创建它的某个内部类的对象,要实现这样的功能,可以使用 .new 语法,在 new 表达式中提供指向其他外部类对象的引用,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在内部类中生成外部类对象的引用

package com.yyj;

public class DotNew {
public class Inner {
void f() {
System.out.println("Inner.f()");
}
}

public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
dni.f(); // Inner.f()
}
}

我们要使用外部类的对象来创建内部类的对象,正如我们在示例代码中所看到的那样,这也解决了内部类的名字作用域问题。

除非已经有了一个外部类的对象,否则创建内部类对象是不可能的,这是因为内部类的对象会暗中连接到用于创建它的外部类对象。然而,如果你创建的是嵌套类(static 修饰的内部类),它就不需要指向外部类对象的引用。

4. 匿名内部类

我们先来看一下如何创建匿名内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.yyj;

interface AnonymousIf {
int value();
}

public class AnonymousClass {
public AnonymousIf getAnanymousIf() {
return new AnonymousIf() {
private int x = 1;

@Override
public int value() {
return x;
}
};
}

public static void main(String[] args) {
AnonymousClass ac = new AnonymousClass();
AnonymousIf ai = ac.getAnanymousIf();
System.out.println(ai.value());
}
}

这段代码的意思是创建一个继承自 AnonymousIf 的匿名类的对象,通过 new 表达式返回的引用会被自动地向上转型为一个 AnonymousIf 引用。

在这个匿名内部类中,AnonymousIf 是用无参构造器创建的,如果基类需要带一个参数的构造器,可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.yyj;

class BaseAnonyClass {
private int x;

BaseAnonyClass(int value) {
x = value;
System.out.println("Construct BaseAnonyClass with " + value);
}

int value() { return x; }
}

public class AnonymousClass {
public BaseAnonyClass getBaseAnonyClass(int value, final String str) { // 用到匿名类之外的对象需要用final修饰
return new BaseAnonyClass(value) { // 基类构造器的调用
private String name;

{ // 匿名内部类的构造器
name = str;
System.out.println("Anonymous class constructor");
}

@Override
public int value() {
return super.value() * 2;
}

@Override
public String toString() {
return name;
}
};
}

public static void main(String[] args) {
AnonymousClass ac = new AnonymousClass();

BaseAnonyClass b = ac.getBaseAnonyClass(10, "AsanoSaki");
System.out.println(b.value());
System.out.println(b);

/*
* Construct BaseAnonyClass with 10
* Anonymous class constructor
* 20
* AsanoSaki
*/
}
}

也可以在定义匿名类中的字段时执行初始化,如果你正在定义一个匿名类,而且一定要用到一个在该匿名类之外定义的对象,编译器要求参数引用需要用 final 修饰,或者是“实际上的最终变量”(也就是说,在初始化之后它永远不会改变,所以它可以被视为 final 的)。这里变量 value 被传给了匿名类的基类构造器,并没有在匿名类的内部被直接用到,因此不是必须为 final 变量。

如果必须在匿名类中执行某个类似构造器的动作,该怎么办呢?因为匿名类没有名字,所以不可能有命名的构造器。我们可以借助实例初始化,使用 {} 语句块在效果上为匿名内部类创建一个构造器。

5. 嵌套类

如果不需要内部类对象和外部类对象之间的连接,可以将内部类设置为 static 的,我们通常称之为嵌套类。要理解 static 应用于内部类时的含义,请记住,普通内部类对象中隐式地保留了一个引用,指向创建该对象的外部类对象,对于 static 的内部类来说,情况就不是这样了,嵌套类意味着:

  • 不需要一个外部类对象来创建嵌套类对象。
  • 无法从嵌套类对象内部访问非 static 的外部类对象。

从另一方面来看,嵌套类和普通内部类还有些不同。普通内部类的字段和方法,只能放在类的外部层次中,所以普通内部类中不能有 static 数据、static 字段,也不能包含嵌套类,但是嵌套类中可以包含所有这些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.yyj;

public class NestedClass {
private static class StaticInner {
static int x = 10;

public static void f() {
System.out.println("StaticInner static f()");
}

StaticInner() {
System.out.println("Construct StaticInner");
}

static class AnotherStaticClass {
public static void f() {
System.out.println("AnotherStaticClass static f()");
}
}
}

public static void main(String[] args) {
StaticInner si = new StaticInner();
StaticInner.f();
StaticInner.AnotherStaticClass.f();

/*
* Construct StaticInner
* StaticInner static f()
* AnotherStaticClass static f()
*/
}
}

可以看到我们在 main() 中并不需要创建 NestedClass 对象就能直接创建 static 的内部类对象。

6. 接口中的类

嵌套类可以是接口的一部分,放到接口中的任何类都会自动成为 publicstatic 的,因为类是 static 的,所以被嵌套的类只是放在了这个接口的命名空间内,甚至可以在内部类内实现包围它的这个接口,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.yyj;

public interface ClassInInterface {
void f();

class Inner implements ClassInInterface {
@Override
public void f() {
System.out.println("ClassInInterface.Inner f()");
}

public static void main(String[] args) {
new Inner().f();
}
}
}