前言
在开发过程中不想创建类文件或者想把逻辑相关的类放在一起,就在类中创建类,这就是本节要讨论——内部类。
正题
在开始编写文章前,有几个问题需要思考一下:
- 什么是内部类?
- 内部类的作用
- 如何创建内部类对象
- 内部类的其他特性
1. 什么是内部类?
将一个类的定义放在另一个类的定义内部,这就是内部类。
2. 内部类的作用
- 内部类方法可以访问该类定义所在的外围类作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
3. 如何创建内部类对象
有时需要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,必须在 new 表达式中提供对其他外部类对象的引用,这是需要使用 .new 语法。
public class OutClass {
void f()
{
System.out.println("OutClass.f()");
}
public class Inner {
public OutClass outer()
{
return OutClass.this;
}
}
public static void main(String[] args) {
OutClass outClass = new OutClass();
outClass.new Inner();
}
}
.new 表达式对应的字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #5 // class back/mdj/com/test/OutClass
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: new #7 // class back/mdj/com/test/OutClass$Inner
11: dup
12: aload_1
13: dup
14: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class;
17: pop
18: invokespecial #9 // Method back/mdj/com/test/OutClass$Inner."<init>":(Lback/mdj/com/test/OutClass;)V
21: pop
22: return
}
Inner.class 对应的字节码:
{
final back.mdj.com.test.OutClass this$0;
descriptor: Lback/mdj/com/test/OutClass;
flags: ACC_FINAL, ACC_SYNTHETIC
}
字段二级制数据:10 10 00 05 00 06
在常量池里维护一个指向外部类的 final 对象 this$0,在内部类的构造函数里会把外围类对象引用赋值给该对象,从而使内部类维护外部类的一个引用。所以,在拥有外部类对象之前是不可能创建内部类对象的,这是因为内部类对象会暗暗的连接到创建它的外部类对象上。
一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
注意:当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密的捕获一个指向那个外围类对象的引用,此后在你访问外围类成员时,就是用那个引用来选择外围类的成员。必须要用外部类的对象来创建内部类对象。
4. 内部类的其他特性
4.1 多重继承
如果没有内部类提供的可以继承多个具体的或抽象的类的能力,一些设计编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口实现了部分问题,而内部类有效的实现了“多重继承”,也就是说,内部类允许继承多个非接口类型。
public class D {
}
public abstract class E {
}
public class F extends E{
public D makeD()
{
return new D();
}
}
public class G
{
static void takesD(D d){}
static void takesE(E e){}
public static void main(String args[]) {
F f = new F();
takesD(f.makeD());
takesE(f);
}
}
java语言单继承特性,以上场景通过内部类来实现多重继承。
4.2 在方法和作用域内的内部类
上面只是简单的在类中创建内部类,然而内部类的语法覆盖了大量其他难以理解的技术,例如,可以在一个方法或者任意作用域内定义内部类,这么做有两大理由:
- 实现了某类型的接口,于是可以创建并返回对其的引用。
- 你需要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共的。
public interface Destination {
String readLabel();
}
public class Parcel1 {
public Destination destination(String s)
{
class PDestination implements Destination
{
private String label;
private PDestination(String s)
{
label = s;
}
@Override
public String readLabel() {
return label;
}
}
return new PDestination(s);
}
public static void main(String args[]) {
Parcel1 parcel1 = new Parcel1();
parcel1.destination("child");
}
}
PDestination类时destination()方法的一部分,而不是Parcel1的一部分。所以在destination()方法之外不能访问PDestination。虽然 PDestination 构造方法是 private,但是 Java 虚拟机会自动转为包访问权限:
com.inner.Parcel1$1PDestination(com.inner.Parcel1, java.lang.String);
flags:
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/inner/Parcel1;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: aload_0
10: aload_2
11: putfield #3 // Field label:Ljava/lang/String;
14: return
4.3 内部类覆盖
public class Egg {
private York y;
protected class York {
public York() {
System.out.println("Egg.York");
}
}
public Egg() {
System.out.println("Egg");
y = new York();
}
}
public class BigEgg extends Egg {
public class York {
public York() {
System.out.println("BigEgg.York");
}
}
public static void main(String args[]) {
BigEgg bigEgg = new BigEgg();
}
}
运行结果:
Egg
Egg.York
从结果可以看出:当继承某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立地两个实体,各自在自己的命名空间内。
4.4 继承内部类
内部类的构造器必须连接到指向其外围类对象的引用上,所以在继承内部类的时候,事情会变得有点复杂。问题在于那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类不再存在可连接的默认对象,要解决问题,必须使用特殊的语法来明确说清它们之间的关联。
public class D {
public class A{}
}
public class C extends D.A{
C(D d) {
d.super();
}
}
可以看到 C 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用,此外必须在构造器内使用如下语法:enclosingClassReference.super();
4.5 接口内部类
正常情况下,不能在接口内放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动的是 public 和 static。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口。
public interface AInterface {
void A();
class B implements AInterface {
@Override
public void A() {
System.out.println("A()");
}
public static void main(String args[]) {
new B().A();
}
}
}
B.class 对应的字节码:
Constant pool:
#1 = Methodref #8.#19 // java/lang/Object."<init>":()V
#2 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #22 // A()
#4 = Methodref #23.#24 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #25 // back/mdj/com/test/AInterface$B
#6 = Methodref #5.#19 // back/mdj/com/test/AInterface$B."<init>":()V
#7 = Methodref #5.#28 // back/mdj/com/test/AInterface$B.A:()V
#8 = Class #29 // java/lang/Object
#9 = Class #30 // back/mdj/com/test/AInterface
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 A
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 SourceFile
#18 = Utf8 AInterface.java
#19 = NameAndType #10:#11 // "<init>":()V
#20 = Class #31 // java/lang/System
#21 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#22 = Utf8 A()
#23 = Class #34 // java/io/PrintStream
#24 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#25 = Utf8 back/mdj/com/test/AInterface$B
#26 = Utf8 B
#27 = Utf8 InnerClasses
#28 = NameAndType #14:#11 // A:()V
#29 = Utf8 java/lang/Object
#30 = Utf8 back/mdj/com/test/AInterface
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public back.mdj.com.test.AInterface$B();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
public void A();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String A()
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #5 // class back/mdj/com/test/AInterface$B
3: dup
4: invokespecial #6 // Method "<init>":()V
7: invokevirtual #7 // Method A:()V
10: return
LineNumberTable:
line 18: 0
line 19: 10
}
其中 InnerClasses 属性值:00 1B 00 00 00 0A 00 01 00 05 00 09 00 1A 00 09
从 InnerClasses 属性的访问标志位:00 09 可知该内部类是 public 和 static 的。从内部类的字节码可以获知:接口内部类没有指向一个对接口的引用。
4.6 匿名内部类
public interface Contents {
int value();
}
public class Parcel {
public Contents contents()
{
return new Contents() {
private int i = 8;
public int value() {
return i;
}
};
}
public static void main(String args[]) {
Parcel parcel = new Parcel();
parcel.contents();
}
}
使用 javac 命令编译Parcel文件,会自动生成 Parcel$1.class 文件,对应函数 contents() 创建的匿名内部类:
class Parcel$1 implements Contents {
private int i;
Parcel$1(Parcel var1) {
this.this$0 = var1;
this.i = 8;
}
public int value() {
return this.i;
}
}
在代码里创建内部类,但是在字节码层次上,每一个内部类都有一个对应的 class 文件。在某种程度上,内部类方便了开发(不需要新建一个继承某个接口的类),但是在字节码层次上,虚拟机替我们完成了创建一个新类的工作。
Contents.class 对应的字节码:
Constant pool:
#1 = Fieldref #4.#22 // com/inner/Parcel$1.this$0:Lcom/inner/Parcel;
#2 = Methodref #5.#23 // java/lang/Object."<init>":()V
#3 = Fieldref #4.#24 // com/inner/Parcel$1.i:I
#4 = Class #25 // com/inner/Parcel$1
#5 = Class #27 // java/lang/Object
#6 = Class #28 // com/inner/Contents
#7 = Utf8 i
#8 = Utf8 I
#9 = Utf8 this$0
#10 = Utf8 Lcom/inner/Parcel;
#11 = Utf8 <init>
#12 = Utf8 (Lcom/inner/Parcel;)V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 value
#16 = Utf8 ()I
#17 = Utf8 SourceFile
#18 = Utf8 Parcel.java
#19 = Utf8 EnclosingMethod
#20 = Class #29 // com/inner/Parcel
#21 = NameAndType #30:#31 // contents:()Lcom/inner/Contents;
#22 = NameAndType #9:#10 // this$0:Lcom/inner/Parcel;
#23 = NameAndType #11:#32 // "<init>":()V
#24 = NameAndType #7:#8 // i:I
#25 = Utf8 com/inner/Parcel$1
#26 = Utf8 InnerClasses
#27 = Utf8 java/lang/Object
#28 = Utf8 com/inner/Contents
#29 = Utf8 com/inner/Parcel
#30 = Utf8 contents
#31 = Utf8 ()Lcom/inner/Contents;
#32 = Utf8 ()V
{
final com.inner.Parcel this$0;
descriptor: Lcom/inner/Parcel;
flags: ACC_FINAL, ACC_SYNTHETIC
com.inner.Parcel$1(com.inner.Parcel);
descriptor: (Lcom/inner/Parcel;)V
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/inner/Parcel;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: aload_0
10: bipush 8
12: putfield #3 // Field i:I
15: return
LineNumberTable:
line 6: 0
line 7: 9
public int value();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #3 // Field i:I
4: ireturn
LineNumberTable:
line 10: 0
}
contents() 方法将返回值的生成与表示这个这个返回值的类的定义结合在一起,另外这个类时匿名的,它没有名字。更糟糕的是,看起来似乎是你正要创建一个 Contents 对象。这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。
在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此这与别的地方使用的分号是一致的。
下面看看内部类的一些特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与外围类对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口或继承同一个类。
- 创建内部类对象的时刻不依赖于外围类对象的创建。
- 内部类并没有令人迷惑的 "is-a" 关系,它就是一个独立地实体。
4.7 嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类,想要理解 static 应用于内部类时的含义,就必须记住普通的内部类对象隐藏的保存了一个引用,指向创建它的外围类对象上。然而,当内部类是 static 时,就不是这样了。
public class OuterClass {
public static class Inner {}
public static void main(String[] args) {
Inner nInner = new Inner();
}
}
但是如果你创建的是嵌套类(静态内部类),那么它就不需要外部类对象的引用。
嵌套类意味着:
- 要创建嵌套类对象并不需要创建外围类对象。
- 不能从嵌套类对象中访问非静态的外围类对象
嵌套类与普通内部类还有一个区别:普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类(内部类对象依附于外围类,在拥有外部类对象之前是不可能创建内部类对象,内部维护一个指向外围类对象的引用,而 static 数据和字段只属于类不属于对象,所以不能再内部类中包含 static 数据)。
就像你再本节前面看到的那样,在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外围类对象。嵌套类就没有这个特殊的 this 引用,这使得它类似于一个 static 方法。
当继承了某个外围类的时候,内部类并没有发生什么特别的神奇变化,这两个内部类是完全独立地个体,各自在自己的命名空间内。
参考文章: