Java中异常关键字throw和throws使用方式的理解

Java中应用程序在非正常的情况下停止运行主要包含两种方式: Error 和 Exception ,像我们熟知的 OutOfMemoryError 和 IndexOutOfBoundsException 等。在日常的开发过程中 Error 我们是不用处理的,一旦 Error 发生必然证明应用程序代码上出现了问题,这个时候我们只能是修改代码。而 Exception 则是在程序运行的过程中可以进行捕获并处理的。接下来的所有讨论均会以 Exception 为主。

异常的继承图

所有的异常均继承在 Exception 类,而在其子类中又分为运行时异常RuntimeException 和 其他的异常类。所谓的运行时异常指的是在运行的时候所抛出的异常,例如:IndexOutOfBoundsException 就是 RuntimeException 的子类,访问数组下标范围外的元素,在代码的编译时期是无法被检测到的,只有在运行的时候才会抛出这样的异常。其他的异常则是 Exception 的直接子类,这些异常也可以称为编译时异常,编译时异常虽然是运行时被抛出的,但是在代码编写的过程中就会知道某些代码可能会抛出异常,并需要对其作出反应:处理或者是继续向上一层调用栈抛出。他们的主要区别可以用一句话来总结:编译时期可以发现并且需要对其作出反应的异常为编译时异常,在编译时期不能发现的异常叫运行时异常。当运行时异常发生的时候,也意味着我们是需要调整我们的代码的。在下面讨论 throws 关键字的时候会详细说明。

throw 异常的可抛性

所有的异常均可以使用 throw 关键字主动抛出,但 throw 并不能抛出一切东西,例如我们无法使用 throw 抛出一个数组或者是一个 Interger 实例。异常具有可抛性是因为 Exception 的父类是 Throwable ,只有继承它,才具有可抛性。这一点作为了解就可以了,在Java内部已经为我们定义了很多的异常,如果感觉内置的异常无法满足要求,我们也可以通过继承自`Exception 或 RuntimeExcetpion 来扩展异常。

throws 异常声明

上面我们讲过了异常的分类,包含了编译时异常和运行时异常。但是在Java中异常是需要声明的,当我们使用 throw 进行主动抛出异常时,需要在方法的后面加上 throws 关键字和异常的名字进行声明,否则代码无法正常编译。

public void  getName() throws CustomException1,CustomException2 .... {  //方法体 }

在这种情况下,如果别人调用我们的代码或者是我们调用其他人有类似声明的代码(例如:SimpleDateFormat 的 Parse方法)时,调用者就要对其作出反应,处理或是在调用者的方法体上继续声明,从而告诉上一层调动栈去处理,如果所有的调用栈均不处理,那么最后只能由JVM进行兜底。对于使用throws关键字进行声明的异常均为编译时异常,运行时异常在编译时无法感知,所以不需要进行异常声明。另在异常声明的时,声明的异常类型可以是抛出异常的名字或者是该异常父类的名字。

public void getName() throws Exception{
     throw new CustomException1("自定义Exception异常");
}

异常的捕获

当调用异常声明的方法时,需要对其作出反应,Java提供了 try .... catch ....finally 关键字,try 中的代码块主要是用于作为异常检测,catch 用于捕获检测到的异常并处理,而 finally 则是必须执行的代码块。下面我看看一个例子

// 方法的声明
public void getName() throws CustomException { // 方法体 }

// 调用体
try { getName(); }
catch(CustomException  ex){ // 处理异常 }
finally{ // 必须执行的代码 }

代码 finally 体内的代码块,不管异常能不能被捕获到,都是会执行的,通常用于做资源的释放操作。上述的代码只是一个简单捕获的例子,如果 getName 方法抛出多个异常 throws CustomException1, CusomException2 , Exception ,那又该如果捕获呢?在Java中是支持多个catch 捕获异常的,当异常的种类是同级时,多 catch 的编写顺序可以使任意的,但如果异常的声明中包含非同级时( 例如包含其父类异常 ) 编写多 catch 异常捕获的时候,需要在最后写父类捕获代码。

try { getName(); }
catch(CustomException2  ex){ // 处理异常 }
catch(CustomException1  ex){ // 处理异常 }
catch(Exception ex){ // 处理异常 }
finally{ //必须执行的代码 }

方法重写时的异常处理

由于Java的编译时异常需要进行声明,Java又是面向对象编程语言,存在继承和多态特性,随之而来的就会产生对存在这种关系的方法体异常的声明该如何定义?下面我们直接来说结论

  1. 当父类中对方法进行异常声明,那么子类对该方法进行覆盖时,可以进行声明也可以不进行声明,当进行声明的时候,声明的异常在继承关系上只能是该异常或者是它的子类。
  2. 当父类中对方法没有进行异常声明时,那么子类对该方法进行覆盖时,也不可以进行异常声明,如果子类中的方法调用了带有异常声明的方法,由于这个限制,只能在方法中进行 try....catch 处理

下面我们一起看下几个场景:

父类中的方法声明异常

public class Demo {
	public static void main(String[] args) throws CustomException {
		Person  p = new Student();
		p.GetName();
	}
}

class Person{
	public String GetName() throws CustomException { throw new CustomException("自定义异常"); }
}

class Student extends Person {
	public String GetName() throws CustomException2 , CustomException  
        {  
            if(true) {  throw new CustomException("自定义异常");  } 
            else { throw new CustomException2("自定义异常2");  }
        }
}

class CustomException extends Exception {
	public CustomException() {}
	
	public CustomException(String message) { super(message); }
}

class CustomException2 extends CustomException {
	public CustomException2() {}
	
	public CustomException2(String message) { super(message); }
}

class Student extends Person {
	public String GetName()  { // 方法体 }
}

父类中的方法未声明异常

class Person{
	public String GetName()  { // 方法体 }
}

class Student extends Person {
	public String GetName()  {  
            try { // 调用带有异常声明的方法 } 
            catch( ... ) { // 处理异常 } 
        }
}

我们来理解一下为什么会有这样的限制?其根源即使多态性,编译看父类,运行是子类,当代码编译的时候,编译器解析的是父类的方法,查看的是父类的异常声明。如果子类中的异常声明高于父类或者和父类同级,试想如果父类是CustomException,子类的声明是 Exception,那么在编译的时候,编译器会提示需要处理异常 CustomException,但是在运行的时候,抛出的确是Exception,这样对于异常的捕捉就能造成很大的困扰,以至于达不到我们的预期。

总结

在本节中我们讲了Java中的异常,包含异常的分类,自定义异常,异常声明和捕捉以及在多态调用中异常的声明。在代码开发的过程中,我们应该在不同的场景中选择不同的异常使用。但是总体来看我们会使用运行时类型的异常会比较多,因为交付的软件预期内的问题我已经解决了,在软件运行的过程中产生的异常往往不在预期内,而且一旦发生就需要我们调整代码。

posted @ 2019-09-15 13:00  猿记ATALL  阅读(4293)  评论(0编辑  收藏  举报