疯狂Java讲义-异常处理

异常处理

本章思维导图

异常处理

Java的异常处理机制主要依赖于try、catch、finally、throw和throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。

Java7进一步增强了异常处理机制的功能,包括带资源的try语句、捕获多异常的catch两个新功能。

Java将异常分为两种,Checked异常Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。Checked异常可以提醒程序员需要处理所有可能发生的异常。

异常处理机制

Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

使用try…catch捕获异常

Java异常处理机制的语法结构。

try {
    // 业务实现代码
    ...
} catch (Exception e) {
    // 错误处理代码
    ...
}

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。

当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块则把异常对象交给该catch块处理,这个过程称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此退出。

异常类的继承体系

异常类之间的继承关系

每个catch块都是专门用于处理该异常类及其子类的异常实例。

当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。

try块后的花括号({…})不可以省略,catch块后的花括号({…})也不可以省略。try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。

Java把所有的非正常情况分为两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,这将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

Java运行时的异常处理逻辑可能有如下几种情形。

  • 数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理异常。
  • 数字格式异常,Java运行时将调用NumberFormatException对应的catch块处理异常。
  • 算术异常(例除0异常),Java运行时将调用AritheticException对应的catch块处理该异常。
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch块处理该异常。
  • 当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常,Java运行将会调用对应的catch块来处理该异常。

总是把对应Exception类的catch块放在最后,实际上,进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,即先处理小异常,再处理大异常,否则将出现编译错误。

Java7新增的多异常捕获

在Java7以前,每个catch块只能捕获一种类型的异常;但从Java7开始,一个catch块可以捕获多种类型的异常。

使用一个catch块捕获多种类型的异常时需要注意如下两个地方。

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
public class MultiExceptionTest {
	public static void main(String[] args) {
		try {
			int a = Integer.parseInt(args[0]);
			int b = Integer.parseInt(args[1]);
			int c = a / b;
			System.out.println("您输入的两个数相除的结果是: " + c);
		} catch(IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie) {
			System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
			// 捕获多异常时,异常变量默认有final修饰
			// 所以下面代码有错
			ie = new ArithmeticException("test");
		}
	}
}

访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

所有的异常对象都包含了如下几个常用方法。

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。
public class AccessExceptionMsg {
	public static void main(String[] args) {
		try {
			FileInputStream fis = new FileInputStream("a.txt");
		} catch (IOException ioe) {
			System.out.println(ioe.getMessage());
			ioe.printStackTrace();
		}
	}
}

程序将输出:

a.txt (系统找不到指定的文件。)
java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
	at exception_handling.mechanism.AccessExceptionMsg.main(AccessExceptionMsg.java:10)

使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。

Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现了异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行return语句,finally块总被执行。完整的Java异常处理语法结构如下:

try {
    // 业务实现代码
    ...
} catch (SubExceotion e) {
    // 异常处理块1
    ...
} catch (SubException2 e) {
    // 异常处理快2
    ...
} ... finally {
    // 资源回收
    ...
}

异常处理语法结构中只有try是必需的,catch块和fianlly块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有的catch块之后。

除非在try块、catch块中调用了退出虚拟机的方法(System.exit()),否则不管在try块、catch块执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。

在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。

当Java程序执行try块、catch块时遇到了return或throws语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throws语句,方法终止;如果有finally块,系统立即开始执行finally块——只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throws语句;如果finally块里也使用了return或throws等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。

异常处理的嵌套

在try块、catch块或在finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。

异常处理代码流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程即可放在try块里,也可放在catch块里,还可放在finally块里。

异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理。

Java9增强的自动关闭资源的try语句

Java7增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源是指那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。

为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseableCloseable接口,实现这两个接口就必须实现close()方法。

Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常。

自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。

public class AutoCloseTest {
	public static void main(String[] args) throws IOException{
		try(
				// 声明、初始化两个可关闭的资源
				// try语句会自动关闭这两个资源
				BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
				PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) {
			// 使用两个资源
			System.out.println(br.readLine());
			ps.println("庄生晓梦迷蝴蝶");
		}
	}
}

Java7几乎把所有的“资源类”(包括文件IO的各种类、JDBC变成的Connection、Statement等接口)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。

如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。

Java9再次增强了这种try语句,Java9要求不在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或是有效的final(effectively final),Java9允许将资源变量放在try后的圆括号内。上面程序在Java9中可改写为如下形式。

