内部类
内部类是定义在另一个类中的类,使用内部类有三个主要原因:
-
访问外围类的部分私有数据(取决于作用域)
-
对同一个包中的其他类隐藏起来
-
先看一个简单的例子:
public class TalkingClock { private int interval; private boolean beep; TalkingClock(int interval, boolean beep) {...} public void start() {...} // an inner class private class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println("At the tone, the time is " + new Date()); if (beep) { Toolkit.getDefaultToolkit().beep(); } } } ... }
只有内部类可以是私有类,常规类只可以具有包/公有可见性。注意,虽然 TimePrinter 类位于 TalkingClock 类的内部,但并不意味着每个 TalkingClock 都有一个 TimePrinter 实例域。
这个例子其实想说明一点:TimePrinter 类中没有名为 beep 的变量,取而代之的是 beep 引用了 TalkingClock 对象的域。将这个结论推广开来就是,内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
内部类为什么可以这么做?实际上,内部类的对象有一个隐式引用(不可见),由编译器负责在内部类的默认构造器中设置这个引用,让它指向外部类对象:
private TimePrinter(TalkingClock clock) { outer = clock; // outer 不是关键字,只是为了方便说明 }
当在某个方法中创建 TimePrinter 对象时,编译器会将 this 引用传递给构造器,不需要我们自己添加:
public void aMethodOfTalkingClock() { // ... ActionListener listener = new TimePrinter(); // parameter automatically added : ActionListener listener = new TimePrinter(this); // ... }
二、语法
可以用表达式 OuterClass.this 表示外围类引用,用法如下:
public void actionPerformed(ActionEvent event) { ... if (TalkingClock.this.beep) { Toolkit.getDefaultToolkit().beep(); } }
对于公有的或包可见的内部类,可以用表达式 OuterClass.InnerClass 引用内部类,还可以通过外围类的引用在其他地方构建实例:
TalkingClock aTalkingClock = new TalkingClock(1000, true); TalkingClock.TimePrinter listener = aTalkingClock.new TimerPrinter();
内部类中声明的所有静态域都必须是 final,这是因为,对于每个外部对象,会分别有一个单独的内部类实例,将静态域设置为 final 可以确保这些域是唯一的。
三、通过 javap 命令理解内部类
必须注意,内部类是一种编译器现象,与虚拟机无关,编译器会把内部类翻译成用美元符号($)分隔外部类名与内部类名的常规类文件。在上面举的例子中,TalkingClock 类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$TimePrinter.class :
编译好了之后,可以用 javap 指令看看内部类的细节。javap 是 jdk 自带的反解析工具,作用是根据字节码文件反解析出当前类的信息,怎么用呢?先用 -help 选项获取帮助信息:
想要了解 TimePrinter 的内部细节,使用 -private 选项就可以了,结果如下:
可以看到,编译器为了引用外围类,生成了一个实例域 this$0,这个名字是自动合成的。
四、局部内部类
延续上面的例子,如果说 TimePrinter 这个类名字只在某个方法中创建其对象时使用了一次,那么就可以把 TimePrinter 类的定义放在这个方法中,成为一个局部内部类:
public void start() { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println("The time is " + new Date()); if (beep) { Toolkit.getDefaultToolkit().beep(); } } } ActionListener listener = new TimePrinter(); Timer timer = new Timer(interval, listener); timer.start(); }
public void start(int interval, boolean beep) { // beep 不再是 TalkingClock 类的字段 class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println("The time is " + new Date()); if (beep) { Toolkit.getDefaultToolkit().beep(); } } } ActionListener listener = new TimePrinter(); Timer timer = new Timer(interval, listener); timer.start(); }
这个方法特殊在哪里呢?来看一下控制流程:
①调用 start 方法 -> ②调用 TimePrinter 构造器,初始化 listener -> ③将 listener 引用传递给 Timer 构造器,timer 开始计时,随着 start 方法结束,beep 对应的内存被释放 -> ④actionPerformed 方法执行 if (beep) ...
看到这里,是不是觉得跟 lambda 捕获自由变量有些相似?局部类是这样做的:TimePrinter 在 beep 域释放之前对其进行了备份,现在同样用 javap 命令对 TimePrinter 的字节码文件进行解析,结果如下:
注意,这里多了一个 boolean 类型的字段 val$beep ,这是因为编译器会检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到 TimePrinter 的构造器中(通过 javap 无法看到构造器的实际参数,但利用反射就可以),局部变量必须事实上为 final 的意义在于,能与局部类内建立的拷贝保持一致。
五、匿名内部类
假如我只创建局部类的一个对象,有没有继续简化的办法呢?有,不过前提是这个类必须用来实现某个接口或继承某个已存在的类,这样就可以在创建对象时只使用接口名或父类名:
public void start(int interval, boolean beep) { ActionListener listener = new ActionListener { public void actionPerformed(ActionEvent event) { System.out.println("...") } } ... }
这种语法的含义是:创建一个实现 ActionListener 接口的类的新对象,需要实现的方法 actionPerformed 定义在括号 {} 内。一般格式为:
new SuperType(construction parameters) { // SuperType 可以是接口或类 inner class methods and data }
六、静态内部类
如果只想把一个类隐藏在另一个类的内部,而不需要内部类引用外围类的对象或者进行回调,那么,就可以将内部类声明为 static ,又叫嵌套类。静态内部类的对象的特殊之处在于:
-
没有外围类对象的引用特权(因为不一定需要创建外围类的对象)
-
可以有自己的静态域和方法
下面来看一个例子:
public class ArrayAlg { public static class Pair { private double first; private double second; public Pair(double f, double s) { first = f; second = s; } public double getFirst() {...} public double getSecond() {...} } public static Pair minmax(double[] values) { // 返回 values 中最小值和最大值 double min = Double.POSITIVE_INFINITE; double max = Double.NEGATIVE_INFINITE; for (double v : values) { if (min > v) min = v; if (max < v) max = v; } return new Pair(min, max); } }
调用 minmax 方法得到最值:
ArrayAlg.Pair pair = ArrayAlg.minmax(arr); System.out.println("min = " + pair.getFirst()); System.out.println("max = " + pair.getSecond());
如果没有将 Pair 类声明为 static,编译器会给出错误报告:
另外要注意:声明在接口中的内部类会自动成为 static 和 public 。