使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。
day043 内部类(使用内部类访问对象状态、内部类的特殊语法规则、内部类是否有用、必要和安全、局部内部类)
内部类(inner class)是定义在另一个类中的类。为什么需要使用内部类呢?其主要原因有以下三点:
•内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
•内部类可以对同一个包中的其他类隐藏起来。
•当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
1.使用内部类访问对象状态
内部类的语法比较复杂。鉴于此情况,我们选择一个简单但不太实用的例子说明内部类的使用方式。下面将进一步分析TimerTest示例,并抽象出一个TalkingClock类。构造一个语音时钟时需要提供两个参数:发布通告的间隔和开关铃声的标志。
public class TalkingClock
{
private int interval;
private boolean beep;
public TalkingClock(int interval,boolean beep) {...}
public void start() {...}
public class TimePrinter implements ActionListener
//an inner class
{
...
}
}
需要注意,这里的TimePrinter类位于TalkingClock类内部。这并不意味着每个TalkingClock都有一个TimePrinter实例域,如前所示,TimePrinter对象是由TalkingClock类的方法构造。
下面是TimePrinter类的详细内容。需要注意一点,actionPerformed方法在发出铃声之前检查了beep标志。
public 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类没有实例域或者名为beep的变量,取而代之的是beep引用了创建TimePrinter的TalkingClock对象的域。这是一种创新的想法。从传统意义上讲,一个方法可以引用调用这个方法的对象数据域。内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
为了能够运行这个程序,内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。
这个引用在内部类的定义中是不可见的。然而,为了说明这个概念,我们将外围类对象的引用称为outer。于是actionPerformed方法将等价于下列形式:
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone,the time is " + new Date());
if(outer.beep)
Toolkit.getDefaultToolkit().beep();
}
外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。因为TimePrinter类没有定义构造器,所以编译器为这个类生成了一个默认的构造器,其代码如下所示:
public TimePrinter(TalkingClock clock) // automatically generated code
{
outer = clock;
}
请再注意一下,outer不是Java的关键字。我们只是用它说明内部类中的机制。
当在start方法中创建了TimePrinter对象后,编译器就会将this引用传递给当前的语音时钟的构造器:
ActionListener listener = new TimePrinter(this);//parameter automatically added
下面的程序给出了一个测试内部类的完整程序。下面我们再看一下访问控制。如果有一个TimePrinter类是一个常规类,它就需要通过TalkingClock类的公有方法访问beep标志,而使用内部类可以给予改进,即不必提供仅用于访问其他类的访问器。
/**
*@author zzehao
*/
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
public class InnerClassTest
{
public static void main(String[] args)
{
TalkingClock clock = new TalkingClock(1000,true);
clock.start();
//keep program running until user selects "0k"
JOptionPane.showMessageDialog(null,"Quit program?");
System.exit(0);
}
}
/*
*A clock that prints the time in regular intervals.
*/
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
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone,the time is " + new Date());
if(beep)
Toolkit.getDefaultToolkit().beep();
}
}
}
运行的结果是:
2.内部类的特殊语法规则
在上面,已经讲述了内部类有一个外围类的引用outer。事实上,使用外围类引用的正规语法还要复杂一些。表达式
OuterClass.this
表示外围类引用。例如,可以像下面这样编写TimePrinter内部类的actionPerformed方法:
public void actionPerformed(ActionEvent event)
{
...
if(TalkingClock.this.beep)
Toolkit.getDefaultTookit().beep();
}
反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
例如:
ActionListener listener = this.new TimePrinter();
在这里,最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法中的this引用。这是一种最常见的情况。通常,this限定词是多余的。不过,可以通过显式地命名将外围类引用设置为其他的对象。例如,如果TimePrinter是一个公有内部类,对于任意的语音时钟都可以构造一个TimePrinter:
TalkingClock jabberer = new TalkingClock(1000,true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
需要注意,在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
内部类中声明的所有静态域都必须是final。原因很简单。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是final,它可能就不是唯一的。
内部类不能有static方法。Java语言规范对这个限制没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态域和方法。显然,Java设计者认为相对于这种复杂性来说,它带来的好处有些得不偿失。
3.内部类是否有用、必要和安全
当在Java1.1的Java语言中增加内部类时,很多程序员都认为这是一项很主要的新特性,但这却违背了Java要比C++更加简单的设计理念。内部类的语法很复杂(可以看到,稍后介绍的匿名内部类更加复杂)。它与访问控制和安全性等其他的语言特性的没有明显的关联。
由于增加了一些看似优美有趣,实属没必要的特性,似乎Java也开始走上了许多语言饱受折磨的毁灭性道路上。内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用$(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。
例如,在TalkingClock类内部的TimePrinter类将被翻译成类文件TalkingClock$TimePrinter.class,为了能够看到执行的效果,可以做一下这个实验:运行之前的程序ReflectionTest,并将类TalkingClockSTimePrinter传递给它进行反射。也可以选择简单地使用javap,如下所示:
javap -private ClassName
ReflectionTest程序如下:
/**
*@author zzehao
*/
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest
{
public static void main(String[]args)
{
//read class name from command line args or user input
String name;
if(args.length>0)
name=args[0];
else
{
Scanner in=new Scanner(System.in);
System.out.println("Enter class name(e.g.java.util.Date):");
name=in.next();
}
try
{
//print class name and super class name(if!=Object)
Class cl=Class.forName(name);
Class supercl=cl.getSuperclass();
String modifiers=Modifier.toString(cl.getModifiers());
if(modifiers.length()>0)
System.out.print(modifiers+"");
System.out.print("class"+name);
if(supercl!=null&&supercl!=Object.class)
System.out.print("extends"+supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
System.exit(0);
}
public static void printConstructors(Class cl)
{
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors)
{
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length()> 0)
System.out.print(modifiers+" ");
System.out.print(name+"(");
//print parameter types
Class[] paramTypes= c.getParameterTypes();
for (int j = 0; j <paramTypes.length;j++)
{
if(j>0)
System.out.print(",");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
public static void printMethods(Class cl)
{
Method[] methods=cl.getDeclaredMethods();
for (Method m : methods)
{
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
//print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if(modifiers.length()>0)
System.out.print(modifiers+ " ");
System.out.print(retType.getName()+" "+name+"(");
//print parameter types
Class[] paramTypes= m.getParameterTypes();
for (int j = 0; j< paramTypes.length; j++)
{
if (j> 0)
System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
public static void printFields(Class cl)
{
Field[] fields = cl.getDeclaredFields();
for (Field f : fields)
{
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length()> 0)
System.out.print(modifiers+" ");
System.out.println(type.getName()+ " " +name+";");
}
}
}
这时会看到下面的输出结果:
public class TalkingClock$TimePrinter
{
public TalkingClock$TimePrinter(TalkingClock);
public void actionPerformed(java.awt.event.ActionEvent);
final TalkingClock this$0;
}
可以清楚地看到,编译器为了引用外围类,生成了一个附加的实例域this$0(名字this$0是由编译器合成的,在自己编写的代码中不能够引用它)。另外,还可以看到构造器的TalkingClock参数。
如果编译器能够自动地进行转换,那么能不能自己编写程序实现这种机制呢?让我们试试看。将TimePrinter定义成一个常规类,并把它置于TalkingClock类的外部。在构造TimePrinter对象的时候,将创建该对象的this指针传递给它
class TalkingClock
{
...
public void start()
{
ActionListener listener = new TimePrinter(this);
Timer t = new Timer(interval,listener);
t.start();
}
}
class TimePrinter implements ActionListener
{
private TalkingClock outer;
...
public TimePrinter(TalkingClock clock)
{
outer = clock;
}
}
现在,看一下actionPerformed方法,它需要访问outer.beep。
if(outer.beep) ...//Error
这就遇到了一个问题。内部类可以访问外围类的私有数据,但这里的TimePrinter类则不行。
可见,由于内部类拥有访问特权,所以与常规类比较起来功能更加强大。
可能有人会好奇,既然内部类可以被翻译成名字很古怪的常规类(而虚拟机对此一点也不了解),内部类如何管理那些额外的访问特权呢?为了揭开这个谜团,让我们再次利用ReflectTest程序査看一下TalkingClock类:
class TalkingClock
{
int interval;
private boolean beep;
public TalkingClock(int, boolean);
static boolean access$0(TalkingClock);
public void start();
}
请注意编译器在外围类添加静态方法access$0。它将返回作为参数传递给它的对象域beep。(方法名可能稍有不同,如access$000,这取决于你的编译器。)
内部类方法将调用那个方法。在TimePrinter类的actionPerformed方法中编写语句:
if(beep)
将会提高下列调用的效率:
if(TalkingClock.access$0(outer))
这样做不是存在安全风险吗?这种担心是很有道理的。任何人都可以通过调用access$0方法很容易地读取到私有域beep。当然,access$0不是Java的合法方法名。但熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。由于隐秘地访问方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。
总而言之,如果内部类访问了私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但做这些事情需要高超的技巧和极大的决心。程序员不可能无意之中就获得对类的访问权限,而必须刻意地构建或修改类文件才有可能达到这个目的。
4.局部内部类
如果仔细地阅读一下TalkingClock示例的代码就会发现,TimePrinter这个类名字只在start方法中创建这个类型的对象时使用了一次。
当遇到这类情况时,可以在一个方法中定义局部类。
public void start()
{
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();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。