public class AutoCloseTest2 {
	public static void main(String[] args) throws IOException {
		// 有final修饰的资源
		final BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
		// 没有显式使用final修饰,但只要不对该变量重新赋值,该变量就是有效的final
		PrintStream ps = new PrintStream(new FileOutputStream("a.txt"));
		// 只要将两个资源放在try后的圆括号内即可
		try (br; ps) {
			// 使用两个资源
			System.out.println(br.readLine());
			ps.println("庄生晓梦迷蝴蝶");
		}
	}
}

Checked异常和Runtime异常体系

Java的异常被分为两大类:Checked异常Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。

Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。

对于Checked异常的处理方式有如下两种。

  • 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后在对应的catch块中修复该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现。

使用throws声明抛出异常

使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行。

throws声明抛出只能在方法签名中使用,throws可以声明多个异常类,多个异常类之间以逗号隔开。throws声明抛出的语法格式如下:

throws ExceptionClass1, ExceptionClass2...

throws声明抛出的语法格式仅跟在方法签名之后,一旦使用throws语句声明抛出该异常,程序就无须使用try…catch块来捕获异常了。

如果某段代码中调用了一个带throws声明的方法,该方法抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。

方法重写时声明抛出异常的限制

使用throws声明抛出异常时有一个限制,就是方法重写时”两小“中的一条规则:子类方法声明抛出的异常类型应该的父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

使用Checked异常至少存在如下两大不便之处。

  • 对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出该异常。这样就增加了编程的复杂度。
  • 如果在方法中显式声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

在大部分时候推荐使用Runtime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。

当使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。

如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以使用try…catch块来捕获Runtime异常。

使用Runtime异常是比较省事的方式,使用这种方式既可以享受”正常代码和错误处理代码分离“,”保证程序具有较好的健壮性“的优势,又可以避免因为使用Checked异常带来的编程繁琐性。

但Checked异常也有优势——Checked异常能在编译时提醒程序云代码可能存在的问题,提醒程序员必须注意处理该异常,或者声明该异常由该方法调用者来处理。

使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,使用throw语句来完成。

抛出异常

如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:

throw ExceptionInstance;

当Java运行时接收到自行抛出的异常时,同样会中止当前的执行流,跳到对应的catch块,由该catch块来处理异常。也就是说,不管是系统自动抛出的异常,还是手动抛出的异常,Java运行时环境对异常的处理没有任何差别。

如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throws声明抛出的方法中;程序既可以显式使用try…catch块来捕获并处理该异常,也可以完全不理会该异常,把异常交给该方法的调用者处理。

自定义异常类

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。

public class AuctionException extends Exception {
	// 无参数的构造器
	public AuctionException() {
	}
	// 带一个字符串参数的构造器
	public AuctionException(String msg) {
		super(msg);
	}
}

如果需要自定义Runtime异常,只需将Exception基类改为RuntimeException基类。

catch和throw同时使用

前面介绍处理异常的方式有两种。

  • 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。
  • 该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。

在实际应用中往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常。

为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。如下例子程序示范了这种catch和throw同时使用的方法。

public class AuctionTest {
	private double initPrice = 30.0;
	// 因为该方法中显式抛出了AuctionException异常
	// 所以此处需要声明抛出AuctionException异常
	public void bid(String bidPrice) throws AuctionException {
		double d = 0.0;
		try {
			d = Double.parseDouble(bidPrice);
		} catch (Exception e) {
			// 此处完成本方法中可以对异常执行的修复处理
			// 此处仅仅是在控制台打印异常的跟踪栈信息
			e.printStackTrace();
			// 再次抛出自定义异常
			throw new AuctionException("竞拍价必须是数子,不能包含其他字符!");
		}
		if (initPrice > d) {
			throw new AuctionException("竞拍价比起拍价低,不允许竞拍!");
		}
		initPrice = d;
	}
	public static void main(String[] args) {
		AuctionTest at = new AuctionTest();
		try {
			at.bid("df");
		} catch (AuctionException ae) {
			// 再次捕获到bid()方法中的异常,并对该异常进行处理
			System.out.println(ae.getMessage());
		}
	}
}

Java7增强的throw语句

try {
    new FileOutputStream("a.txt");
} catch (Excetpion ex) {
    ex.printStackTrace();
    throw ex;
}

程序捕获该异常时,声明该异常类型为Exception;但实际上try块中可能只抛出了FileNotFoundException异常。

在Java7以前,Java编译器的处理“简单而粗暴”——由于在捕获该异常时声明ex类型是Exception,因此Java编译器认为这段代码可能抛出Exception异常,所以包含这段代码的方法通常需要声明抛出Exception异常。里如下代码。

public class ThrowTest2 {
	public static void main(String[] args)
			// Java6 认为可能抛出Exception异常
			// 所以此处声明抛出Exception异常
			throws Exception {
		try {
			new FileOutputStream("a.txt");
		} catch (Exception ex) {
			ex.printStackTrace();
			throw ex;
		}
	}
}

