JavaSE学习笔记 - 内部类

  虽然内部类这一块的内容没有Java某些部分多,但是当我们在学习或工作中或多或少地接触到内部类时,就会因某些奇葩的问题而碰壁。确实,平常我们接触最多的就是集合类容器以及反射这两大版块,而内部类的接触和使用是相对少很多,最多就在事件监听器相关的代码时碰到内部类,因此我们就很容易在这块遇到问题。现在这就来将内部类的方方面面知识详细总结一下。此处主要参考的是《Thinking In Java》和《Java核心技术》。

什么是内部类?

一个类定义在另一个类中,不管它处于外部类的哪个作用域,这个类就是内部类。

内部类的种类

内部类有多个种类,细分下一共有4种不同类型内部类:

  • 成员内部类(member innber class)
  • 静态内部类(static inner class)
  • 局部内部类(local inner class)
  • 匿名内部类(anonymous inner class)

所有类型内部类的共性就是:“内部类可以访问外部类的数据和方法,不管它们是否被private所修饰”,就像是内部类拥有它们似的。下面详细介绍这几种内部类。

成员内部类

  • 定义
成员内部类直接定义在类中,它是外部类的成员之一,并且不使用static修饰。
  • 代码

成员内部类是最普通的内部类,但也是平常使用比较频繁的内部类,它直接定义在另一个类的内部。

1
2
3
4
5
public class OuterClass {
public class InnerClass {

}
}

这看起来很简单,InnerClass就是OuterClass的成员内部类,而OuterClass称为外部类封闭类外围类、英文术语<enclosing class>,通常我们叫它外部类。


成员内部类可以无条件地访问外部类所有成员,不管它们静态与否。
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
53
54
55
56
interface Selector {
boolean end();

Object current();

void next();
}

public class Sequence {
private Object[] items;
private int next = 0;

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

public void add(Object item) {
if (next < items.length) {
items[next++] = item;
}
}

private class SequenceSelector implements Selector {
private int i = 0;

public boolean end() {
return i == items.length;
}

public Object current() {
return items[i];
}

public void next() {
if (i < items.length) {
i++;
}
}
}

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

public static void main(String[] args) {
Sequence sequence = new Sequence(10);
for (int i = 0; i < 10; i++) {
sequence.add(Integer.toString(i));
}
Selector selector = sequence.selector();
while (!selector.end()) {
System.out.print(selector.current() + " ");
selector.next();
}
}
}

从上面这段代码可以让我们很清楚地看到,成员内部类SequenceSelector没有使用任何约束条件就访问到外部类Sequence的成员items。更进一步,外部类的所有成员都可以被成员内部类无条件访问,不管它们是不是静态的,也不管它们是什么可见性。

此刻,又引申出一个问题:如果外部类和成员内部类有同名成员,那在该内部类如何访问这些外部类的成员呢?

如果我们在内部类没有使用任何约束来访问外部类的同名成员,默认是访问成员内部类的成员,这是隐藏(hide)现象,通常发生在有继承关系的类之间,针对的是双方的成员变量、静态方法以及成员内部类。

成员内部类访问外部类的同名成员,此时必须使用这个特殊语法:外部类.this.同名成员变量外部类.this.同名成员方法,这个统称为.this语法.

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
public class OuterClass {
private int val = 0;

public void foo() {
System.out.println("OuterClass.foo()");
}

public class InnerClass {
private int val = 1;

public void foo() {
System.out.println("InnerClass.foo()");
}

public void test() {
// field
System.out.println("OuterClass.val = " + OuterClass.this.val);
System.out.println("InnerClass.val = " + val);

// method
OuterClass.this.foo();
foo();
}
}
}
既然成员内部类能够访问外部类的成员,那反过来呢?

这时我们就可以这样推断了:一个普通的类,如果要访问它的数据成员或成员方法,首先要做的就是创建它的对象,然后再通过这个对象的引用进行访问。在外部类访问成员内部类的成员也是一样的道理,在进行访问之前肯定要先创建内部类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OuterClass {
private int outer_val = new InnerClass().inner_val;

public void outerMethod() {
InnerClass inner = new InnerClass();
System.out.println("inner_val = " + inner.inner_val);
System.out.println("outer_val + 1 = " + outer_val + 1);
inner.innerMethod();
}

public class InnerClass {
private int inner_val = 0;

private void innerMethod() {
System.out.println("InnerClass.innerMethod()");
}
}
}

从上面的代码示例可以看出:外部类只要有内部类对象的引用,内部类的任何成员都可以被轻松访问。


成员内部类是外部类的普通成员

成员内部类是外部类的普通成员,就像是类中其它的非静态元素一样,比如非静态数据成员和非静态成员方法。

类中的所有普通成员都是依赖于类对象的存在而存在的。这就像你有一张永动机的图纸,但你没有按照图纸来创建出永动机的实物,那图纸中所描绘的都不会存在。类就像一张蓝图,而对象则是蓝图的产品,而产品中的各种部件就是类中的成员实例。

所以这就可以推断出:“要使用成员内部类创建对象,首先要创建外部类的对象”

此时,则要使用特殊语法。
使用this.new InnerClassName()在外部类中(除静态块和静态方法)创建成员内部类对象,使用outerClassObj.new InnerClassName()在外部类的外部以及外部类中的静态块和静态方法中创建内部类对象。这个统称为.new语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SuppressWarnings("unused")
public class OuterClass {
public class InnerClass { }

public void outerMethod() {
InnerClass inner_a = this.new InnerClass();
InnerClass inner_b = new InnerClass();
}

public static void main(String[] args) {
OuterClass outer = new OuterClass();
InnerClass inner = outer.new InnerClass();
}
}

成员内部类是外部类的一个普通成员,所以它可以被各种访问权限修饰符所修饰:

  • private修饰,那只能在外部类的内部访问
  • protected修饰,那只能在同一个包下或外部类的继承链中访问
  • public修饰,任何地方都可以访问它
  • 默认访问权限修饰,那只能在同一个包下访问


成员内部类内部不可以定义静态成员。

其实这一点也是很容易理解的。这好比我们知道在Java的普通成员方法中是绝对不可以定义静态数据,这和C/C++还是有区别的,Java定死的规则我们无法改变。

静态内部类

  • 定义
静态内部类其实就是静态的成员内部类,在Java中有时也称为嵌套类(nested class);它也是直接定义在类中,并使用static修饰。
  • 代码
静态内部类是外部类的静态成员,它就像类中的其它静态元素一样,就像类的静态数据成员或静态方法,不与类对象相关联。
1
2
3
4
5
public class OuterClass {
public static class StaticInnerClass {

}
}


静态内部类只能访问外部类的静态成员,而不能访问外部类的非静态成员,并且静态内部类能够定义静态成员
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
@SuppressWarnings("unused")
public class OuterClass {
private int outer_val = 0;
private static int static_outer_val = 1;

public void outerMethod() {
int inner_val = new StaticInnerClass().inner_val;
int static_inner_val = StaticInnerClass.static_inner_val;
}

public static void staticOuterMethod() {
System.out.println("OuterClass.staticOuterMethod()");
}

public static class StaticInnerClass {
private int inner_val = 0;
private static int static_inner_val = static_outer_val;

public void innerMethod() {
System.out.println("StaticInnerClass.innerMethod()");

// compile error
//! int val = outer_val;
//! outerMethod();
}

public static void staticInnerMethod() {
System.out.println("StaticInnerClass.staticInnerMethod()");
staticOuterMethod();
}
}
}

如果不需要内部类对象与其外部类对象之间有联系,那么就使用静态内部类。在这4种内部类中,也只有静态内部类内部能够定义静态成员。

局部内部类

  • 定义
局部内部类是定义在方法或任意的作用域中。
  • 代码
局部内部类对外部世界完全隐藏起来,除了该内部类所在的作用域,其它地方都不可以访问到它。
1
2
3
4
5
6
7
8
9
10
11
12
13
@SuppressWarnings("unused")
public class OuterClass {
// compile error
//! private LocalInnerClass inner = new LocalInnerClass();


public void outerMehtod() {
class LocalInnerClass {

}
LocalInnerClass localInner = new LocalInnerClass();
}
}

局部内部类是这4种内部类中平时最少用到的。


局部内部类不能被访问权限修饰符以及static所修饰,但能够被abstractfinal修饰。
1
2
3
4
5
6
7
public class OuterClass {
public void outerMehtod() {
// Illegal modifier for the local class LocalInnerClass;
// only abstract or final is permitted
// private class LocalInnerClass { }
}
}


局部内部类能否访问外部类的非静态成员,那主要得看局部内部类是定义在哪里了。
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
@SuppressWarnings("unused")
public class OuterClass {
private int outer_val = 0;
private static int outer_static_val = 1;

{
class LocalInnerClass {
private int val_a = outer_val;
private int val_b = outer_static_val;
}
}

static {
class LocalInnerClass {
// cannot make a static reference to the non-static field outer_val
//! private int val_a = outer_val;
private int val_b = outer_static_val;
}
}

public void outerMehtod() {
class LocalInnerClass {
private int val_a = outer_val;
private int val_b = outer_static_val;
}
}

public static void staticOuterMethod() {
class LocalInnerClass {
// cannot make a static reference to the non-static field outer_val
//! private int val_a = outer_val;
private int val_b = outer_static_val;
}
}
}

上面的代码也很好说明,局部内部类能否访问外部类的非静态成员,那得看所定义处的方法或作用域是否为非静态的。静态块和静态方法内定义的局部内部类肯定是不能访问外部类的非静态成员的,就像静态方法不能访问类的普通成员一样,因为没有this关联。


局部内部类只能访问方法或作用域中定义的final局部变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
@SuppressWarnings("unused")
public class OuterClass {
public void outerMethod() {
int val_a = 0;
final int val_b = 1;

class LocalInnerClass {
// cannot refer to a non-final variable val_a inside an inner class defined in a different method
//! private int inner_val_a = val_a;
private int inner_val_b = val_b;
}
}
}

这一点也是Java所规定死的,但是为何要这样做,文章后面会有详细解释。

匿名内部类

  • 定义
从字面上就能理解,匿名内部类就是没有名字的内部类。
  • 代码
匿名内部类可以在任意处定义,它隐式地继承一个父类或者实现一个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface OnChangeListener {
public void onChange();
}

@SuppressWarnings("unused")
public class OuterClass {
// anonymous inner class
private OnChangeListener listener = new OnChangeListener() {
@Override
public void onChange() {
System.out.println("onChange()");
}
};

public OnChangeListener getListener() {
// anonymous inner class
return new OnChangeListener() {
@Override
public void onChange() {
System.out.println("onChange()");
}
};
}
}

此处可看到,定义匿名内部类时并不使用关键字classextendsimplements;更不能被staticfinal所修饰。
并且,定义的通用格式为:

1
2
3
new SuperType {
...
};


匿名内部类没有命名的构造器,但是可以用实例初始化块达到构造器的效果。
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
abstract class Base {
public Base(int i) {
System.out.println("Base constructor, i = " + i);
}

public abstract void f();
}


public class AnonymousConstructor {
public static Base getBase(int i) {
return new Base(i) {
{
System.out.println("Inside instance initializer");
}

@Override
public void f() {
System.out.println("In anonymous f()");
}
};
}

public static void main(String[] args) {
Base base = getBase(17);
base.f();
}
}

对于匿名内部类而言,实例初始化块就是它唯一的构造器,有且仅有这一个构造器,而且不能重载它。很多时候,可以使用局部内部类来代替匿名内部类所产生的效果是一样的,但是这样代码就会变得冗长且难以维护。


匿名内部类经常被我们所用,但它的使用范围却非常有限,一般匿名内部类被用作编写事件监听代码,即接口回调。其中,图形化界面代码就经常用到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.awt.Button;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class AnonymousClass {
public static void main(String[] args) {
Button btn = new Button();
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {

}
});
}
}

最后在此啰嗦地补充一句,匿名内部类和外部类相互访问成员的规则与之前所介绍过的几种内部类一样。

内部类的深层细节

疑问1 - 成员内部类的访问权限

  
  第一个疑问:为什么成员内部类能够无条件地直接访问外部类的非静态成员?之前在介绍那几种内部类的时候我们也看到:成员内部类能够无条件地访问外部类的所有成员,而同样是非静态内部类的局部内部类以及匿名内部类能否无条件地访问外部类的所有成员,那要取决于它们定义的位置。所以,此处就拿成员内部类作为例子,只要能够了解到它内在的原理,那我们就能用同样的原理去解释其它内部类了。

首先,我们来看看《Thinking In Java》里是如何描述这一个问题的:

当生成一个内部类对象时,此对象与制造它的外围类对象(enclosing object)之间就有了一种联系,实际上,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在访问外围类的成员时,就会用那个引用来选择外围类的成员,因此,无需任何特殊条件就能访问外围类的成员。幸运的是,编译器会帮你处理所有的细节。

按照书上这么说,编译器会帮助我们处理内部类的所有细节,那这时我们就要来看看编译后字节码文件,或许就能得知其中实现的细节。

下面还是采用最简单的成员内部类代码作为例子。

1
2
3
4
5
public class Outer {
public class Inner {

}
}

这里使用jdk自带的反汇编工具javap查看java编译器编译java源代码后所生成的字节码,输入javap -c Outer$Inner>bytecode.txt这条命令,这样对应类的字节码信息就会保存在文本文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
Compiled from "Outer.java"
public class com.gtr.Outer$Inner {
final com.gtr.Outer this$0;

public com.gtr.Outer$Inner(com.gtr.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #10 // Field this$0:Lcom/gtr/Outer;
5: aload_0
6: invokespecial #12 // Method java/lang/Object."<init>":()V
9: return
}

上面这个就是Outer$Inner.class的字节码信息。我们可以发现这和源代码很相似,但却有一点点不相同,或许这些不同之处就是编译器编译源代码后所处理的细节。

首先,我们把集中力放到第3行代码上。

1
final com.gtr.Outer this$0;

这是编译器编译成员内部类Inner后给它增加了一个数据成员,并且这个数据成员是指向外部类Outer对象的引用。
看到这里,或许你就能想到:内部类就是用这个额外添加的外部类引用this$0访问外部类的非静态数据成员

但此处还有个小小的疑问:那这个引用的值是从何而来的呢

要解答这个问题则要从第5行内部类的构造器以及第8、9这2条字节码指令开始看了。

1
2
3
4
public com.gtr.Outer$Inner(com.gtr.Outer);

1: aload_1
2: putfield #10 // Field this$0:Lcom/gtr/Outer;

  源代码中的成员内部类Inner并没有定义构造器,所以默认使用无参构造器,但是这里我们也可以很清楚地看到,编译器会给无参构造器添加一个参数,这个参数也是指向外部类对象的引用。接着,剩下的2条字节码指令所做的操作是至关重要的。这时,大家就算看不懂这些奇怪的字节码指令,想必也能猜到这2条指令所发挥的具体作用了吧?
  其中,指令aload_1就是把指向外部类对象的引用这一参数加载到操作数栈,而指令putfield就是把刚才加载到操作数栈的引用值赋值给内部类的数据成员this$0进行初始化。

最后小结一下:

  • 在成员内部类中是使用指向外部类对象的引用成员this$0访问外部类的非静态成员,只不过这细节是编译器帮我们处理
  • 这也更有说服力地证明了之前所下的定论“要使用成员内部类创建对象,首先要创建外部类的对象”,
    如果没有创建外部类对象,则无法调用已经被编译器修改过的成员内部类构造器为成员this$0赋值,这也就无法创建成员内部类对象

疑问2 - 必须用final修饰局部对象

  同时有第二个疑问:局部内部类访问外部定义的局部对象,为什么这个对象就一定要被final修饰?这个问题对于初学者来说确实是一个难题,很多书都是一句话带过就是泛泛而谈,读者对此只能是一知半解,或者也就是死记硬背这是Java中无法改变的规定。估计大多数人刚学Java的时候,面对这个问题都是这种境况,包括我自己也是这样。不过也未必,如果你精通其它编程语言,特别是C/C++这种语言,或许能举一反三来理解Java这种做法也不是不可能的。

现在还是来看一下《Thinking In Java》里所说的再继续从字节码上深究也不迟。

如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。如果你忘记了,将会得到一个编译时错误消息。

这本书只提到过这一点,并且在内部类这一章也没有对这个问题给出更多的解释。对于初学者来说,利弊都有,但个人觉得书本还是应该把这一问题说清楚会更好。而另外一本书《Java核心技术》里面对这一问题解释得比较清楚,而且还用字节码说明了这一个问题。

现在使用一个匿名内部类作为例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface OnChangeListener {
void onChange();
}

public class Outer {
public OnChangeListener getOnChangeListener(final int parameter) {
final int localVarA = 31;
final Integer localVarB = new Integer(31);

return new OnChangeListener() {
@Override
public void onChange() {
System.out.println(parameter);
System.out.println(localVarA);
System.out.println(localVarB);
}
};
}
}

代码很简单,自定义了一个监听器接口,并使用它来定义了匿名内部类,并在匿名内部类内使用外部定义的局部对象。就像之前所说道那样,它们必须用final修饰,不然会有编译时错误。Java为何要求我们这样做呢?下面就来逐步解开这个问题。

现在,我们再来仔细想想上面那段代码的执行流程:

  • 在某个地方,使用Outer对象调用getOnChangeListener方法
  • 随后便返回实现OnChangeListener接口的匿名内部类对象
  • 方法getOnChangeListener返回后执行完毕,方法结束,属于这方法的局部对象都会被销毁,然而返回的OnChangeListener对象生命周期还没结束
  • 在某个时刻,onChange方法被回调,执行内部的代码时使用了parameterlocalVarAlocalVarB3个局部对象

按照这逻辑可推断出:匿名内部类的对象和局部对象两者的生命周期长短不一致,导致内部类对象会访问可能已经不存在的局部对象

为了解决这一个问题,Java就运用了复制这种对策,从而“延长”局部对象的生命周期。

最终结果:实际上,局部内部类对象访问的是局部对象的复制品

局部内部类对象访问外部定义的局部对象时,实际上访问的是局部对象的复制品,这样就铁定会出现下面这种情境:

在内部类中修改局部对象的值(实际上是修改局部对象的复制品),然后在内部类的外部再使用这个局部对象,发现它的值根本没有改变

所以,使用复制这种策略虽然解决了生命周期问题,但最终实现的却不是“局部内部类访问外部定义的局部对象“这个的语义。

现在,我们终于可以着手解决最后一个问题了:
怎样做才是使得局部内部类访问局部对象的复制品与局部内部类访问真正的局部对象两者的语义效果保持一致?

答案很明显:使用final修饰局部对象

我们想要了解更多细节,现在还是来看匿名内部类的字节码,执行命令javap -c -private Outer$1>bytecode.txt后便可以得到以下字节码信息。

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
Compiled from "Outer.java"
class com.gtr.Outer$1 implements com.gtr.OnChangeListener {
final com.gtr.Outer this$0;

private final int val$parameter;

private final java.lang.Integer val$localVarB;

com.gtr.Outer$1(com.gtr.Outer, int, java.lang.Integer);
Code:
0: aload_0
1: aload_1
2: putfield #16 // Field this$0:Lcom/gtr/Outer;
5: aload_0
6: iload_2
7: putfield #18 // Field val$parameter:I
10: aload_0
11: aload_3
12: putfield #20 // Field val$localVarB:Ljava/lang/Integer;
15: aload_0
16: invokespecial #22 // Method java/lang/Object."<init>":()V
19: return

public void onChange();
Code:
0: getstatic #30 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #18 // Field val$parameter:I
7: invokevirtual #36 // Method java/io/PrintStream.println:(I)V
10: getstatic #30 // Field java/lang/System.out:Ljava/io/PrintStream;
13: bipush 31
15: invokevirtual #36 // Method java/io/PrintStream.println:(I)V
18: getstatic #30 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_0
22: getfield #20 // Field val$localVarB:Ljava/lang/Integer;
25: invokevirtual #42 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
28: return
}

匿名内部类访问外部定义的局部对象时,编译器对于基本类型和引用类型所作出的处理细节是有所不同的。

首先来看onChange方法中的一条字节码指令

1
13: bipush        31

这条指令的意思是:将一个常量加载到操作数栈,常量值是31。这个值刚好和源代码中localVarA的值是一样的。这个值是存储在常量池中,当匿名内部类有代码要使用localVarA时,便引用常量池中对应对象的值。对于在编译期就能确定字面值的基本类型局部对象,局部内部类都会直接引用常量池中对应对象的值。这是对于基本类型局部对象的解决方案。

而对于引用类型,或者更确切地说对于运行时才能确定其值的局部对象,解决方案又是如何?那就要重点关注字节码信息中的数据成员和构造器那部分。

1
2
3
4
5
6
7
private final int val$parameter;

private final java.lang.Integer val$localVarB;

com.gtr.Outer$1(com.gtr.Outer, int, java.lang.Integer);
7: putfield #18 // Field val$parameter:I
12: putfield #20 // Field val$

我们可以很清楚地看到:

  • 除了编译器给匿名内部类额外添加的外部类引用成员this$0外,还多了额外增添了2个被final修饰的私有成员val$parameterval$localVarB,在$号之后,这2个成员的名字刚好和匿名内部类所使用的其中2个局部对象的名字一样。
  • 同时,匿名内部类的构造器也发生了改变。除了之前就提及过的指向外部类对象的引用这一参数,这里还额外添加了2个参数,一个是int类型的,另一个是Integer类型的。
  • 在匿名内部类构造器的内部,也对val$parameterval$localVarB进行赋值初始化。

从上面这个细节我们就可以总结出一个规律:局部内部类访问外部定义的局部对象时,所访问的局部对象中有多少个的值是运行时才能确定的,那编译器就会给局部内部类添加多少个局部对象复制品成员。

所以,只要局部对象的值是运行时确定的,有1个就给内部类增加1个成员,有10个就增加10个,有100个就增加100个。并通过构造器传递外部的值对它们进行初始化。而成员也被final修饰是保证数据的统一性,使得最终的结果保持之前一直在强调的那个语义:“局部内部类访问外部定义的局部对象”。

或许这时你会觉得复制这种对策并没有运用在基本类型的局部对象中。其实不然,使用final后,其实就已经算是把复制对策运用进去了。因为使用fianl后局部对象的值就在常量池中进行备份了。

以上的一步一步的推断都是十分重要的,或许一大段看下来会消化不了,不过慢慢看,自己也慢慢跟着这些步骤进行思考,也会渐渐明白其中的道理。

最后,想必大家都清楚为何要使用final修饰局部内部类所访问的局部对象了吧。
使用final就是在使用复制策略的情况下让局部内部类访问局部对象的复制品与局部内部类访问真正的局部对象2种不同行为的语义效果保持一致

内部类的其他特性

定义在接口中的内部类

静态内部类可以作为接口的一部分,接口中的任何类都自动被publicstatic修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface ClassInterface {
void howdy();

class Test implements ClassInterface {
public void howdy() {
System.out.println("Howdy!");
}

public static void main(String[] args) {
new Test().howdy();
}
}
}

如果你想要创建某些公共的代码,使得它们可以被某个接口的所有不同实现所共用,那么在接口内定义内部类是最有效的。

从多层嵌套内部类中访问外部类的成员

不管内部类嵌套多少层,它都能无条件地访问所有它所嵌入的外围类的所有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MNA {
private void f() { }

class A {
private void g() { }

public class B {
void h() {
g();
f();
}
}
}
}

public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}

内部类的继承

我们都知道创建内部类对象时,就必须将内部类中那个指向外部类对象的引用成员进行初始化,而在内部类的子类用不再存在默认给定初始化的外部类对象。此刻,我们必须使用特殊的语法来说清楚外部类与内部类的子类之间的关联。

这个特殊语法在子类的构造器中使用:enclosingClassReference.super();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WithInner {
class Inner { }
}

public class InheritInner extends WithInner.Inner {
// won't compile
//! InheritInner() { }

InheritInner(WithInner wi) {
wi.super();
}

@SuppressWarnings("unused")
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}

我们可以看到内部类子类InheritInner的构造器必须有一个指向外部类对象的引用参数,并且使用特殊语法在内部类中用这个引用调用父类(即内部类)的构造器;这两个步骤缺一不可,少一个步骤编译器都会报错,而Eclipse就会报错:
No enclosing instance of type WithInner is available due to some intermediate constructor invocation.

