参考自–《Java核心技术卷1》
内部类(inner class)是定义在另一个类中的类。使用内部类的原因如下:
通过一个例子认识内部类:每隔一段时间输出当前时间
public class TalkingClock {
private int interval; //时间间隔
private boolean beep; //控制输出
public TalkingClock(int interval, boolean beep) {
this.interval = interval;
this.beep = beep;
}
public void start(){
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
public class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
if(beep){
System.out.println(new Date());
}
}
}
}
TimePrinter
类位于 TalkingClock
类内部。但是这并不意味着每个 TalkingClock
都有一个 TimePrinter
实例域,只有 start
方法才能访问到TimePrinter
实例。
需要注意,TimePrinter
类没有实例域(即没有名为 beep 的变量),它引用的 beep 是创建TimePrinter
的 TalkingClock
对象的域。
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
究其原理:内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。
上述的 outer
即指代这个隐式引用。这个引用在内部类的定义中是不可见的。
内部类中,外围类的引用(隐式引用)在构造器中设置。编译器修改了所有内部类的构造器,添加了一个外围类引用的参数。因为上述的TimePrinter
没有定义构造器,所有编译器为这个类生成了一个默认的构造器,其代码如下所示:
public TimePrinter(TalkingClock clock){
outer = clock;
}
//actionPerformed方法的实现中调用外部类的beep
if(outer.beep){ ... }
注:outer 不是 Java 的关键字,它在此处只用于指代外围类的引用。
当TalkingClock
的 start
方法创建 TimePrinter
对象时,编译器传入TalkingClock
类的 this:
ActionListener listener = new TimePrinter(this);
在访问控制上,若TimePrinter
只是一个常规类,它就只能通过访问TalkingClock
类中的公有方法访问 beep域,而使用内部类则可以直接访问TalkingClock
的 beep域。
注:TimePrinter
类可以声明为私有的;这样就只有TalkingClock
的方法可以构造TimePrinter
对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
在上述内容中,使用 outer
代指外围类的引用,事实上,使用外围类引用的正规语法还要复杂一些。
表达式:OuterClass.this
表示外围类引用,如:可以这样编写内部类TimePrinter
的 actionPerformed
方法
public void actionPerformed(ActionEvent e) {
if(TalkingClock.this.beep){
System.out.println(new Date());
}
}
同样,可以采用如下语法更加明确地编写内部对象的构造器:
outerObject.new InnerClass(内部类构造参数);
例如:
ActionListener listener = this.new TimePrinter();
在外围类的作用域之外,可以这样引用内部类(前提是此内部类是公有内部类):
OuterClass.InnerClass
例如:
TalkingClock outer = new TalkingClock(1000,true);
TalkingClock.TimePrinter listener = outer.new TimePrinter();
注:内部类中声明的所有静态域都必须是 final(如果是可变的静态域,则不同外部类对象的内部类实例可能会有所不同).内部类不能有static方法(也可以有,但只能访问外围类的静态域和静态方法)。
在上述的例子中,仔细看可以发现,TimePrinter
这个内部类只在 start 方法中创建这个内部类对象时使用了一次。这种情况就可以在方法中定义局部类:
public void start(){
class TimePrinter implements ActionListener{
public void actionPerformed(ActionEvent e) {
if(beep){
System.out.println(new Date());
}
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
局部类不能使用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势:它对外部代码可以完全地隐藏起来。即使 TalkingClock
类中的其他代码也不能访问它。除了 start
方法外,没有任何其他方法知道 TimePrinter
类的存在。
相对其他内部类,局部类还有一个优点:它不仅能够访问包含它们的外部类,还可以访问局部变量(不过这些局部变量必须事实上为 final,它们一旦赋值就不能改变)。
如下:将TalkingClock
类的 interval 和 deep 域移至 start 方法参数上
public void start(int interval,boolean deep){
class TimePrinter implements ActionListener{
public void actionPerformed(ActionEvent e) {
if(beep){
System.out.println(new Date());
}
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
此处,局部类引用的 deep 变量是一个局部变量。
看起来很正常,走一遍控制流程看看:
1)调用 start
方法
2)调用内部类 TimePrinter
的构造器,初始化对象变量 listener
3)将 listener 引用传递给 Timer 构造器,定时器开始计时,start 方法结束。此时 start 方法的 deep 参数变量已经不存在了
4)然后,actionPerformed
方法运行 if(beep) ...
为了让actionPerformed
方法正常工作(即可以调用 beep 变量),TimePrinter
类在 beep 域释放之前将 beep 域用 start 方法的局部变量进行备份。实际上也是这样做的。编译器在创建 TimePrinter
对象时,beep 就会被传递给构造器,并存储在 TimePrinter
类的 final 域中。
将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必给这个类命名了。这种类被称为匿名内部类。
public void start(int interval,boolean deep){
//匿名类
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent e) {
if(beep){
System.out.println(new Date());
}
}
}
Timer t = new Timer(interval,listener);
t.start();
}
上述语句的含义是:创建一个实现 ActionListener
接口的类的新对象,需要实现的方法 actionPerformed
定义在 { } 中。
通常的匿名内部类的语法格式为:
new SuperType(构造器参数){
//实现超类的内部类
inner class methods and data
}
其中,如果SuperType
是接口,那么内部类就要实现这个接口;如果 SuperType
是一个类,内部类就需要扩展它。
由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。
new InterfaceType(){
//实现接口的内部类
inner class methods and data
}
内部类一般用于实现事件监听器和其他回调,lambda 表达式也可以实现类似功能,甚至还更加简便:
public void start(int interval,boolean deep){
//lambda表达式
Timer t = new Timer(interval,event -> {
if(beep){
System.out.println(new Date());
}
});
t.start();
}
注:匿名列表,“双括号初始化”技巧:
假设构造一个数据列表,并把它传递到一个方法(invite):
ArrayList<String> friends = new ArrayList<>();
friends.add("Tom");
friends.add("Mary");
invite(friends);
如果此后不再需要这个数组列表,最好将它作为一个匿名列表:
invite(new ArrayList<String>(){{ add("Tom");add("Mary"); }});
特别注意此处的双括号。外层括号建立了 ArrayList
的一个匿名子类;内层括号则是一个对象构造块。
有时候使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。因此,可以将内部类声明为 static ,以便取消产生的引用。
下面是一个使用静态内部类的例子:考虑计算数组中最小值和最大值的问题。
可以编写两个方法,一个方法用于计算最小值,另一个方法用于计算最大值。但是,在调用这两个方法时,数组被遍历了两次。如果只遍历一次数组,便能计算出数组的最小值和最大值,就可以提高代码效率:
定义需要的方法和类:
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(){
return first;
}
public double getSecond(){
return second;
}
}
//定义方法,遍历一次数组计算出数组的最小值和最大值(会返回两个数值,此时可以使用定义的静态内部类Pair)
public static Pair minmax(double[] values){
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (double v : values){
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
}
//主函数,调用方法计算得出数组的最小值和最大值
public class StaticInnerClassTest {
public static void main(String[] args){
double[] d = new double[20];
for (int i = 0; i < d.length; i++)
d[i] = 100 * Math.random();
//定义静态内部类对象
ArrayAlg.Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
显然,上述的 Pair 对象不需要引用任何其他的对象。
只有内部类可以声明为 static 。静态内部类的对象除了没有生成它的外围类对象的引用特权外,与其他所有内部类完全是一样的。
注:在内部类不需要访问外围类对象时,有关使用静态内部类。与常规内部类不同,静态内部类可以有静态域和方法。
注:声明在接口中的内部类自动称为 static 和 public 类。