从Java7开始,Java编译器会执行更细致的检查,Java编译器会检查throw语句抛出异常的实际类型,这样编译器将知道try块里抛出的异常类型,无须在方法签名中对应catch块的抛出的异常。即可将上面代码改为如下代码。

public class ThrowTest2 {
	public static void main(String[] args)
			// Java7 会检查此处可能抛出的异常的实际类型
			// 因此此处只需要抛出FileNotFoundException异常即可
			throws FileNotFoundException {
		try {
			new FileOutputStream("a.txt");
		} catch (Exception ex) {
			ex.printStackTrace();
			throw ex;
		}
	}
}

异常链

分层结构示意图

对于真实的企业级应用而言,常常由严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。

当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,有如下两个原因。

  • 对于正常用户而言,他们不想看到底层的SQLException异常,SQLException异常对他们使用该系统没有任何帮助。
  • 对于恶意用户而言,将SQLException异常暴露出来不安全。

把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。

把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。

这种把捕获一个异常然后接着抛出一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式:职责链模式),也被称为“异常链”。

在JDK1.4以前,必须自己编写代码来保持原始异常信息。从JDK1.4以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也可以通过这个异常链追踪到异常最初发生的位置。

从JDK1.4以后,Throwable基类已有了一个可以接收Exception参数的方法,所以可以采用如下代码来自定义异常类。

public class SalException extends Exception {
	public SalException() {
	}
	public SalException(String msg) {
		super(msg);
	}
	// 创建一个可以接收Throwable参数的构造器
	public SalException(Throwable t) {
		super(t);
	}
}

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。有如下代码。

class SelException extends RuntimeException {
	SelException() {
	}

	SelException(String msg) {
		super(msg);
	}
}

public class PrintStackTraceTest {

	public static void main(String[] args) {
		firstMethod();
	}

	private static void firstMethod() {
		secondMethod();
	}

	private static void secondMethod() {
		thirdMethod();
	}

	private static void thirdMethod() {
		throw new SelException("自定义异常信息");
	}

}

上面程序中main方法调用firstMethod,firstMethod调用secondMethod,secondMethod调用thirdMethod,thirdMethod直接抛出一个SelException异常。运行程序将看到输出结果为如下所示。

Exception in thread "main" SelException: 自定义异常信息
	at exception_handling.trace_stack.PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:27)
	at exception_handling.trace_stack.PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:23)
	at exception_handling.trace_stack.PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:19)
	at exception_handling.trace_stack.PrintStackTraceTest.main(PrintStackTraceTest.java:15)

异常从thirdMethod方法开始触发,传到secondMethod方法,再传到firstMethod方法,最后传到main方法,再main方法终止,这个过程就是Java的异常跟踪栈。

面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新的异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者······直至最后传给main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。

虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。

异常的处理规则

成功的异常处理应该实现如下4个目标。

  • 使程序代码混乱最小化。
  • 捕获并保留诊断信息。
  • 通知合适的人员。
  • 采用合适的方式结束异常活动。

不要过度使用异常

不可否认,Java的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要有两个方面。

  • 把异常和普通错误混淆在一起,不在编写任何错误处理代码,而是以简单地抛出异常来代替所以的错误处理。
  • 使用异常处理来代替流程控制。

熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地抛出异常。实际上这样做是不对的,对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有针对外部的、不能确定和预知的运行时错误才使用异常。

异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。

异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。

不要使用过于庞大的try块

try块过于庞大时,就难免在try块后紧跟大量的catch块才可以针对不同的异常提供不同的处理逻辑。同一个try块后紧跟大量的catch块则需要分析它们之间的逻辑关系,反而增加了编程复杂度。

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。

避免使用Catch All语句

所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。例如下代码片段。

try {
    // 可能引发Checked异常的代码
} catch (Throwable t) {
    // 进行异常处理
    t.printStackTrace();
}

这种处理方式有如下两点不足之处。

  • 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
  • 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现一些“关键”异常,那么异常也会被“静悄悄”地忽略。

不要忽略捕获到的异常

既然已经捕获到异常,那catch块理应做些有用的事情——处理并修复这个错误。catch整个块为空,或者仅仅打印出错误信息都是不妥的。

对异常采取适当措施,比如:

  • 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续执行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作······总之,对于Checked异常,程序应该尽量修复。
  • 重新抛出新的异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
  • 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。
发布了32 篇原创文章 · 获赞 18 · 访问量 3248

猜你喜欢

转载自blog.csdn.net/u011714517/article/details/103847207
今日推荐