Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

java基础(八)之异常

2017-06-10
nivelle

异常

概念

使用异常所带来的一个相当的明显好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需要在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。

基本异常

当异常出现并抛出时,首先java会通过new 在堆上创建对象,然后,从当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。

  • 异常使得我们可以将每件事当做一个事务考虑,而异常可以看护着这些事务的底线。
  • 异常可以视为一种内建的恢复系统,因为我们在程序中可以拥有各种不同的恢复点。如果程序某部分失败了,异常将“恢复”到程序中某个已知的稳定点上。

异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。

异常允许我们强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并放回到稳定状态。

异常参数

所有标准异常类都有两个构造器:一个默认构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:

throw new NullPointerException("t=null");

在使用new创建了异常对象之后,此对象的引用将传给throw。尽管返回的异常对象其类型不同,但从效果看,就像从方法“返回”的。(1) 可以简单地把异常处理看成一种不同的返回机制(2)抛出异常的方式还能从当前域退出。这两种情况下,将返回一个异常对象,然后退出方法或作用域。


捕获异常

try块

如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以称为try块。它是跟在try关键字之后的普通程序块:

try{
    //code that might generate exception  
}

有了异常处理机制,可以把所有动作都放在try块里,然后只需在一个地方捕获所有异常。这使得代码更清晰,因为完成任务的代码没有与错误检查的代码混在一起。

异常处理程序

抛出的异常必须在某处得到处理。这个“地点”就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在try块之后,以关键字catch表示:

try{
    //code that might generate exception
}catch(Type1 id1){
    //handle exceptions of Type1
}catch(Type2 id2){
    //handle exceptions of Type2
}catch(Type3 id3){
    //handle exceptions of Type3
}

每个catch子句(异常处理程序)看起来就像是接收一个且仅接收一个特殊类型的参数的方法。

异常处理程序必须紧跟在try块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序程序的查找过程结束。只有匹配的catch子句才能得到执行。

终止与恢复

异常处理理论上有两种基本模型。

  • 终止模式:假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
  • 恢复模式:异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。想要用java实现类似恢复行为,那么在遇见错误时就不能抛出异常,而是调用修正方法来修正该错误。或者,把try块放在while循环里,这样就不断地进入try块,直到得到满意的结果。

创建自定义异常

要定义自己的异常类,必须从已有的异常类继承,最好的选择意思相近的异常类继承。建立新的异常类型最简单的方法就是ran编译器为你产生默认构造器,所以这几乎不用写多少代码:

class SimpleException extends Exception{}

public class InheritinExceptions{
    public void f() throws SimpleException{
        System.out.println("Throw SimpleException from f()");
        throw new SimpleException();
    }
}

public static void main(Stirn[] args){
    InheritinExceptions sed = InheritinExceptions();
    try{
        sed.f();
    }catch(SimpleException e){
        System.out.println("catch it!");
    }
    
}

在异常处理程序中,调用了在Throwable类中声明(Exception即从此类继承)的printStackTrace()方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。

异常说明

java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员,这是中优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在异常。

异常说明使用了附加的关键字throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:

