软件构造 6-2 Error and Exception Handling

6.2 错误与异常处理

一. 异常的分类

  分为 ExceptionError,在类 java.lang.Throwable 的子类 java.lang.Exceptionjava.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. CheckedUnchecked Exception

  • Runntime Exception 以及 ErrorUncheck 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 进行捕获,但大多数时候是不需要的,也不应该这么做——这是掩耳盗铃,对发现的编程错误充耳不闻。


  分辨 UncheckedChecked 异常:当要决定是采用 Checked Exception 还是 Unchecked Exception 的时候,问一个问题:“如果这种异常一旦抛出, client 会做怎样的补救?”

  • 如果客户端可以通过其他的方法恢复异常,那么采用 Checked Exception
  • 如果客户端对出现的这种异常无能为力,那么采用 Unchecked Exception
  • 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。

  尽量使用 Unchecked Exception 来处理编程错误:因为 Unchecked Exception 不用使客户端代码显式的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。

  • 充分利用 Java API 中提供的丰富 Unchecked Exception ,如 NullPointerException , IllegalArgumentExceptionIllegalStateException等,使用这些标准的异常类而不需亲自创建新的异常类,使代码易于理解并避免过多消耗内存。

  如果 client 端对某种异常(如 SQLException)无能为力,可以把它转变为一个 Unchecked Exception ,程序被挂起并返回客户端异常信息。

try{
	..some code that throws SQLException
}catch(SQLException ex){
	throw new RuntimeException(ex);
}

  但实质上不同的程序员有不同的观点,关于该如何选择 CheckedUnchecked 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;

  对异常而言,通常 throwthrows 应该是一一对应的。


  利用 Exception 的构造函数,将发生错误的现场信息充分的传递给 client

String gripe = "Content length: " + len + ", Received: " + n;
throw new EOFException(gripe);

  对于 checked Exception

  • 找到一个能表达错误的 Exception 类,或者构造一个新的 Exception 类。
  • 构造 Exception 类的实例,将错误信息写入
  • 抛出它

  一旦抛出异常,方法不会再将控制权返回给调用它的 client ,因此也无需考虑返回错误代码

6. 创建 Exception

  如果 JDK 提供的 Exception 类无法充分描述你的程序发生的错误,可以创建自己的异常类。
  大多数时候只需从 ExceptionException 的子类(如 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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值