异常处理机制和体系结构

        Java异常是指在程序运行过程中出现错误,从而影响程序流程的正常运行。而异常处理机制可以保证程序出现错误后,控制接下来的程序流程,是选择定位错误信息,还是抛出异常或捕获异常、还是避免程序非正常退出,都取决于我们。


异常体系结构(附网图一张)

                       

android全局的异常处理 java全局异常处理器的原理_异常

        通过上面的异常体系结构图可以清晰看出Java把所有异常都当做对象来处理,所有异常的基类是Throwable类(可抛出),两大子类分别是Error和Exception,Error异常表示程序出现了十分严重、不可恢复的错误,例如Java虚拟机出现错误,这种情况仅凭程序自身是无法处理的,在程序中也不会对Error异常进行捕捉和抛出。Exception又分为RuntimeException(未检查异常)和CheckedException(检查异常),两者区别如下:

RuntimeException(运行时异常):

       只有在运行过程中出现错误,才会被检查的异常,是unchecked Exception,编译器不会对运行时异常进行检查和处理。但如果出现了运行时异常,则表明编程出现了错误,则应该找到错误并修改,而不是对其捕获。

CheckedException(检查异常,即非运行时异常):

       所有非RuntimeException且来自于Exception的异常都是检查异常,该异常会在程序运行之前被编译器检查,对于检查异常,必须对其进行处理,可以选择捕获或抛出。但应该遵循该原则:谁知情谁处理,谁负责谁处理,谁导致谁处理。



异常处理方式

try、catch、finally、throw、throws,关于它们的用法和注意点,会在下面一一介绍,并附带实例。

  • try、catch和finally都是不能单独使用的,只能像try-catch 或者 try-finally,或者三者一起使用try-catch-finally。
  • try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
  • finally语句块中的代码一定会被执行,常用于回收资源 。
  • throws:往上级调用者抛出一个异常。
  • throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。
//使用if-else来处理异常
public class Demo01 {
	public static void main(String[] args) {
		test(5, 0);	
	}	
	public static void test(int a, int b) {
		//处理除数为0的情况
		if(b == 0) {
			System.out.println("不能为0");//给出错误提示
			System.out.println("程序结束");
		}else {
			System.out.println("a / b = " + a / b);
		}
	}
}

     如果不使用异常处理机制,用if-else来处理异常,首先影响代码阅读,逻辑层次不清楚,容易把核心代码和异常处理代码搞混,还需要频繁进行逻辑判断,影响程序运行效率,当处理异常过多时,就显得过于鸡肋了,这时候就应该使用异常处理机制来解决。


1.try-catch语句块处理异常

public static void main(String[] args) {
		try {
			test(5, 0);
		}catch(Exception e) {//捕获异常
			System.out.println("传值错误,除数不能为0");
			System.out.println("程序结束");
			e.printStackTrace(); //打印异常信息
		}
	}	
	public static void test(int a, int b) {
		System.out.println("a / b = " + a / b);
	}

      比起通过if-else来处理异常的方式,通过try语句块把异常处理和核心代码分开,不仅可以提高可读性,还能通过try语句块来提升程度健壮性,提供更多功能。

     上面只是用到try-catch,那么finally语句的用法是怎样呢?实例如下:


2.try-catch-finally处理异常

public class Demo01 {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		try {
			int a = sc.nextInt(); //从键盘输入一个整型
			System.out.println(a);
		}catch(Exception e){
			System.out.println("输入类型不符");
			System.out.println("--catch语句执行完成--");
			throw e; //把捕获的异常再次抛出
		}finally {
			System.out.println("--finally语句被执行--");
		}
	}
}
//输入数字,没有引发异常,结果:
15
15
--finally语句被执行--

//输入字符,引发异常,结果:
abc
输入类型不符
--catch语句执行完成--
--finally语句被执行--
Exception in thread "main" java.util.InputMismatchException
//省略后续异常定位代码...

    上面执行结果表明了finally语句块无论是代码正常执行或是捕获到异常,finally语句块中的代码都会执行。但如果我们在try-catch-finally三个语句块中分别使用return,它们的执行顺序又是怎样呢?


3.try-catch-finally使用return的执行顺序

public class Demo02 {
	public static void main(String[] args) {
		System.out.println(Demo02.test());
	}
	
	public static String test() {
		Scanner sc = new Scanner(System.in);
		try {
			int a = sc.nextInt(); //从键盘输入一个整型
			System.out.println(a);
			return "try-return";
		}catch(Exception e){
			System.out.println("--catch语句执行--");
//			throw e; 抛出异常后,后面的代码不再执行。
			return "catch-return";
		}finally {
			System.out.println("--finally语句执行--");
			return "finally-return";
		}
	}
}
//输入数字,没有引发异常,结果:
20
20
--finally语句执行--
finally-return

//输入字符,引发异常,结果:
abc
--catch语句执行--
--finally语句执行--
finally-return

      结论先不急着给出,我们将finally语句块注释掉,再看看try-catch的return语句又是怎样的执行顺序?

