简介
可以将一个类的定义放在另一个类的定义内部,这就是内部类。内部类允许你把一些逻辑相关的类组织在一起,并控制内部类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。
那为什么需要内部类呢?
一般来说,内部类继承自某个类或实现某个接口,内部类的代码可以操作创建它的外围类的独对象。所以可以认为内部类提供了某种进入其外围类的窗口。
那如果只需要一个对接口的引用,为什么不通过外围类来实现接口呢?确实是是这样,如果外围类能实现该接口,就应该用外围类来实现,但是外围类并不总能享用到接口带来的方便,有时需要用到接口的实现,所以,内部类最吸引人的原因是:
每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。内部类使得多重继承的解决方案变得完整,接口解决了部分问题,而内部类有效地实现了“多重继承”。但是如果我们不需要解决多重继承问题,那么我们自然可以使用其他的编码方式,但是使用内部类还能够为我们带来如下特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
- 创建内部类对象的时刻并不依赖于外围类对象的创建。
- 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体。
在Java中,内部类主要分为四种:静态内部类、成员内部类、匿名内部类和局部内部类,如下代码所示:
abstract class Father {
abstract void f();
}
public class OuterClass {
// 静态内部类
static class innerClass1{}
// 成员内部类
class innerClass2{}
public void f1() {
// 局部内部类
class innerClass3{}
// 匿名内部类
f2(new Father() {
@Override
void f() {
System.out.println("hello world");
}
});
}
public void f2(Father father) {
father.f();
}
}
静态内部类和成员内部类
静态内部类是指被声明为static的内部类,它可以不依赖于外部类实例而被实例化。它可以访问外围类的所有成员,包括那些私有成员。静态内部类与其他的静态成员一样,也遵守同样的可访问性规则,即它不能访问任何外围类的非static成员变量和方法。如果它被声明为私有的,那它就只能在外围类的内部才可以被访问。
静态内部类的一种常见用法就是作为共有的辅助类,仅当与它的外部类一起使用时才有意义。
静态内部类如果去掉“static”关键字,就成为成员内部类,成员内部类为非静态内部类,它可以访问外围类的所有成员,无论是静态的还是非静态的。当我们在创建一个内部类的时候,它无形中就与外围类有了一种联系,依赖于这种联系,它可以无限制地访问外围类的元素。
成员内部类不可以定义静态的属性和方法,只有在外围类被实例化后,这个内部类才可以被实例化,我们看下面一段代码:
public class OuterClass {
private static int num1 = 1;
private int num2 = 1;
public void f() {
System.out.println("OuterClass.f()");
}
public Inner1 getInner1() {
return new Inner1();
}
public class Inner1 {
public static final int a = 1;
public int b = 1;
// 不可以定义静态属性和方法
// public static int c = 1;
// public static void f() {}
public OuterClass getOuter() {
// 访问外围类属性
System.out.println(num1);
// 访问外围类方法
f();
return OuterClass.this;
}
}
public static class Inner2 {
public void f() {
System.out.println(num1);
// 不可以访问外围类非静态属性
// System.out.println(num2);
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
System.out.println(outerClass);
OuterClass.Inner1 inner1 = outerClass.getInner1();
OuterClass.Inner1 inner2 = outerClass.new Inner1();
System.out.println(inner1.getOuter());
System.out.println(inner2.getOuter());
OuterClass.Inner2 inner3 = new Inner2();
inner3.f();
}
}
运行结果:
innerclass.OuterClass@7852e922
1
OuterClass.f()
innerclass.OuterClass@7852e922
1
OuterClass.f()
innerclass.OuterClass@7852e922
1
上述代码演示了我们前面提到的注意事项。需要注意的是,由于成员内部类与外围类存在一种联系,创建成员内部类实例时,必须要通过已创建的外围类实例来创建,即类似于outclass.new InnerClass(),同时,通过成员内部类实例可以获取与其关联的外围类实例,需要通过OutClass.this的方式来获取。
那成员内部类与外围类存在的联系到底是什么呢?我们通过反编译软件来看一下:
我们将OuterClass$Inner1.class反编译如下:
public class innerclass/OuterClass$Inner1 {
<ClassVersion=52>
<SourceFile=OuterClass.java>
public static final int a = 1 (java.lang.Integer);
public int b;
synthetic final innerclass.OuterClass this$0;
public OuterClass$Inner1(innerclass.OuterClass arg0) { // <init> //(Linnerclass/OuterClass;)V
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner1;, sig=null, start=L1, end=L2>
L1 {
aload0 // reference to self
aload1
putfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
aload0 // reference to self
invokespecial java/lang/Object.<init>()V
}
L3 {
aload0 // reference to self
iconst_1
putfield innerclass/OuterClass$Inner1.b:int
return
}
L2 {
}
}
public getOuter() { //()Linnerclass/OuterClass;
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner1;, sig=null, start=L1, end=L2>
L1 {
getstatic java/lang/System.out:java.io.PrintStream
invokestatic innerclass/OuterClass.access$0()I
invokevirtual java/io/PrintStream.println(I)V
}
L3 {
aload0 // reference to self
getfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
invokevirtual innerclass/OuterClass.f()V
}
L4 {
aload0 // reference to self
getfield innerclass/OuterClass$Inner1.this$0:innerclass.OuterClass
areturn
}
L2 {
}
}
}
从上述代码中可以看到,成员内部类Inner1中,存在一个OuterClass类的变量this$0,该变量被synthetic所修饰,这个修饰符表示由java编译器生成的(除了像默认构造函数这一类的)方法,或者类等信息,具体介绍可以看这一篇博客。在Inner1的默认构造方法中存在一个外围类参数arg0,这就是创建内部类的外围类对象了,该对象会被赋值给this$0,也就是说,成员内部类存在一个有java编译器生成的外围类的对象。那静态内部类是不是就没有了呢,我们看一下OuterClass$Inner2.class的反编译代码:
public class innerclass/OuterClass$Inner2 {
<ClassVersion=52>
<SourceFile=OuterClass.java>
public OuterClass$Inner2() { // <init> //()V
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner2;, sig=null, start=L1, end=L2>
L1 {
aload0 // reference to self
invokespecial java/lang/Object.<init>()V
return
}
L2 {
}
}
public f() { //()V
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$Inner2;, sig=null, start=L1, end=L2>
L1 {
getstatic java/lang/System.out:java.io.PrintStream
invokestatic innerclass/OuterClass.access$0()I
invokevirtual java/io/PrintStream.println(I)V
}
L3 {
return
}
L2 {
}
}
}
静态内部类中确实是没有的,这也就说明了为什么创建成员内部类必须要先创建外围类实例了,因为外围类实例创建成员内部类时,需要将自己传给内名内部类的构造方法中,而静态内部类就不需要了。
那什么时候用静态内部类,什么时候用成员内部类呢?
建议如果声明成员类不要求访问外部实例,就要始终把static修饰符放在它的声明中,使它成为静态内部类。
匿名内部类
匿名内部类没有名字,它不是外围类的一个成员,它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名内部类可以出现在代码中任何允许存在表达式的地方。
匿名内部类不使用class、extends、implements等关键字,没有构造方法,它必须继承其他类或者实现其他接口。匿名内部类的好处就是代码更加紧凑简洁,但会带来易读性下降的问题。它一般用于GUI编程中实现事件处理等。在使用匿名内部类时,要注意以下原则:
- 匿名内部类没有构造方法。
- 匿名内部类是没有访问修饰符的。
- 匿名内部类不能定义静态成员、方法和类。
- 只能创建匿名内部类的一个实例。
- 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是同时只能继承一个类或者实现一个接口。
- 匿名内部类访问外部定义的对象需要是final的。
匿名内部类没有构造方法,那我们怎么初始化变量呢?答案是用构造代码块啦,如下所示:
abstract class MyClass{
abstract void f();
}
public class OuterClass {
public int a = 1;
public MyClass getMyClass(final int num) {
final OuterClass outerClass = new OuterClass();
System.out.println(outerClass);
return new MyClass() {
int myNum;
{
myNum = num;
}
@Override
void f() {
System.out.println(myNum);
System.out.println(outerClass);
System.out.println(outerClass.a);
}
};
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
MyClass myClass = outerClass.getMyClass(10);
myClass.f();
}
}
myNum通过构造代码块进行了初始化,其中,匿名内部类访问了外部变量num和outerClass,这两个变量都声明为了final,上述代码是在JDK1.7中运行的,必须要final参数,否则无法编译,但是在JDK1.8中,可以不声明为final,具体可以看在线文档:
这是JDK1.8新增的Effectively final功能,虽然我们不必再添加final修饰符,但我们在代码中也是不能修改外部变量的,如下所示:
那为什么外部变量要声明为final呢?我们先来看一下生成的字节码文件,除了MyClass.class和OuterClass.class两个文件,还有一个OuterClass$1.class文件,这就是生成的匿名内部类了:
反编译结果如下:
class innerclass/OuterClass$1 extends innerclass/MyClass {
<ClassVersion=52>
<SourceFile=OuterClass.java>
int myNum;
synthetic final innerclass.OuterClass this$0;
private synthetic final innerclass.OuterClass val$outerClass;
OuterClass$1(innerclass.OuterClass arg0, int arg1, innerclass.OuterClass arg2) { // <init> //(Linnerclass/OuterClass;ILinnerclass/OuterClass;)V
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$1;, sig=null, start=L1, end=L2>
L1 {
aload0 // reference to self
aload1 // reference to arg0
putfield innerclass/OuterClass$1.this$0:innerclass.OuterClass
aload0 // reference to self
aload3
putfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
}
L3 {
aload0 // reference to self
invokespecial innerclass/MyClass.<init>()V
}
L4 {
aload0 // reference to self
iload2 // reference to arg1
putfield innerclass/OuterClass$1.myNum:int
return
}
L2 {
}
}
f() { //()V
<localVar:index=0 , name=this , desc=Linnerclass/OuterClass$1;, sig=null, start=L1, end=L2>
L1 {
getstatic java/lang/System.out:java.io.PrintStream
aload0 // reference to self
getfield innerclass/OuterClass$1.myNum:int
invokevirtual java/io/PrintStream.println(I)V
}
L3 {
getstatic java/lang/System.out:java.io.PrintStream
aload0 // reference to self
getfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
invokevirtual java/io/PrintStream.println(Ljava/lang/Object;)V
}
L4 {
getstatic java/lang/System.out:java.io.PrintStream
aload0 // reference to self
getfield innerclass/OuterClass$1.val$outerClass:innerclass.OuterClass
getfield innerclass/OuterClass.a:int
invokevirtual java/io/PrintStream.println(I)V
}
L5 {
return
}
L2 {
}
}
}
可以看到,该类有如下两个属性:
int myNum;
synthetic final innerclass.OuterClass this$0;
这说明匿名内部类将外部变量备份了一份,内部类中的属性和外部方法的参数两者实际不是同一个东西,所以他们两者是可以任意变化的,而然这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。
简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。同时,拷贝引用也解决了匿名内部类的生命周期可能比外部的类长的问题,比如在上述代码中,getMyClass(int)方法返回之后,栈帧中对应的outClass变量就会被销毁,即匿名内部类的生命周期可能比外部的类长,那匿名内部类访问外部局部变量有可能是访问不到的。通过拷贝引用也解决了上述问题。
局部内部类
局部内部类是定义在一个代码块中的类,它的作用范围为其所在的代码块,它就像局部变量一样,不能被public、protected、private及static修饰,局部内部类同样只能访问方法中定义为final的变量。若局部内部类定义在一个静态方法中,它就成为了局部静态内部类,局部静态内部类与静态内部类特性基本相同,局部内部类与成员内部类特性基本相同。
参考资料
Bruce Eckel:《Java编程思想》
Joshua Bloch:《Effective Java》