内部类可以被覆盖吗?

内部内能像方法一样被覆盖么?

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
class Egg {
@SuppressWarnings("unused")
private Yolk y;

protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}

public Egg() {
System.out.println("new Egg()");
y = new Yolk();
}
}

public class BigEgg extends Egg {
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}

public static void main(String[] args) {
new BigEgg();
}
}

当继承某个外部类并“覆盖”其中的内部类时,并没有什么特别的变化,两个内部类Yolk是完全独立的两个实体,各自在自己的命名空间内。

为什么需要内部类

这个问题或许在平日自己写代码中也会感悟到一些:使代码更加简洁紧凑,加快开发效率,代码更便于后期维护什么的。但这些都并不是重点。

逻辑组织

使用内部类可以将一些逻辑相关的类组织在一起,并控制位于内部的类的可视性

这里是一些之前就有过的例子以及Java源代码中出现过使用内部类的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Sequence {
//...
private class SequenceSelector implements Selector {
private int i = 0;

public boolean end() {
return i == items.length;
}

public Object current() {
return items[i];
}

public void next() {
if (i < items.length) {
i++;
}
}
}
//..
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//...

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

//...
}

多重继承解决方案

Java虽然只支持单继承,但它也用接口和内部类这种折中的形式实现了多重继承的效果。

每个内部类都能独立地继承自一个接口的实现,所以无论外部类是否已经继承了某个接口的实现,对于内部类都没有影响。内部类使得Java中多重继承的解决方案变得完整。

一个类要实现多个接口的情况,可以使用单一类实现多个接口,或者可以使用内部类实现那些接口。

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
interface A { }
interface B { }

class X implements A, B { }

class Y implements A {
B makeB() {
// anonymous inner class
return new B() { };
}
}

public class MultiInterfaces {
static void takesA(A a) { }
static void takesB(B b) { }

public static void main(String[] args) {
X x = new X();
Y y = new Y();
takesA(x);
takesA(y);
takesB(x);
takesB(y.makeB());
}
}

如果一个类要继承多个具体类或抽象类,而不是实现接口,那就只能使用内部类才能实现多重继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class D { }

abstract class E { }

class Z extends D {
E makeE() { return new E() { }; }
}

public class MultiImplementation {
static void takesD(D d) { }
static void takesE(E d) { }

public static void main(String[] args) {
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}

实现回调和闭包

使用内部类+接口这种形式实现闭包(closure)回调(callback)更灵活、更安全。

闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。而通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。使用内部类提供闭包和回调功能比使用指针更灵活、更安全。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
interface Incrementable {
void increment();
}

// very simple to just implement the interface
class Callee1 implements Incrementable {
private int i = 0;

public void increment() {
i++;
System.out.println(i);
}
}

class MyIncrement {
public void increment() {
System.out.println("Other operation");
}

static void f(MyIncrement mi) { mi.increment(); }
}


// if your class must implement increment() in
// some other way. you must use an inner class
class Callee2 extends MyIncrement {
private int i = 0;

public void increment() {
super.increment();
i++;
System.out.println(i);
}

private class Closure implements Incrementable {
public void increment() {
// specify outer-class method,
// otherwise you'd get an infinite recursion
Callee2.this.increment();
}
}

Incrementable getCallbackReference() {
return new Closure();
}
}

class Caller {
private Incrementable callbackReference;

Caller(Incrementable cbh) { callbackReference = cbh; }

void go() { callbackReference.increment(); }
}

public class Callbacks {
public static void main(String[] args) {
Callee1 c1 = new Callee1();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);

Caller caller1 = new Caller(c1);
Caller caller2 = new Caller(c2.getCallbackReference());
caller1.go();
caller1.go();
caller2.go();
caller2.go();
}
}

内部类Closure实现了Incrementable接口,以提供一个返回Callee2的“钩子”(hook)——而且是一个安全的钩子。无论谁获得此Incrementable的引用,都只能调用increment方法,除此之外没有其他功能。

这段代码是《Thinking In Java》上闭包与回调的演示。

事件驱动编程

