内部类
内部类(inner class) 是定义在另一个类中的类。为什么需要使用内部类呢? 其主要原因有以下三点:
•内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
•内部类可以对同一个包中的其他类隐藏起来。
•当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较 便捷。
使用内部类访问对象状态
语法比较复杂。鉴于此情况, 我们选择一个简单但不太实用的例子说明内部类 的使用方式。下面将进一步分析 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(); } }
令人惊讶的事情发生了。
内部类既可以访问自身的数据域,也 可以访问创建它的外围类对象的数据域。
程序清单 6-7给出了一个测试内部类的完整程序。下面我们再看一下访问控制。如果有 一个 TimePrinter 类是一个常规类,它就需要通过 TalkingClock 类的公有方法访问 beep标志, 而使用内部类可以给予改进, 即不必提供仅用于访问其他类的访问器。
//程序清单6-7 innerClass/InnerClassTest.java package innerClass; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.Timer; /** * This program demonstrates the use of inner classes. * @version 1.10 2004-02-27 * @author Cay Horstmann */ public class InnerClassTest { public static void main(String[] args) { TalkingClock clock = new TalkingClock(1000, true); clock.start(); // keep program running until user selects "Ok" 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; /** * Constructs a talking clock * @param interval the interval between messages (in milliseconds) * @param beep true if the clock should beep */ public TalkingClock(int interval, boolean beep) { this.interval = interval; this.beep = beep; } /** * Starts the clock. */ 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) { Date now = new Date(); System.out.println("At the tone, the time is " + now); if (beep) Toolkit.getDefaultToolkit().beep(); } } }
内部类的特殊语法规则
事实上,使用外围类引用的 正规语法还要复杂一些。表达式
OwterClass.this
表示外围类引用。例如,可以像下面这样编写 TimePrinter 内部类的 actionPerformed方法:
public void actionPerformed(ActionEvent event) { . . . if (TalkingClock.this,beep) Toolkit.getDefaultToolkitO.beep(); }
反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.n&H InnerClass{construction parameters)
例如,
ActionListener listener = this.new TimePrinter();
在这里,最新构造的 TimePrinter 对象的外围类引用被设置为创建内部类对象的方法中的 this 引用。这是一种最常见的情况。通常,this 限定词是多余的。不过,可以通过显式地命名 将外围类引用设置为其他的对象。
需要注意, 在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
局部内部类
如果仔细地阅读一下 TalkingClock 示例的代码就会发现, TimePrinter 这个类名字只在 start 方法中创建这个类型的对象时使用了一次。
当遇到这类情况时, 可以在一个方法中定义局部类。
public void start(){ class TiiePrinter inpleients ActionListener { public void actionPerforaed(ActionEvent event) { System.out.println("At the tone, the tine is " + new DateO);
if (beep)
Toolkit.getDefaultToolkit.beep(); } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interva1, listener); t.start(); }
局部类不能用 public 或 private访问说明符进行声明。它的作用域被限定在声明这个局部 类的块中。
局部类有一个优势, 即对外部世界可以完全地隐藏起来。 即使 TalkingClock 类中的其他 代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。
由外部方法访问变量
与其他内部类相比较, 局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还 可以访问局部变量。不过,那些局部变量必须事实上为 final。这说明,它们一旦赋值就绝不 会改变。
下面是一个典型的示例。这里, 将 TalkingClock 构造器的参数 interva丨和 beep 移至 start 方法中。
public void start(int interval, boolean beep) { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { Systea.out.println("At the tone, the tiie is " + new DateO); if (beep) Toolkit.getDefaultToolki10•beep(); } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval,listener); t.start(); }
请注意,TalkingClock 类不再需要存储实例变量 beep 了,它只是引用 start 方法中的 beep 参数变量。
为了能够清楚地看到内部的问题,让我们仔细地考査一下控制流程。
1 ) 调用 start 方法。
2) 调用内部类 TimePrinter 的构造器, 以便初始化对象变量 listener。
3 ) 将 listener 引用传递给 Timer 构造器,定时器开始计时, start 方法结束。此时,start 方法的 beep 参数变量不复存在。
4 ) 然后,actionPerformed方法执行 if(beep)...。
匿名内部类
假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。
public void start(int interval, boolean beep) { ActionListener listener = new ActionListenerO { public void actionPerformed(ActionEvent event) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } } ; Timer t = new Timer(interval, listener); t.start(); }
这种语法确实有些难以理解。它的含义是:创建一个实现 ActionListener 接口的类的新 对象,需要实现的方法 actionPerformed定义在括号内。
通常的语法格式为:
new SuperType(constructionparameters) { inner class methodsanddata }
其中, SuperType 可以是 ActionListener 这样的接口, 于是内部类就要实现这个接口。 SuperType 也可以是一个类,于是内部类就要扩展它。
由于构造器的名字必须与类名相同, 而匿名类没有类名,所以,匿名类不能有构造器。 取而代之的是,将构造器参数传递给超类(superclass) 构造器。尤其是在内部类实现接口的 时候, 不能有任何构造参数。不仅如此,还要像下面这样提供一组括号:
new InterfaceType(){ methods and data }
如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类。
程序清单 6-8包含了用匿名内部类实现语音时钟程序的全部源代码。将这个程序与程序 清单 6-7相比较就会发现使用匿名内部类的解决方案比较简短、更切实际、 更易于理解。
//程序清单 6-8 anonymousInnerClass/AnonymousInnerClassTest.java package anonymousInnerClass; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.Timer; /** * This program demonstrates anonymous inner classes. * @version 1.10 2004-02-27 * @author Cay Horstmann */ public class AnonymousInnerClassTest { public static void main(String[] args) { TalkingClock clock = new TalkingClock(); clock.start(1000, true); // keep program running until user selects "Ok" JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); } } /** * A clock that prints the time in regular intervals. */ class TalkingClock { /** * Starts the clock. * @param interval the interval between messages (in milliseconds) * @param beep true if the clock should beep */ public void start(int interval, final boolean beep) { ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { Date now = new Date(); System.out.println("At the tone, the time is " + now); if (beep) Toolkit.getDefaultToolkit().beep(); } }; Timer t = new Timer(interval, listener); t.start(); } }
静态内部类
有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用 外围类对象。为此,可以将内部类声明为 static, 以便取消产生的引用。
在内部类不需要访问外围类对象的时候, 应该使用静态内部类。 有些程序员用嵌 套类 (nested class) 表示静态内部类。
与常规内部类不同, 静态内部类可以有静态域和方法。
声明在接口中的内部类自动成为 static 和 public 类。
程序清单 6-9 包含 ArrayAIg 类和嵌套的 Pair 类的全部源代码。
//程序清单 6-9 staticInnerClass/StaticInnerClassTest.java package staticInnerClass; /** * This program demonstrates the use of static inner classes. * @version 1.01 2004-02-27 * @author Cay Horstmann */ 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()); } } class ArrayAlg { /** * A pair of floating-point numbers */ public static class Pair { private double first; private double second; /** * Constructs a pair from two floating-point numbers * @param f the first number * @param s the second number */ public Pair(double f, double s) { first = f; second = s; } /** * Returns the first number of the pair * @return the first number */ public double getFirst() { return first; } /** * Returns the second number of the pair * @return the second number */ public double getSecond() { return second; } } /** * Computes both the minimum and the maximum of an array * @param values an array of floating-point numbers * @return a pair whose first element is the minimum and whose second element * is the maximum */ 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); } }
第七章 异常、断言和曰志
处理错误
也有可能是因为使用无效的数组下标, 或者试图使用 一个没有被赋值的对象引用而造成的。用户期望在出现错误时, 程序能够采用一些理智的行 为。如果由于出现错误而使得某些操作没有完成, 程序应该:
•返回到一种安全状态,并能够让用户执行一些其他的命令;
•允许用户保存所有操作的结果,并以妥善的方式终止程序。
为了能够在程序中处理异常情况, 必须研究程序中可能会出现的错误和问题, 以及哪类 问题需要关注。
1.用户输入错误
除了那些不可避免的键盘输人错误外, 有些用户喜欢各行其是,不遵守程序的要求。例 如, 假设有一个用户请求连接一个 URL,而语法却不正确。在程序代码中应该对此进行检 查, 如果没有检査,网络层就会给出警告。
2.设备错误
硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页可能临时性地不能浏 览。在一个任务的处理过程中,硬件经常出现问题。例如,打印机在打印过程中可能没有纸了。
3.物理限制
磁盘满了,可用存储空间已被用完。
4.代码错误
程序方法有可能无法正确执行。例如,方法可能返回了一个错误的答案,或者错误地调 用了其他的方法。计算的数组索引不合法,试图在散列表中查找一个不存在的记录, 或者试 图让一个空找执行弹出操作,这些都属于代码错误。
异常分类
在 Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。
需要注意的是,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分 支:Error 和 Exception。
Error类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该 抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地 终止之外, 再也无能为力了。这种情况很少出现。
在设计 Java 程序时, 需要关注 Exception 层次结构。这个层次结构又分解为两个分支: 一个分支派生于 RuntimeException ; 另一个分支包含其他异常。划分两个分支的规则是: 由 程序错误导致的异常属于 RuntimeException ; 而程序本身没有问题, 但由于像 I/O 错误这类 问题导致的异常属于其他异常。
派生于 RuntimeException 的异常包含下面几种情况:
•错误的类型转换。
•数组访问越界 。
•访问 null 指针。
不是派生于 RuntimeException 的异常包括:
•试图在文件尾部后面读取数据。
•试图打开一个不存在的文件。
•试图根据给定的字符串查找 Class 对象, 而这个字符串表示的类并不存在。
Java语 言 规 范 将 派 生 于 Error 类 或 RuntimeException类的所有异常称为非受查 ( unchecked) 异常,所有其他的异常称为受查(checked) 异常。这是两个很有用的术语,在 后面还会用到。编译器将核查是否为所有的受査异常提供了异常处理器。
声明受查异常
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出 哪类受査异常。
在自己编写方法时, 不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法 中用 throws 子句声明异常, 什么异常必须使用 throws 子句声明, 需要记住在遇到下面 4 种 情况时应该抛出异常:
1 ) 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。
2 ) 程序运行过程中发现错误,并且利用 throw语句抛出一个受查异常(下一节将详细地 介绍 throw 语句)。
3 ) 程序出现错误, 例如,a[-l]=0 会抛出一个 ArraylndexOutOffloundsException 这样的 非受查异常。
4 ) Java 虚拟机和运行时库出现的内部错误。
如何抛出异常
假设在程序代码中发生了一些很糟糕的事情。一个名为 readData 的方法正在读取一个首 部具有下列信息的文件:
Content-length: 1024
然而,读到 733 个字符之后文件就结束了。我们认为这是一种不正常的情况,希望抛出一个 异常。
首先要决定应该抛出什么类型的异常。将上述异常归结为 IOException 是一种很好的选 择。仔细地阅读 Java API 文档之后会发现:EOFException异常描述的是“ 在输人过程中, 遇 到了一个未预期的 EOF 后的信号”。这正是我们要抛出的异常。下面是抛出这个异常的语句:
throw new EOFExceptionQ;
或者
EOFException e = new EOFExceptionO;
throw e;
下面将这些代码放在一起:
String readData(Scanner in) throws EOFException{ . . . while (…) { if (Mn.hasNextQ) // EOF encountered { if (n < len) throw new EOFExceptionQ; } ... } return s; }
EOFException类还有一个含有一个字符串型参数的构造器。这个构造器可以更加细致的 描述异常出现的情况。
String gripe = "Content-length: " + len + ", Received: " + n; throw new EOFException(gripe);
在前面已经看到, 对于一个已经存在的异常类, 将其抛出非常容易 D 在这种情况下:
1 ) 找到一个合适的异常类。
2 ) 创建这个类的一个对象。
3 ) 将对象抛出。 一旦方法抛出了异常, 这个方法就不可能返回到调用者。也就是说, 不必为返回的默认 值或错误代码担忧。
创建异常类
定义一个派生于 IOException 的类。 习惯上, 定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息 的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。
class FileFormatException extends IOException { public FileFormatException() {} public FileFormatException(String gripe) { super(gripe); } }
现在,就可以抛出自己定义的异常类型了。
String readData(BufferedReader in) throws FileFormatException { ... while (. ..) { if (ch == -1 ) // EOF encountered { if (n < len) throw new FileFornatExceptionQ; } ... } return s; }
浙公网安备 33010602011771号