void f() throws TooBig,TooSaml,DivZero{//}

代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,java在编译时就可以保证一定水平的异常正确性。

这种在编译时被强制检查的异常称为被检查的异常

捕获所有异常

可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类Exception,就可以做到这一点(事实上还有其他的基类,但Exception是同编程活动相关的基类):

catch(Exception e){
  System.out.println("Catch an exception");
}

这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。

因为Exception是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从其基类Throwable继承的方法:

String getMessage()
String getLocalizedMessage()


栈轨迹

printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构造的数组,其中每一个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用。 数组中的最后一个元素和栈底是调用序列中的第一个方法调用。

public class WhoCalled {
  static void f() {
    try {
      throw new Exception();
    } catch (Exception e) {
      for (StackTraceElement ste : e.getStackTrace()) {
        System.out.println(ste.getMethodName());
      }
    }
  }
  
  static void g(){f();};
  static void h(){g();};
  
  public static void main(String[] args) {
    f();
    System.out.println("---------------------------");
    g();
    System.out.println("---------------------------");
    h();
  }
}


结果:

f main \ ————————— f g main \ ————————— f g h main

重新抛出异常

有时希望重新抛出异常,尤其是在使用Exception捕获所有异常的时候。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出:

catch(Exception e){
  System.out.println("an exception was thrown");
  throw e;
}

** 重新抛异常会把异常抛给上一级环境中的异常处理程序,同一个try块后续catch子句将被忽略。**

如果只是把当前异常对象重新抛出,那么printStackTrace()方法将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

异常链

常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。现在所有Throwable的子类在构造器中都可以接受一个cause(由因)对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。

在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器。它们Error、Exception以及RuntimeException。如果要把其他类型的异常链连接起来,应该使用initCauser()方法而不是构造器。

Java标准异常

Throwable这个java类被用来表示任何可以作为异常被抛出的类。Throwadble可以分为两类:

  • Error用来表示编译时和系统时错误;
  • Exception是可以被抛出的基本类型,在java类库、用户方法以及运行时故障中都可能抛出Exception型异常。
RuntimeException

它属于java的标准运行时检测的一部分。如果对null引用进行调用,java会自动抛出NullPointerException异常,所以上述代码是多余的,尽管你也许想执行其他检查以确保NullPointerException不会出现。

属于运行时异常类型很多,它们会被java虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从RuntimeException类继承而来,所以既体现了继承的优点,使用起来同样简单。这构成了一组具有相同特征和行为的异常类型。并且,不需要在异常说明中声明方法将抛出RuntimeException类型的异常。它们也被称为“不受检查异常”。

务必记住:只能在代码中忽略RuntimeException类型的异常,其他类型的异常的处理都是由编译器强制实施的。究其原因,RuntimeException代表的是编程错误:

  • 无法预料的错误。比如从你控制范围之外传递进来的null引用

  • 应该在代码中进行检查的错误。在一个地方发生的异常,常常会在另一个地方导致错误。

使用finally进行清理

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。这通常适用内存回收之外的一些情况。为了达到这个效果,可以在异常处理处程序后面加上finall子句。

class ThreeException extends Exception {}

public class FinallyWorks {
  static int count = 0;

  public static void main(String args[]) {
    while (true) {
      try {
        if (count++ == 0){
          throw new ThreeException();         
        }
        System.out.println("noException");
      } catch (ThreeException e) {
        System.out.println("ThreeException");
      } finally {
        System.out.println("in finally clause");
        if (count == 2) break;
      }
    }
  }
}


结果:

ThreeException
in finally clause
noException
in finally clause

无论异常是否被抛出,finally子句总能被执行。

finally用来做什么

无论try块里发生了什么,内存总能得到释放。java有垃圾回收机制,所以内存一定可以释放。

当要把除内存之外的资源恢复到它们的初始状态,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形。

当涉及break和continue语句的时候,finally子句也会得到执行。如果把finally子句和带标签的break以及continue配合使用,在java里就没必要使用goto语句了。

在return中使用finally

public class MultipleReturns {
  public static void f(int i) {
    System.out.println("initialization that requires cleanup");
    try {
      System.out.println("point1");
      if (i == 1)
        return;
      System.out.println("point2");
      if (i == 2)
        return;
      System.out.println("point3");
      if (i == 3)
        return;
      System.out.println("end");
    } finally {
      System.out.println("performing cleanup");
    }
  }
  
  public static void main(String args[]){
    for(int i=1;i<=4;i++){
      f(i);
    }
  }
}

结论:
initialization that requires cleanup
point1
performing cleanup
initialization that requires cleanup
point1
point2
performing cleanup
initialization that requires cleanup
point1
point2
point3
performing cleanup
initialization that requires cleanup
point1
point2
point3
end
performing cleanup

异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作,异常也不例外。

构造器和普通方法都声明抛出异常,但实际上并没有抛出。这种方法使能强制用户去捕获可能在覆盖后的方法中增加的异常,这对抽象方法也成立。

如果在继承的基类和实现了某个接口,有同样的方法,那么在派生了中不能改变咋基类中同名的方法,否则,在使用基类的时候,就不能判断是否捕获了正确的异常。如果接口里定义的方法不是来自于基类,那么此方法抛出任何异常都没有问题。

异常限制对构造器不起作用,构造器可以抛出任何异常,而不必理会基类构造器抛出的异常。因为基类构造器必须以这样或者那样的方式被调用,派生类构造器的异常说明必须包含基类构造器的异常说明。

派生类构造器不能捕获基类构造器抛出的异常。

通过强制派生类遵守基类 方法的异常说明,对象的可替换性得到了保证。


评论