1. 为什么要使用内部类
内部类就是定义在一个类内部的类,那么为什么要使用内部类呢?主要原因有以下几点:第一,内部类中定义的方法能访问到它所在外部类的私有属性及方法;第二,外部类无法实现对同一包中的其他类隐藏,而内部类可以做到这一点;第三,匿名内部类在我们只需使用该类的实例依次时可以有效减少我们的代码量。关于以上三点,我们在下文中会举出具体例子进行进一步的说明。
2. 如何使用内部类
(1)使用内部类访问外围类私有变量
在内部类中,我们能够访问到它所在外部类中的私有实例变量及方法,请看以下代码:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { 4 System.out.println("In Outer class"); 5 Inner inner = new Inner(); 6 inner.innerMethod(); 7 } 8 public static void main(String[] args) { 9 Outer outer = new Outer(); 10 outer.outerMethod(); 11 } 12 13 private class Inner { 14 public void innerMethod() { 15 System.out.println("The var own in Outer is " + own); 16 } 17 } 18 }
这段代码的输出如下所示:
我们可以看到,在内部类中确实访问到了外部类Outer的private变量own。那么,这是如何做到的呢?实际上,内部类对象隐式地持有一个外部类对象的引用,我们假设这个引用名为outer,那么实际上内部类的innerMethod方法是这样子的:
1 public void innerMethod() { 2 System.out.println("The var own in Outer is " + <strong>outer</strong>.own); 3 }
编译器会修改Inner类的构造器,添加一个外部类Outer的引用作为参数,大概是这个样子:
1 public Inner(Outer outer) { 2 this.outer = outer; 3 }
所以我们在Outer类的outerMethod方法中调用Inner构造器那条语句实际上会被编译器“改成“这个样子:
1 Inner inner = new Inner(this);
我们来通过javap看下生成的字节码,来直观地感受下:
我们重点看一下这一行:
我们可以看到,调用Inner类的构造方法时,确实传入了类型为Outer的参数(即外围类的引用)。
我们还可以看到,编译器为这个类生成了一个名为access$100的静态方法,在这个方法中,加载并获取了own变量。实际上,内部类就会调用这个方法来获取外部类的私有实例变量own。
我们再来看下编译器为内部类生成的字节码:
我们来看一下标号16和19的行,确实是现获取外围类引用,然后调用了access$100方法,并传入了外围类引用作为参数,从而在内部类中能够访问外围类中的private变量。
(2)内部类的特殊语法规则
实际上,使用外围类引用的正规语法规则如下所示:
1 OuterClass.this
例如,以上Inner类的innerMethod方法我们使用正规语法应该这么写:
public void innerMethod() { System.out.println("The var own in Outer is " + Outer.this.own); }
另一方面,我么也可以采用以下语法更加明确地初始化内部类:
Inner inner = this.new Inner();
我们还可以显示的将内部类持有的外围类引用指向其它的外围类对象,假设outerObject是一个Outer类实例,我们可以这样写:
Outer.Inner inner = outerObject.new Inner();
这样一来,inner所持有的外围类对象引用即为outerObject。
在外围类的作用域之外,我们还可以像下面这样引用它的内部类:
OuterClass.InnerClass
(3)局部内部类
具备内部类即定义在一个方法内部的类,如以下代码所示:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { 4 class Inner { 5 public void innerMethod() { 6 System.out.println("The var own in Outer is " + own); 7 } 8 } 9 System.out.println("In Outer class"); 10 Inner inner = new Inner(); 11 inner.innerMethod(); 12 } 13 public static void main(String[] args) { 14 Outer outer = new Outer(); 15 outer.outerMethod(); 16 } 17 }
局部类的作用域就被限制在定义它的方法的方法体中,因此它不能用public或private访问修饰符来修饰。
与常规内部类比较,局部类具有一个优势:可以访问局部变量。但是这有一个限制,就是它访问的局部变量必须被声明为final。简单地说,这是出于一致性的考虑。因为局部变量的生命周期随着方法的运行结束也随之结束了,而局部类的生命周期却不会随着方法的结束而结束。在方法运行完后,局部类为了能够继续访问局部变量,需要对局部变量进行备份。
实际上,在创建局部类的对象时,编译器会隐式修改具备类的构造器,并将局部类要访问的“外部变量”作为参数传递给它,这样具备类可以在其内部创建一个拷贝并存储在自己的实例域中。设想若这个变量不是final的,即我们可以在具备类对它进行修改,这无疑会破坏数据的一致性(局部变量与其在局部类内部的拷贝版本不一样)。所以想让局部类访问的变量必须加上final修饰符。
(4)匿名内部类
对于只需要实例化一次的类,我们可以不给它命名,而是通过匿名内部类的形式来使用。匿名内部类的语法形式如下:
new SuperType(construction parameters) { inner class methods and data }
匿名类不能有构造器,因此将构造器参数传给超类的构造器(SuperType)。匿名类内部可以定义一些方法与属性。
还有一种形式的匿名内部类是实现了某种接口,它的语法格式如下:
new Interface() { methods and data }
注意,以上代码的含义并不是实例化一个接口,而是实例化实现了某种接口的匿名内部类。
我们上面提到的传递给Time的构造器的参数之一是一个实现了ActionListener接口的类对象,显然那个类只需要实例化一次,因此我们可以用匿名内部类来实例化:
... ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { ... } };
(5)静态内部类
有时候,我们不想让一个内部类持有外围类对象的引用,这是我们可以选择使用静态内部类。静态内部类不会持有外围类的引用,而非静态的内部类都会持有外围类对象的引用(隐式持有),而这也是导致内存泄露(Memory Leak)的一个常见原因之一。
请看以下代码:
1 public class Outer { 2 private int own = 1; 3 public void outerMethod() { } 4 public static void main(String[] args) { } 5 6 private class Inner { 7 public void innerMethod() { } 8 } 9 }
现在内部类Inner是非静态的,我们用javap查看下编译器生成的相应class文件:
可以看到,Inner类内部持有一个Outer类的引用。
现在我们给Inner类加上static修饰符,让它变为一个静态内部类,再来看一下:
可以看到,现在内部类不再持有外围类Outer的引用了。