概述

工作过程中很少使用内部类,不过倒是经常在一些代码里面又看到内部类的身影,因此特意抽时间学习了一下,顺便总结一下方便日后查看。 内部类分为以下几种:

  • 成员内部类
    • 静态成员内部类
    • 非静态成员内部类
  • 匿名内部类
  • 局部内部类

现就以上内部类分别进行探讨,值得一提的是,我们在谈到这几种类的时候,需要时时刻刻在大脑中想象以下这几个类以及其对应的对象在内存中的布局,这样再理解起来 就很简单了,不然总靠死记是没有办法很好的应用的。

成员内部类

首先强调一下,既然是成员内部类,那么其特点就是成员化,也即是内部类是作为一个外部类的成员进行看待的,既然是作为成员看待的,那么 可以修饰成员的修饰符就可以拿来修饰这种内部类。而对于静态和非静态的理解则将其同变量进行比较就可以知道,静态的那必然是属于类的, 而非静态的则是属于对象的

非静态成员内部类

我们首先来看一下非静态内部类的定义,然后演示以下其用法,并探讨一些特性。非静态内部类的形式如下:

1
2
3
4
5
6
class Outter {

class Inner {

}
}

其在内存中的布局如下: 如上,由于是非静态的内部类,因此该内部类是只存在于外部对象上(并非类上)的,这种特性会导致内部类只有在外部类被实例化之后才会被加载到内存中(图中实线所示), 也因此我们不能在非静态内部类定义静态变量,因为静态变量本身就是类级别的,要求类在虚拟机启动的时候就应该加载到永久带的内存中, 而非静态内部类在外部类没有生成对象的时候是不存在的,所以内部类的字段和方法都不能是静态的。 下面我们来演示一下具体的用法:

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
public class TestOutter {

public static void main(String[] args) {
Outter.Inner inner = new Outter(1).new Inner(2);
inner.test();
}
}

