内部类(inner class)是定义在另一个类中的类,一般有三个特点:
- 内部类可访问该类定义所在作用域中的数据,包括私有数据
- 内部类可以对同一个包的其他类隐藏起来
- 定义一个回调函数且不想编写大量代码时,可通过匿名内部类实现
1 内部类如何访问对象状态
public class TalkingClock
{
private int interval;
private boolean beep;
public TalkingClock(int interval,boolean 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)
{
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}
内部类TimePrinter中访问了外部类TalkingClock对象中的beep变量,这里实际上可以表示为;
TalkingClock.this.beep
在内部类中存在一个对外围类的引用outer(TalkingClock.this),同样,外围类的作用域之外,可以通过OuterClass.InnerClass引用内部类。
2 编译器如何处理内部类
内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用$符号分割外部类名与内部类名的常规类文件,而虚拟机不知道这些。这一点可以通过反射来测试:4.5 通过反射打印类的信息。
编译器为了让内部类引用外围类,生成了一个附加实例域tihis$0,同时还能看到构造器中的TalkingClock参数。通过this$0可以访问外围类的私有数据。同时编译器根据内部类对外围类变量的引用,在外围类添加静态方法,使用上面的反射程序查看TalkingClock的类信息:
这里编译器添加了access$000静态方法,返回布尔类型值,即对应了原程序中内部类对外围类beep变量的引用。这样做其实有一定的风险,beep变量在TalkingClock中是私有变量,而内部类TimePrinter类是公有状态,可以在类外访问,这样任何人都可以调用access$000方法轻易地读取到私有域beep。
3 局部内部类
上面示例中,TimePrinter类仅在start方法中创建listener对象时使用了一次,对于这种情况,可以将TimePrinter定义为局部内部类,将其作用域限定在start方法中。
局部类声明在某个块中,不能用public或private访问说明符进行声明,它的作用域声明在这个局部类的块中。局部类的优势在于,可以对外界完全隐藏起来,除了start方法之外,没有任何方法知道TimePrinter类的存在:
public void start(int interval, boolean beep)
{
class TimePrinter implements ActionListener
{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
这里将interval和beep变量从构造器参数移到start方法参数中,这样局部内部类访问的就不是内部类的变量,而是start方法参数引用的局部变量,为了能够让actionPerformed方法工作,TimePrinter类在beep域释放之前将beep域用start方法的局部变量进行备份。通过上面的反射程序再次查看TimePrinter类的信息:
可以看到,编译器在TimePrinter类中生成了一个val$beep变量,并且在构造器参数中有一个布尔类型参数,当创建一个TimePrinter对象时,beep就会被传递给构造器,并存储在val$beep域中。注意,局部变量引用到内部类后被声明为final类型,初始化后便不能修改。
4 匿名内部类
假如只创建内部类的一个对象,就可以不用命名了,这种类被称为匿名内部类(anonymous inner class),即创建一个实现ActionListener接口的新对象,需要实现的方法定义在中括号{}内:
public void start(int interval, boolean beep)
{
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval,listener);
t.start();
}
由于没有类名,所以匿名类不能有构造器,取而代之的是,将构造器参数传递给超类构造器。匿名内部类一般用于实现事件监听器和其他回调,这一点目前主要改为使用lambda表达式实现。
public void start(int interval, boolean beep)
{
Timer t = new Timer(interval, event ->
{
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
});
t.start();
}
当需要构造一个只使用一次的列表,可以将其作为一个匿名列表,采用双括号初始化(double brace initialization)方法:
ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);
//double brace initialization
invite(new ArrayList<String>() {{add("Harry");add("Tony");}});
外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象构造块。
5 静态内部类
有时,使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。下面的例子中,minmax函数需要同时返回数组中的最小值和最大值,为此需要定义Pair类描述这种返回类型,为了防止产生名字冲突,将其定义为ArrayAlg的内部公有类,通过ArrayAlg.Pair访问它:
public class StaticInnerClassTest {
public static void main(String[] args)
{
double[] d = new double[20];
for (int i = 0; i < 20; i++)
{
d[i] = 100 * Math.random();
}
ArrayAlg.Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
class ArrayAlg
{
/**
* A pair of floating-point numbers
*/
public static class Pair
{
private double first;
private double second;
/**
* 构造一对浮点数
* @param f first number
* @param s second number
*/
public Pair(double f,double s)
{
first = f;
second = s;
}
/**
* 返回第一个数字
* @return first number
*/
public double getFirst()
{
return first;
}
/**
* 返回第二个数组
* @return second number
*/
public double getSecond()
{
return second;
}
}
/**
* 同时计算数组的最小值和最大值
*/
public static Pair minmax(double[] values)
{
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values)
{
min = Math.min(min, v);
max = Math.max(max, v);
}
return new Pair(min,max);
}
}
只有内部类可以声明为static,与常规内部类不同,只有静态内部类可以用于静态域和静态方法,而声明在接口中的内部类自动成为static和public类。