控制框架(control framework)是一类特殊的应用程序框架(application framework),它用来解决响应事件的需求,主要用来响应事件的系统被称作事件驱动系统**

这里给出的也是那本书里的源代码。这里所要描述的是基于时间触发的事件驱动系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// the common methods for any control event

public abstract class Event {
private long eventTime;
protected final long delayTime;

public Event(long delayTime) {
this.delayTime = delayTime;
start();
}

public void start() {
eventTime = System.nanoTime() + delayTime;
}

public boolean ready() {
return System.nanoTime() >= eventTime;
}

public abstract void action();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//the reusable framework for control systems

public class Controller {
// a class from java.util to hold Event objects
private List<Event> eventList = new ArrayList<Event>();

public void addEvent(Event e) { eventList.add(e); }

public void run() {
while (eventList.size() > 0) {
// make a copy so you're not modifying the list
// while you're selecting the elements in it
for (Event e : new ArrayList<Event>(eventList)) {
if (e.ready()) {
System.out.println(e);
e.action();
eventList.remove(e);
}
}
}
}
}
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// this produces a specific application of the control system, all in a single class.
// Inner classes allow you to encapsulate different fuctionality for each type of event.

public class GreenHouseControls extends Controller {
@SuppressWarnings("unused")
private boolean light = false;

public class LightOn extends Event {
public LightOn(long delayTime) { super(delayTime); }

@Override
public void action() {
// put hardware control code here to
// physically turn on the light
light = true;
}

public String toString() { return "Light is on"; }
}

public class LightOff extends Event {
public LightOff(long delayTime) { super(delayTime); }

@Override
public void action() {
// put hardware control code here to
// physically turn off the light
light = false;
}

public String toString() { return "Light is off"; }
}

@SuppressWarnings("unused")
private boolean water = false;

public class WaterOn extends Event {
public WaterOn(long delayTime) { super(delayTime); }

@Override
public void action() {
// put hardware control code here
water = true;
}

public String toString() { return "GreenHouse water is on"; }
}

public class WaterOff extends Event {
public WaterOff(long delayTime) { super(delayTime); }

@Override
public void action() {
// put hardware control code here
water = false;
}

public String toString() { return "GreenHouse water is off"; }
}

@SuppressWarnings("unused")
private String thermostat = "Day";

public class ThermostatNight extends Event {
public ThermostatNight(long delayTime) {
super(delayTime);
}

@Override
public void action() {
// put hardware control code here
thermostat = "Night";
}

public String toString() {
return "Thermostat on night setting";
}
}

public class ThermostatDay extends Event {
public ThermostatDay(long delayTime) {
super(delayTime);
}

@Override
public void action() {
// put hardware control code here
thermostat = "Day";
}

public String toString() {
return "Thermostat on day setting";
}
}

// an example of an action() that inserts a
// new one of itself into the event list
public class Bell extends Event {
public Bell(long delayTime) { super(delayTime); }

@Override
public void action() {
addEvent(new Bell(delayTime));
}

public String toString() { return "Bing!"; }
}

public class Restart extends Event {
private Event[] eventList;

public Restart(long delayTime, Event[] eventList) {
super(delayTime);
this.eventList = eventList;
for (Event e : eventList) {
addEvent(e);
}
}

@Override
public void action() {
for (Event e : eventList) {
e.start(); // return each event
addEvent(e);
}
start(); // return this Event
addEvent(this);
}

public String toString() {
return "restarting system";
}
}

public static class Terminate extends Event {
public Terminate(long delayTime) { super(delayTime); }

@Override
public void action() { System.exit(0); }

public String toString() { return "Terminating"; }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GreenHouseController {
public static void main(String[] args) {
GreenHouseControls gc = new GreenHouseControls();
// instead of hard-wiring, you could parse
// configuration information from a text file here
gc.addEvent(gc.new Bell(900));
Event[] eventList = {
gc.new ThermostatNight(0),
gc.new LightOn(200),
gc.new LightOff(400),
gc.new WaterOn(600),
gc.new WaterOff(800),
gc.new ThermostatDay(1400)
};
gc.addEvent(gc.new Restart(2000, eventList));
gc.addEvent(new GreenHouseControls.Terminate(3000));

gc.run();
}
}