7.1 处理错误
假设Java程序正在运行时发生错误。错误可能是由包含错误信息的文件、不稳定的网络连接或(我们不想提及)使用无效的数组索引或尚未分配给对象的对象引用引起的。用户希望程序在发生错误时能够灵敏地运行。如果操作因错误而无法完成,程序应该
- 返回到安全状态并允许用户执行其他命令;或
- 允许用户保存所有工作并优雅地终止程序。
这可能不容易做到,因为检测(甚至导致)错误条件的代码通常远离可以将数据回滚到安全状态或保存用户工作并愉快退出的代码。异常处理的任务是将控制权从错误发生的位置转移到可以处理这种情况的错误处理程序。要处理程序中的异常情况,必须考虑可能发生的错误和问题。你需要考虑哪些问题?
- 用户输入错误。除了不可避免的打字错误外,一些用户喜欢开辟自己的道路,而不是遵循指示。例如,假设用户请求连接到语法错误的URL。您的代码应该检查语法,但假设它不检查。然后网络层会抱怨。
- 设备错误。硬件并不总是按您的要求工作。打印机可能已关闭。网页可能暂时不可用。设备通常会在任务执行过程中发生故障。例如,打印机在打印过程中可能会缺纸。
- 物理限制。磁盘可以填满;可用内存不足。
*
代码错误。方法可能无法正确执行。例如,它可能会给出错误的答案,或者错误地使用其他方法。计算无效的数组索引、尝试在哈希表中查找不存在的条目或尝试弹出空堆栈都是代码错误的例子。
对方法中错误的传统反应是返回调用方法分析的特殊错误代码。例如,从文件中读取信息的方法通常返回-1
文件结束值标记,而不是标准字符。这是处理许多异常情况的有效方法。表示错误条件的另一个常见返回值是null
引用。
不幸的是,不可能总是返回错误代码。可能没有明显的方法来区分有效和无效的数据。返回整数的方法不能简单地返回-1
来表示错误;值-1
可能是一个完全有效的结果。
相反,正如我们在第5章中提到的,Java允许每个方法都是一个替代的退出路径,如果它不能以正常的方式完成它的任务。在这种情况下,该方法不返回值。相反,它抛出一个封装错误信息的对象。请注意,该方法立即退出;它不返回其正常(或任何)值。此外,不会在调用该方法的代码处恢复执行;相反,异常处理机制开始搜索可以处理此特定错误条件的异常处理程序。
异常有自己的语法,并且是特殊继承层次结构的一部分。我们将首先学习语法,然后给出一些关于如何有效使用此语言特性的提示。
7.1.1 异常的分类
在Java编程语言中,异常对象始终是由Throwable
类派生的实例。正如您将很快看到的那样,如果内置到Java中的异常类不适合您的需要,则可以创建自己的异常类。
图7.1是Java中异常层次结构的简化图。
图7.1 Java中的异常层次结构
请注意,所有异常都是从Throwable
继承的,但层次结构立即分为两个分支:Error
和Exception
。
Error
层次结构描述了Java运行时系统内部错误和资源耗尽情况。不应抛出此类型的对象。如果发生这样的内部错误,除了通知用户并尝试优雅地终止程序之外,您几乎无能为力。这些情况非常罕见。
在进行Java编程时,请注意Exception
层次结构。Exception
层次结构还分为两个分支:从RuntimeException
派生的异常和不派生的异常。一般规则是:RuntimeException
异常的发生是因为发生了编程错误。任何其他异常都会发生,因为一个坏的事情,比如I/O错误,发生在你原本好的程序上。
继承自RuntimeException
的异常包括这些问题
- 一个糟糕的强制转换
- 一个越界数组访问
- 一个空指针访问
不从RuntimeException
继承的异常包括
- 试图读取超过文件结尾的内容
- 试图打开不存在的文件
- 试图为不表示现有类的字符串查找
Class
对象
“如果它是一个RuntimeException
,那是你的错”规则运行得很好。通过根据数组边界测试数组索引,可以避免ArrayIndexOutOfBoundsException
。如果在使用变量之前检查该变量是否为空,则不会发生NullPointerException
。
不存在的文件怎么样?您不能先检查文件是否存在,然后再打开它吗?好吧,检查文件是否存在后,可能会立即删除它。因此,“存在”的概念取决于环境,而不仅仅取决于代码。
Java语言规范调用任何从Error
类或者不检查的异常RuntimeException
类继承过来的异常。所有其他异常都称为检查异常。这是我们也采用的有用术语。编译器检查您是否为所有选中的异常提供异常处理程序。
注意
RuntimeException
的名称有些令人困惑。当然,我们讨论的所有错误都发生在运行时。
C++注意
如果您熟悉标准C++库的(更为有限的)异常层次结构,那么在这一点上您可能会感到困惑。C++有两个基本的异常类:
runtime_error
和logic_error
。logic_error
类相当于Java的RuntimeException
,也表示程序中的逻辑错误。runtime_error
类是由不可预知的问题引起的异常的基类。它相当于Java中不属于RuntimeException
类型的那些异常。
7.1.2 声明检查异常
Java方法可以在遇到不能处理的情况时抛出异常。这个想法很简单:一种方法不仅能告诉Java编译器它能返回什么值,它还会告诉编译器什么出错。例如,试图从文件中读取的代码知道该文件可能不存在或者可能是空的。因此,试图处理文件中信息的代码需要通知编译器它可以抛出某种IOException。
您公布您的方法可以引发异常的地方是方法的头;头会更改以反映方法可以引发的已检查异常。例如,这里是标准库中FileInputStream类的一个构造函数的声明。(有关输入和输出的更多信息,请参阅第二卷第1章。)
public FileInputStream(String name) throws FileNotFoundException
声明说,此构造函数从String
参数生成一个FileInputStream
对象,但它也可以通过引发FileNotFoundException
以特殊方式出错。如果此糟糕的状态应该发生,则构造函数调用将不会初始化新的FileInputStream
对象,而是抛出FileNotFoundException
类的对象。如果是这样,运行时系统将开始搜索知道如何处理FileNotFoundException
对象的异常处理程序。
当您编写自己的方法时,您不必公布您的方法实际可能抛出的每个可能被丢弃的对象。要理解您必须在编写方法的throws
子句中进行广告的时间(和内容),请记住在以下四种情况中的任何一种情况下都会引发异常:
- 您调用一个方法来引发一个检查异常,例如
FileInputStream
构造函数。 - 您检测到一个错误,并使用
throw
语句抛出一个检查异常(我们将在下一节中介绍throw
语句)。 - 您会产生编程错误,例如导致未检查异常(在本例中为
ArrayIndexOutofBoundsException
)的a[-1]=0。 - 虚拟机或运行时库中发生内部错误。
如果前两个场景中的任何一个发生,您必须告诉将使用您的方法的程序员异常的可能性。为什么?任何抛出异常的方法都是一个潜在的死亡陷阱。如果没有处理程序捕获异常,则当前执行线程将终止。
与提供的类的一部分一样,Java方法声明您的方法可能在方法头中抛出异常,并使用异常规范。
class MyAnimation
{
. . .
public Image loadImage(String s) throws IOException
{
. . .
}
}
如果一个方法可能引发多个选中的异常类型,则必须在头中列出所有异常类。用逗号分隔它们,如下例所示:
class MyAnimation
{
. . .
public Image loadImage(String s) throws FileNotFoundException, EO
{
. . .
}
}
但是,不需要为内部Java错误做广告,也就是说,从Error
继承的异常。任何代码都可能抛出这些异常,它们完全超出您的控制范围。
同样,不应公布继承自RuntimeException
的未检查异常。
class MyAnimation
{
. . .
void drawImage(int i) throws ArrayIndexOutOfBoundsException // ba
{
. . .
}
}
这些运行时错误完全由您控制。如果您如此关注数组索引错误,那么应该花时间修复它们,而不是宣传它们可能发生的可能性。
总之,一个方法必须声明它可能抛出的所有已检查异常。未检查的异常超出了您的控制范围(Error
),或者是由最初不允许的条件(RuntimeException
)导致的。如果方法未能如实声明所有检查的异常,编译器将发出错误消息。
当然,正如您在许多示例中看到的那样,您也可以捕获异常,而不是声明异常。这样就不会从方法中抛出异常,也不需要抛出规范。您将在本章后面看到如何决定是捕获异常,还是让其他人捕获异常。
小心
如果从超类重写方法,则子类方法声明的检查异常不能比超类方法声明的异常更通用。(可以抛出更具体的异常,也可以不抛出子类方法中的任何异常。)特别是,如果超类方法根本没有抛出选中的异常,子类也不能。例如,如果重写
JComponent.paintComponent
,则paintComponent
方法不能抛出任何选中的异常,因为超类方法不会抛出任何异常。
当类中的一个方法声明它抛出了一个属于特定类的实例的异常时,它可以抛出该类或其任何子类的异常。例如,FileInputStream
构造函数可以声明它引发了IOException
。在这种情况下,您可能不知道它是什么类型的IOException
;它可以是一个普通的IOException
,也可以是各种子类(如FileNotFoundException
)之一的对象。
C++注意
throws
说明符与C++中的throw
说明符相同,但有一个重要的区别。在C++中,在运行时强制执行throw
说明符,而不是在编译时执行。也就是说,C++编译器不关注异常规范。但是,如果在一个不属于throw
列表的函数中抛出异常,则调用unexpected
函数,并且在默认情况下,程序终止。此外,在C++中,如果没有给出
throw
规范,函数可能抛出任何异常。在Java中,没有throws
说明符的方法可能根本不会抛出任何检查异常。
7.1.3 如何抛出异常
现在,假设您的代码中发生了可怕的事情。您有一个方法readData
,它在一个文件中读取,该文件的头承诺
Content-length: 1024
但你在733个字符后得到了文件的结尾。你可能会认为这种情况非常反常,以至于你想抛出一个异常。
您需要决定抛出什么异常类型。某种IOException
是个不错的选择。在使用Java API文档时,您会发现一个EOFExchange
,其描述是“在输入过程中意外地到达EOF的信号”。以下是你如何抛出的:
throw new EOFException();
或者,你也可以选择
var e = new EOFException();
throw e;
以下是所有这些元素的组合方式:
String readData(Scanner in) throws EOFException
{
. . .
while (. . .)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new EOFException();
}
. . .
}
return s;
}
EOFException
有一个接受字符串参数的第二个构造函数。您可以更仔细地描述异常情况,从而更好地利用这一点。
String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);
如您所见,如果一个现有的异常类对您有效,那么抛出异常很容易。在这种情况下:
- 查找适当的异常类。
- 构造该类的对象。
- 抛出它
一旦一个方法抛出异常,它就不会返回到其调用方。这意味着您不必担心伪造默认返回值或错误代码。
C++注意
抛出异常在C++和Java中是相同的,只有一个小的区别。在Java中,可以只抛出
Throwable
的子类对象。在C++中,可以抛出任何类型的值。
7.1.4 创建异常类
您的代码可能会遇到任何标准异常类都没有充分描述的问题。在这种情况下,很容易创建自己的异常类。只是从Exception
或从Exception
的子类(如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 FileFormatException();
}
. . .
}
return s;
}
java.lang.Throwable
1.0
Throwable()
构造一个没有详细消息的Throwable
对象。Throwable(String message)
使用指定的详细消息构造新的Throwable
对象。按照惯例,所有派生的异常类都支持一个默认构造函数和一个带有详细消息的构造函数。String getMessage()
获取Throwable
对象的详细消息。