class Outter {

static int i;

public Outter(int i) {
this.i = i;
System.out.println("this is outter cons, i = " + i);
}

public void test() {
System.out.println(i);
}
class Inner {
int x;

public Inner(int x) {
this.x = x;
System.out.println("this is inner cons, x = " + x);
}

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

我们可以看到内部类可以正常的访问外部类的静态的或者非静态的变量,不过需要注意的是,对于两个不同的外部对象, 由于其外部对象不同,因此每一个外部对象所持有的内部对象也正如上图中的虚线所示,我们可以通过反编译相关的字节码来验证这种猜想: 上面的类编译后会生成如下文件: 从上面的文件我们看到外部类以及对应的内部类在编译完之后会分别存放到不同的文件,我们查看一下内部类反编译后的文件,如下:

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
Compiled from "TestOutter.java"
class com.h3c.Outter$Inner {
int x;

final com.h3c.Outter this$0;

public com.h3c.Outter$Inner(com.h3c.Outter, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/h3c/Outter;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: aload_0
10: iload_2
11: putfield #3 // Field x:I
14: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
17: new #5 // class java/lang/StringBuilder
20: dup
21: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
24: ldc #7 // String this is inner cons, x =
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: iload_2
30: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
33: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: return

public void test();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #12 // Field com/h3c/Outter.i:I
6: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
9: return
}

在上面反编译之后的字节码中我们可以看到多了一个成员变量:

com.h3c.Outter this$0;```,并且在内部类的构造函数中会传入一个**外部类对象的引用**
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
,也因此我们可以确定内部类在外部类在生成对应对象的时候要求传入外部类的引用。
使用非静态的内部类还需要注意的是序列化,由于序列化要求所有的成员都实现了序列化的接口(**除非是使用transient修饰的变量**),而从上面反编译后的字节码我们
看到编译器给内部类自动添加了一个外部类的成员变量,因此如果我们仅仅使内部类实现了Serializable接口的化,在序列化内部对象到文件的时候将得到一个无法序列化的错误,
这正是由于外部类没有实现序列化的接口,程序演示如下:

```java
public class TestOutter {

public static void main(String[] args) throws IOException {
Outter.Inner inner = new Outter(1).new Inner(2);
inner.test();
File file = new File("test");
new ObjectOutputStream(new FileOutputStream(file)).writeObject(inner);
}
}

class Outter {


static int i;

public Outter(int i) {
this.i = i;
System.out.println("this is outter cons, i = " + i);
}

public void test() {
System.out.println(i);
}
class Inner implements Serializable {
int x;

public Inner(int x) {
this.x = x;
System.out.println("this is inner cons, x = " + x);
}

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

对应的执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
this is outter cons, i = 1
this is inner cons, x = 2
1
Exception in thread "main" java.io.NotSerializableException: com.h3c.Outter
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.h3c.TestOutter.main(TestOutter.java:11)

上面我们也已经分析这种问题的原因了,那么想要解决这种问题也就很简单了,那就是将外部类也实现Serializable即可,代码不再演示,有兴趣的可以下来自己尝试。

静态成员内部类

静态成员内部类和普通的类并没有什么区别,既然和普通类没有什么区别,那么静态成员内部类对象的创建就不需要依赖外部对象, 当然这种内部类也就没有办法访问外部类的非静态变量或者方法,不过这种内部类也因此可以定义自己的静态成员或者变量。同时也因为这种内部类不再持有外部类的 对象,因此序列化也就不受外部类的影响了,如下简单演示一下: 代码如下:

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
public class TestOutter {

public static void main(String[] args) throws IOException {
Outter.Inner inner = new Outter.Inner(2);
inner.test();
File file = new File("test");
new ObjectOutputStream(new FileOutputStream(file)).writeObject(inner);
}
}

class Outter {


static int i;

public Outter(int i) {
this.i = i;
System.out.println("this is outter cons, i = " + i);
}

public void test() {
System.out.println(i);
}
static class Inner implements Serializable {
static int x;

public Inner(int x) {
x = x;
System.out.println("this is inner cons, x = " + x);
}

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

反编译内部类结果如下:

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
Compiled from "TestOutter.java"
class com.h3c.Outter$Inner implements java.io.Serializable {
static int x;

public com.h3c.Outter$Inner(int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: iload_1
5: istore_1
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #3 // class java/lang/StringBuilder
12: dup
13: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
16: ldc #5 // String this is inner cons, x =
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: iload_1
22: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
25: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
28: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: return

public void test();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #10 // Field com/h3c/Outter.i:I
6: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
9: return
}

局部内部类

在方法中定义的内部类叫做局部类,由于是在方法中定义的,因此其和方法内定义的普通变量并不会有太大的区别,所以这种内部类不能够使用private等修饰符来修饰 ,代码演示如下:

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
public class PartTest {

public static void main(String[] args) {
POutter pOutter = new POutter();
Object x = pOutter.test(12);
System.out.println(x);
}
}


class POutter {

int px;

public Object test(final int j) {
final int k = 0;

class PInner {
int x;

public PInner(int x) {
this.x = x;
System.out.println("pinner cons x= " + x);
System.out.println("poutter cons px= " + px);
}

public PInner say() {
System.out.println("hello pinner k = " + k);
return this;
}
}

PInner inner = new PInner(12);
inner.say();
return inner;
}
}

同样我们编译之后可以看到会存在以下几个文件: 我们再次使用javap将内部类反编译成字节码,如下:

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
Compiled from "PartTest.java"
class com.h3c.POutter$1PInner {
int x;

final com.h3c.POutter this$0;

public com.h3c.POutter$1PInner(com.h3c.POutter, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/h3c/POutter;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: aload_0
10: iload_2
11: putfield #3 // Field x:I
14: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
17: new #5 // class java/lang/StringBuilder
20: dup
21: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
24: ldc #7 // String pinner cons x=
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: iload_2
30: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
33: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
42: new #5 // class java/lang/StringBuilder
45: dup
46: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
49: ldc #12 // String poutter cons px=
51: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: aload_1
55: getfield #13 // Field com/h3c/POutter.px:I
58: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
61: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
64: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
67: return

public com.h3c.POutter$1PInner say();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14 // String hello pinner k = 0
5: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: areturn
}

我们再次看到,对于局部内部类其成员变量新增了外部类对应的对象,因此可以知道序列化肯定是要求外部类也实现序列化,验证结果也正是如此,结果就不展示了,将上面代码贴进去执行就可以看到了。 这里再次强调以下,局部内部类是和局部变量同一个级别的,因此局部变量可以执行的操作,局部内部类也可以执行,如:

