6.2 错误与异常处理
一. 异常的分类
分为 Exception
和 Error
,在类 java.lang.Throwable
的子类 java.lang.Exception
和 java.lang.Error
中。异常一般完善程序的健壮性。断言一般完善程序的正确性。
Error
:出现的错误不是由程序引起的,如内存不够用等外部环境引起的错误。内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束。Exception
:程序引起的错误。分为程序的本身错误(程序员引起的,RuntimeException
)以及调用时错误(其他类型的异常,指client端时如调用参数错误)。异常:你自己程序导致的问题,可以捕获、可以处理。
Error
的种类:
- 用户输入错误,但可以处理
- 设备错误
- 物理限制,如磁盘填满、耗尽内存等等
在大多数时候,程序员不需要实例化 Error
。
二. 典型的 Error
1. VirtualMachineError
虚拟机错误
OutOFMemoryError
:内存超出限定范围StackOverflowError
:栈溢出InternalError
2. LinkageError
链接不在系统文件中等等导致的链接错误
NoClassDefFoundError
三. 异常 Exception
处理
一般捕获的是 client
端的 Exception
。对于程序员自身的 Exception
捕获也无意义,应该尽量修复。
注意:若对程序自身的 Exception
进行捕获,可能是进行 debug
,但针对此使用 assert
更好。
1. Exception
异常:程序执行中的非正常事件,程序无法再按预想的流程执行。
当程序出现异常时,将错误信息传递给上层调用者,并报告“案发现场”的信息。对 Java
来说,这是除 return
之外的第二种退出途径。
需要注意的是,程序出现异常时,若找不到异常处理程序,整个系统完全退出。
异常处理程序 (路径) 不一定与正常程序分开,换言之,异常处理可以在正常执行程序中。看如下的程序例子:
FileInputStream fIn = new FileInputStream(fileName)
if (fIn == null) {
switch (errno)
case _ENOFILE:
System.err.println("File not found: " + ...);
return -1;
default:
System.err.println("Something else bad happened: " + ...);
return -1;
}
}
DataInput dataInput = new DataInputStream(fIn);
if (dataInput == null)
System.err.println("Unknown internal error.");
return -1; // errno > 0 set by new DataInputStream
}
int i = dataInput.readInt();
if (errno > 0)
System.err.println("Error reading binary data from file");
return -1;
}
return i;
但这种方式与职责分配原则(单一职责)相悖。
FileInputStream fileInput = null;
try {
fileInput = new FileInputStream(fileName);
DataInput dataInput = new DataInputStream(fileInput);
return dataInput.readInt();
}
catch (FileNotFoundException e) {
System.out.println("Could not open file " + fileName);
}
catch (IOException e) {
System.out.println("Couldn’t read file: " + e);
}
finally {
if (fileInput != null) fileInput.close();
}
2. Exception
的分类
RuntimeException
(运行时异常):与Error
都是Unchecked Exception
,即程序无需显式捕获,由程序员在代码里处理不当、程序源代码引入的故障造成的- 其他
Exception
:要求明确的捕获,否则编译器报错 ,由程序员无法控制的外部原因造成
3. Checked
和 Unchecked
Exception
Runntime Exception
以及Error
是Uncheck Exception
。可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug
,类似于编程语言中的dynamic type checking
,不需要在编译的时候用try…catch
等机制处理。处理signal bugs
(unexcepted failure
)- 其他
Exception
属于Checked Exception
。必须捕获并指定错误处理器handler
,否则编译无法通过。类似于编程语言中的static type checking
。处理special results
(i.e., anticipated situations
)。
编译器可帮助检查你的程序是否已抛出或处理了可能的异常(Unchecked Exception
) 。
public class NullPointerExceptionExample {
public static void main(String args[]) {
String str = null;
System.out.println(str.trim());
}
}
//Exception in thread "main" java.lang.NullPointerException
public class ArrayIndexOutOfBoundExceptionExample {
public static void main(String args[]) {
String strArray[]={"Arpit","John","Martin"};
System.out.println(strArray[4]);
}
}
//Exception in thread " java.lang.ArrayIndexOutOfBoundsException: 4
ArrayIndexOutOfBoundsException
:数组越界NullPointerException
:空指针引用NumberFormatException
:数值转换ClassCastException
:类型转换
在编程和编译的时候, IDE
与编译器均不会给出任何错误提示。
try
探测是否有异常catch
异常出现后的异常处理路径finally
无论正确路径还是异常路径都得执行的语句throws
抛出异常的方式,用于定义异常。如声明“本方法可能会发生 XX 异常”throw
抛出异常的方式,抛出具体异常
public static void main(String args[]) {
FileInputStream fis = null;
try {
fis = new FileInputStream("sample txt");
int c;
while((c = fis.read()) != -1)
System.out.print((char) c);
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
或者方法加入 throws
使得上一层程序需要对该异常进行处理:
public static void main(String args[]) throws IOException {
FileInputStream fis = null;
fis = new FileInputStream("sample txt");
int k;
while ((k = fis.read()) != -1)
System.out.print((char) k);
fis.close();
}
对于 Unchecked Exception
也可以使用 throws
声明或 try/catch
进行捕获,但大多数时候是不需要的,也不应该这么做——这是掩耳盗铃,对发现的编程错误充耳不闻。
分辨 Unchecked
和 Checked
异常:当要决定是采用 Checked Exception
还是 Unchecked Exception
的时候,问一个问题:“如果这种异常一旦抛出, client
会做怎样的补救?”
- 如果客户端可以通过其他的方法恢复异常,那么采用
Checked Exception
- 如果客户端对出现的这种异常无能为力,那么采用
Unchecked Exception
- 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。
尽量使用 Unchecked Exception
来处理编程错误:因为 Unchecked Exception
不用使客户端代码显式的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。
- 充分利用
Java API
中提供的丰富Unchecked Exception
,如NullPointerException
,IllegalArgumentException
和IllegalStateException
等,使用这些标准的异常类而不需亲自创建新的异常类,使代码易于理解并避免过多消耗内存。
如果 client
端对某种异常(如 SQLException
)无能为力,可以把它转变为一个 Unchecked Exception
,程序被挂起并返回客户端异常信息。
try{
..some code that throws SQLException
}catch(SQLException ex){
throw new RuntimeException(ex);
}
但实质上不同的程序员有不同的观点,关于该如何选择 Checked
和 Unchecked Exception
,在技术人员中存有很大的争论。
对于 Checked Exception
:
不要创建没有意义的异常, client
应该从 Checked Exception
中获取更有价值的信息(案发现场具体是什么样子),利用异常返回的信息来明确操作失败的原因。
但如果 client
仅仅想看到异常信息,可以简单抛出一个 Unchecked Exception
(对于Runtime Exception
)。
throw new RuntimeException ("Username already taken");
于是 Checked Exception
应该让客户端从中得到丰富的信息。
要想让代码更加易读,倾向于用 Unchecked Exception
来处理程序中的错误。
错误可预料,但无法预防(脱离了你的程序的控制范围),但可以有手段从中恢复,此时使用 Checked Exception
。
如果做不到这一点,则使用 Unchecked Exception
。
对于 Checked Exception
,为了提高程序健壮性,应该显式捕获进行处理。如:如果读文件的时候发现文件不存在了,可以让用户选择其他文件;但是如果调用某方法时传入了错误的参数,则无论如何都无法在不中止执行的前提下进行恢复。
4. throws
“异常”也是方法和 client
端之间 spec
的一部分,在 post condition
中刻画:
public FileInputStream (String name) throws FileNotFoundException
强制了程序处理或抛出异常。
Java
程序都不会显式地通过 throws
语句抛出 Runtime Exception
。
程序员必须在方法的 spec
中明确写清本方法会抛出的所有 Checked Exception
以便于调用该方法的 client
加以处理
/**
* Compute the integer square root.
* @param x value to take square root of
* @return square root of x
* @throws NotPerfectSquareException if x is not a perfect square
*/
int integerSquareRoot(int x) throws NotPerfectSquareException;
需要注意的是,Unchecked Exception
也可声明:
/**
* @param lst list of strings to convert to lower case
* @return new list lst' where lst'[i] is lst[i] converted to lowercase
*/
static List<String> toLowerCase(List<String> lst)
但上例并没有写出 throws NullPointerException
。若写出该语句,编译器虽然不会报错,但程序将变成 bad smell
。
方法可抛出多个异常:
class MyAnimation {
. . .
public Image loadImage(String s)
throws FileNotFoundException, EOFException
{
. . .
}
}
throws
的异常:
- 你所调用的其他函数抛出了一个
Checked Exception
——从其他函数传来的异常 - 当前方法检测到错误并使用
throws
抛出了一个Checked Exception
——你自己造出的异常
此时需要告知你的 client
需要处理这些异常,如果没有 handler
来处理被抛出的 Checked Exception
,程序就终止执行。
LSP
原则(目标是子类型多态:客户端可用统一的方式处理不同类型的对象,子类型可替代父类型):
- 如果子类型中
override
了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛 - 子类型方法可以抛出更具体的异常,也可以不抛出任何异常
- 如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。
下例子类型异常应改为比父类更不宽泛的类型。
public class Test {
public boolean readFromFile() throws FileNotFoundException {
...
}
}
class SubType extends Test {
@Override
public boolean readFromFile() throws IOException {
...
}
}
5. throw
抛出异常的方式:
throw new EOFException()
或
EOFException e = new EOFException();
throw e;
对异常而言,通常 throw
与 throws
应该是一一对应的。
利用 Exception
的构造函数,将发生错误的现场信息充分的传递给 client
。
String gripe = "Content length: " + len + ", Received: " + n;
throw new EOFException(gripe);
对于 checked Exception
,
- 找到一个能表达错误的
Exception
类,或者构造一个新的Exception
类。 - 构造
Exception
类的实例,将错误信息写入 - 抛出它
一旦抛出异常,方法不会再将控制权返回给调用它的 client
,因此也无需考虑返回错误代码
6. 创建 Exception
类
如果 JDK
提供的 Exception
类无法充分描述你的程序发生的错误,可以创建自己的异常类。
大多数时候只需从 Exception
或 Exception
的子类(如 IOException
)派生它。
public class FooException extends Exception {
public FooException () { super(); }
public FooException (String message) { super(message); }
public FooException (String message, Throwable cause) {
super(message, cause);
}
public FooException (Throwable cause) { super(cause); }
}
调用此方法的代码必须处理或传播此异常(或两者兼有)。
有时,在某些情况下,您不希望强制每个方法在抛出子句中声明异常实现。在这种情况下,您可以创建一个扩展 java.lang.RuntimeException
的未检查异常(其目的一般是校验)。方法直接可以抛出或传播这种继承了(或就是) RuntimeException
,而无需声明它。
但当程序 release
出去时,不应该有这种 RuntimeException
。
包含更多“案发现场信息”的异常类定义和辅助函数——抛出异常的时候,将现场信息记入异常——在异常处理时,利用这些信息给用户更有价值的帮助。
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
...
double needs = amount - balance;
throw new InsufficientFundsException(needs);
...
try{ ...
}catch(InsufficientFundsException e){
System.out.println(“Money is short for "+ e.getAmount());
}
7. catch
异常
异常发生后,如果找不到处理器,就终止执行程序,在控制台打印出 stack trace
。
对于异常要么处理,要么不在本方法内处理,而是传递给调用方,由 client
处理(“推卸责任”)。
总之:
- 尽量在自己这里处理,实在不行就往上传 要承担责任!
- 但有些时候自己不知道如何处理,那么提醒上家,由
client
自己处理
- 如果父类型中的方法没有抛出异常,那么子类型中的方法必须捕获所有的
checked exception
。 - 子类型方法中不能抛出比父类型方法更多的异常!
使用 e.getMessage()
获取信息 ,使用 e.getMessageName
获取异常的具体类型。
8.重抛与链接异常
本来 catch
语句下面是用来做 exception handling
的,但也可以在 catch
里抛出异常。
这么做的目的是:更改 exception
的类型,更方便 client
端获取错误信息并处理。
try {
//access the database
}
catch (SQLException e) {
throw new ServletException ("database error: " + e.getMessage());
}
但这么做的时候最好保留“根原因”。
try {
//access the database
}
catch (SQLException e) {
Throwable se = new ServletException ("database error");
se.initCause(e);//根原因异常记录
throw se;
}
再通过语句
Throwable e = se.getCause();
得到根原因/根异常具体的信息。
9. finally
当异常抛出时,方法中正常执行的代码被终止。
如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。
无论是否发生异常,finally
中的语句必须执行。
可以只有 try...finally
。有异常未被捕获时,先执行 finally
然后返回控制台(或上一级)。
以下程序执行结果为 false
:
class Indecisive {
public static void main(String[] args) {
System.out.println(decision());
}
static boolean decision() {
try {
return true;
} finally {
return false;
}
}
}
10. Try-with-Resources
(TWR
)
也可以使用以下语句关闭资源:
try (Resource res = . . .) {
//work with res
}
try
无论是否抛出异常,均会关闭 try (...)
中的资源(自动执行 res.close()
)。
try
可以带有多个资源:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF 8");
PrintWriter out = new PrintWriter("out.txt")){
while (in.hasNext())
out.println(in.next().toUpperCase());
}
11. 堆栈跟踪元素分析
调用栈——先进后出。异常显示——JVM
弹栈,直到能够处理,否则最后直接程序中止。
可以使用两种方法打印调用栈信息,一种使用 printStackTrace()
函数,另一种使用数组进行分析。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
//analyze frame