//省略部分没必要的代码
	public static String test() {
		Scanner sc = new Scanner(System.in);
		try {
			int a = sc.nextInt(); //从键盘输入一个整型
			System.out.println(a);
			return "try-return";
		}catch(Exception e){
			System.out.println("--catch语句执行--");
//			throw e; 抛出异常后,后面的代码不再执行。
			return "catch-return";
		}/** 省略finally语句块 */
	}
//try语句块正常执行完成,结果:
10
10
try-return
//引发异常,结果:
abc
--catch语句执行--
catch-return


关于return语句在try-catch-finally语句块的执行顺序结论   

  1. 首先,finally语句块一定会执行,若finally存在return语句,则会优先于try和catch中的return语句执行,当执行了return,则try和catch中的return语句不再执行。
  2. 如果finally语句中没有return,按照正常流程执行,try或catch在执行return语句之前,会先执行finally中的代码,然后再反回去执行return。
  3. 如果没有finally语句,则try或catch就按照正常程序流程执行。
  4. 反过来想,当finally语句存在时,若try或catch执行了return,那finally语句就得不到执行,那finally语句就毫无作用,两者引起悖论。

finally遇见如下情况不会被执行


  • 在前面的代码中用了System.exit()退出程序。
  • finally语句块中发生了异常。
  • 程序所在的线程死亡。
  • 关闭CPU。



4.try-finally的应用场景

try-finally通常用来保证某些操作一定会被执行,例如关闭数据库资源、关闭锁等操作。

public static void main(String[] args) {
		Connection conn = null;
		PreparedStatement pst = null;
		ResultSet rs = null;
		try {
			conn = JDBCUtils.getConnection(); 
			pst = conn.prepareStatement("select * from user");
			rs = pst.executeQuery();
			//省略后续代码
		} catch (SQLException e) {
			e.printStackTrace();
		}finally {
			JDBCUtils.close(rs, pst, conn); //保证打开的资源一定会被关闭
		}
	}

        由于代码篇幅过多,所以上面封装了连接和关闭的代码没给出。

ReentrantLock lock = new ReentrantLock();
		try {
			//需要加锁的代码
		}finally {
			lock.unlock(); //保证锁一定被释放
		}

       使用Lock来保证资源安全时,就需要用到try-finally,如果出现错误,导致锁对象没法释放,那么线程就会出现阻塞,严重时甚至会造成死锁。



抛出异常:throw与throws

throw抛出异常:

      throw语句用于抛出异常,可以是Java已有异常,也可以是自定义异常,它只负责抛出,怎么处理和它无关。抛出之后,throw之后的代码不再执行。

throws声明异常:

      如果方法中有异常,但又无法处理,就可以使用throws来声明该方法中所有无法处理的异常,用于向上级调用者传递异常。如果有多个异常,则依次在方法签名后排列。

public void getLen(int length) 
			throws CertificateException, LastOwnerException {
		//随便瞎搞的两个异常,别太在意
		if(length < 5)
			throw new LastOwnerException();
		if(length > 10)
			throw new CertificateException();
	}

      throw抛出异常,但未被处理,所以throws对未处理的异常进行声明,交给调用者。例子如下:

public void getLen(int length) 
			throws CertificateException, LastOwnerException {
		//随便瞎搞的两个异常,别太在意
		if(length < 5)
			throw new LastOwnerException();
		if(length > 10)
			throw new CertificateException();
	}
	
	//无法处理,选择继续抛出
	void test1() throws CertificateException, LastOwnerException {
		getLen(5); 
	}
	//对异常进行处理
	void test2() {
		try {
			test1();
		} catch (CertificateException | LastOwnerException e) {
			e.printStackTrace();
		}
	}	
}

       最终对其进行捕获进行处理,如果继续往上抛,最后就是交给JVM来处理,但这样做非常不可取。对于异常,有能力处理就处理!上面同时捕获多个异常,触发其中一个异常都会进行统一处理,也可以对每一个异常进行分别捕捉处理。这需要根据你的要求来定。


声明异常和捕获异常应遵循的原则:

  1. 声明异常的第一位不要声明Exception异常,因为Exception是所有检查异常的父类,所以会忽略剩下的异常。
  2. 捕捉异常时第一位不要捕获Exception,应该放在最后面,保证前面的异常能被捕获到。
  3. 即父类异常要放在子类异常之后,避免异常匹配时被屏蔽。


自定义异常

      要明白一点:异常最有用的信息就是异常名,但有时候对于某些情况,Java自带异常不能很好地进行描述时,就可以使用自定义异常来进行准确描述。

一个简单的自定义异常:

public class LengthException extends Exception {
	public LengthException() {}
	public LengthException(String s) {
		super(s);
	}
	@Override
	public String getMessage() {
		return super.getMessage();
	}
}
public class Demo02 {
	public static void main(String[] args) {	
		try {
			test(3);
		} catch (LengthException e) {
			e.printStackTrace();
		}
	}
	public static void test(int len) throws LengthException {
		if(len < 5) throw new LengthException();
		System.out.println(len);
	}	
	//处理异常,结果:
	com.exceptions.test.LengthException
}

自定义异常就是一个普通类继承Exception类就可以,对于自定义异常,不需要太多功能,类名能准确描述问题是关键。



《Think in Java》中对异常的总结

  1. 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的值。
  5. 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
  6. 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。
  7. 终止程序。
  8. 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
  9. 让类库和程序更安全。