  • 静态方法内部的局部类可以访问外部类的静态变量,但是不可以访问对象上的字段
  • 非静态方法北部的局部类可以访问外部类的静态、非静态变量

匿名内部类

匿名内部类简单的说就是没有名字的内部类。我们知道类的构造函数要求和类的名称相同,因此如果连类的名称都没有的话, 那么肯定也就没有办法定义构造函数了,确实是这样子的,如下演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AclassTest {

public static void main(String[] args) {
Aface aface = new Aface() {

};
aface.test();
}
}

abstract class Aface {
int i = 0;
void test() {
System.out.println(i);
}
}

编译完之后生成的类如下: 我们反编译AclassTest后,对应的字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Compiled from "AclassTest.java"
public class com.h3c.AclassTest {
public com.h3c.AclassTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class com/h3c/AclassTest$1
3: dup
4: invokespecial #3 // Method com/h3c/AclassTest$1."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method com/h3c/Aface.test:()V
12: return
}

可以看到在main方法中初始化匿名内部类的时候调用的是抽象类的构造函数,如下:

1
2
3
4
5
6
7
8
Compiled from "AclassTest.java"
final class com.h3c.AclassTest$1 extends com.h3c.Aface {
com.h3c.AclassTest$1();
Code:
0: aload_0
1: invokespecial #1 // Method com/h3c/Aface."<init>":()V
4: return
}

对于匿名内部类的使用场景一般是只使用一次,而且类比较小。除了不可以给匿名内部类定义构造函数外,其也不可以定义静态的字段或者方法,这大概都是 由于静态字段和方法都需要一个明确的类名的原因吧。

内部接口

除了内部类之外,比较常见的还有内部接口,用法我们一并总结过一下:

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

interface Inner {
void hell();
}
}

class InnerTest implements TestInterface.Inner {

@Override
public void hell() {
System.out.println("hello world");
}

public static void main(String[] args) {
InnerTest innerTest = new InnerTest();
innerTest.hell();
}
}

由于接口是没有构造函数,因此也就不能被实例化,所以内部接口只有在静态的时候才是有意义的,不论接口是否声明静态或者非静态。

其他

关于内部类还有一些小的特性,比如,内部类不可以与外部类重名,这个是规范。另外当内部类方法和外部类有重叠的时候也会有一些需要注意的 地方,下面我们就通过一个例子来进行说明:

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
public class Hello {

// 内部类不可以和外部类重名
// public class Hello {
//
// }


class Inner {

public String say() {
return Hello.this.toString();
}

public String say1() {
return this.toString();
}

@Override
public String toString() {
return "this is inner class";
}
}

@Override
public String toString() {
return "this is outter class";
}

public static void main(String[] args) {
Hello hello = new Hello();
Inner inner = hello.new Inner();
System.out.println(inner.say());
System.out.println(inner.say1());
}
}

在上面的代码中,我们可以看到访问非静态内部类的方法的时候直接this就可以了,不过访问外部类的时候要加上类名,这个 可以通过反编译之后来查看更详细的信息的,具体的不再过多的解释了。

总结

关于内部类的总结暂时到此可以告一段落了,后续如果发现不对的或者有趣的用法会持续更新上去。 参考文档: