目录
内部类是定义在另一个类中的类。使用内部类有两个原因:
- 内部类可以对同一个包中的其他类隐藏。
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
1 内部类的语法规则
下面展示一个内部类的实例:
public class TalkingClock
{
private int interval; //发出通知的时间间隔
private boolean beep; //开关铃声的标志
public TalkingClock(int interval, boolean beep)
{
this.interval = interval;
this.beep = beep;
}
public void start()
{
TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
public class TimePrinter implements ActionListener //内部类
{
public void actionPerformed(ActionEvent event)
{
System.out.pringln("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}
这里的TimePrinter类位于TalkingClock类内部,这并不意味着每个TalkingClock对象都有一个TimePrinter实例字段,TimePrinter对象是由TalkingClock类中的start方法构造的。
TimePrinter类中的actionPerformed方法使用了变量beep,这个beep变量实际上是创建TimePrinter对象的TalkingClock对象内部的字段。内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段。为此,内部类对象有一个隐式引用,指向创建它的外围类对象。这个引用在内部类的定义中是不可见的。
外围类的引用在构造器中设置。编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数。例如,上面的TimerPrinter类没有定义构造器,编译器会自动生成一个无参数构造器,并添加一个外围类TalkingClock类型的参数,代码如下:
public TimePrinter(TalkingClock clock)
{
TalkingClock.this = clock;
}
内部类中对外围类引用的调用是隐式的,可以通过外围类名.this
显式调用。例如上面的TalkingClock.this
就是对外围类引用的显式调用。为了明显区分内部类自身的字段和外围类的字段,可以在内部类方法中显式使用外围类引用。例如,TimePrinter内部类的actionPerformed方法可以修改如下:
public void actionPerformed(ActionEvent event)
{
System.out.pringln("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
反过来,可以采用以下语法更加明确地调用内部类的构造器:外围类对象.new 内部类名(参数)
。例如:
public void start()
{
TimePrinter listener = this.new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
在外围类的作用域之外,可以这样引用内部类:外围类名.内部类名
。例如:
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。内部类中不能有静态方法。
2 局部内部类
在TalkingClock类中,只在start方法中使用了TimePrinter内部类。当遇到这种情况时,可以在方法中局部地定义这个类。例如:
public void start()
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.pringln("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
声明局部类时不能有访问说明符(public或private)。局部类的作用域被限定在声明这个局部类的块中,它对外部世界完全隐藏。除了start方法之外,没有任何方法知道TimePrinter类的存在。
局部内部类不仅能够访问外部类的字段,还可以访问局部变量。不过,这些局部变量必须是事实最终变量,一旦赋值就不会改变。例如:
public class TalkingClock
{
public void start(int interval, boolean beep)
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.pringln("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
TimePrinter listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
}
actionPerformed方法中使用的beep变量是start方法的参数变量。编译器检测对局部变量的访问,在局部内部类中为每一个变量建立相应的实例字段,并将局部变量复制到构造器,从而能初始化这些实例字段。在这个例子中,编译器会在TimePrinter类中自动添加beep变量对应的字段,在构造TimePrinter对象时用beep参数变量初始化这个字段。
3 匿名内部类
使用局部内部类时,如果只是创建这个类的一个对象而不需要其他操作,甚至可以省略内部类的名字,这样的类称为匿名内部类。匿名内部类的语法如下:
new SuperType(//构造器参数)
{
//内部类方法和字段
}
SuperType可以是接口,也可以是一个类。当它是接口时,内部类就要实现这个接口;当它是一个类时,内部类就要继承这个类。这个语法的含义是:定义一个匿名内部类,这个内部类扩展了SuperType,并创建这个类的对象。
由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以匿名内部类不能有构造器。上述语法中的构造器参数要传递给超类构造器。如果SuperType是一个接口,就不需要构造器参数。尽管匿名内部类不能有构造器,但可以提供一个对象初始化块。
start方法中只创建TimePrinter类的一个对象,可以使用匿名内部类,代码如下:
public void start(int interval, boolean beep)
{
var listener = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer timer = new Timer(interval, listener);
timer.start();
}
不能在静态方法中直接调用getClass获得类信息,因为调用getClass时调用的是this.getClass,而静态方法没有this。这时应该使用如下表达式:
new Object(){}.getClass().getEnclosingClass()
new Object(){}会建立Object的匿名子类的一个匿名对象,getEnclosingClass得到匿名内部类的外围类,也就是包含这个静态方法的类。
4 静态内部类
有时使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类有外围类对象的引用。为此,可以将内部类声明为static,这样就不会生成那个引用。
下面的例子展示了静态内部类的使用。minmax方法同时计算数组中的最大值和最小值,它需要返回两个数,为此将这两个数包装在Pair类中。为了防止Pair类和其他类产生名字冲突,将它定义为另一个类的公共内部类。这时,Pair类不需要任何其他对象的引用,可以将它声明为static。代码如下:
public class StaticInnerClassTest
{
public static void main(String[] args)
{
double[] values = new double[20];
for (int i = 0; i < values.length; i++)
{
values[i] = 100 * Math.random();
}
ArrayAlg.Pair p = ArrayAlg.minmax(values);
System.out.println("min = " + p.getMin());
System.out.println("max = " + p.getMax());
}
}
class ArrayAlg
{
public static class Pair
{
private double min;
private double max;
public Pair(double f, double s)
{
min = f;
max = s;
}
public double getMin()
{
return min;
}
public double getMax()
{
return max;
}
}
public static Pair minmax(double[] values)
{
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values)
{
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
}
在这里,Pair类必须声明为static。因为Pair类对象是在静态方法minmax中构造的,没有外围类对象,无法对外围类对象的引用进行初始化。
只要内部类不需要访问外围类对象,就应该使用静态内部类。
与常规内部类不同,静态内部类可以有静态字段和方法。
在接口中声明的内部类自动是static和public。