Java 中的异常和处理详解

简介

程序运行时,发生的不被指望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,马上退出终止,仍是输出错误给给用户?或者用 C 语言风格:用函数返回值做为执行状态?
Java 提供了更加优秀解决办法:异常处理机制。java

异常处理机制能让程序在异常发生时,按照代码预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java 中的异常能够是函数中语句执行时引起的,也能够是程序员经过 throw 语句手动抛出的,只要在 Java 程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE 就会试图寻找异常处理程序来处理异常。程序员

Throwable 类是 Java 异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或间接)实例,它才是一个异常对象,才能被异常处理机制识别。JDK 中内建了一些经常使用的异常类,咱们也能够自定义异常。数据库


Java 异常的分类和类结构图

Java 标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类。
Throwable 又派生出 Error 类和 Exception 类。编程

错误:Error 类以及它的子类的实例,表明 JVM 自己的错误。错误不能被程序员经过代码处理,Error 不多出现。所以,程序员应该关注 Exception 为父类的分支下的各类异常类。数组

异常:Exception 以及它的子类,表明程序运行时发送的各类不被指望发生的事件。能够被 Java 异常处理机制使用,是异常处理的核心。 多线程

整体上咱们根据 Java 对异常的处理要求,将异常分为两类。编程语言

非检查异常(unchecked exception)
Error 和 RuntimeException 以及它们的子类。Java 在编译时,不会提示和发现这样的异常,不要求在程序中处理这些异常。因此若是愿意,咱们能够编写代码处理(使用 try...catch...finally)这样的异常,也能够不处理。对于这些异常,咱们更应该的不是去处理这些异常,而是应该修正代码。这样的异常发生的缘由多半是代码逻辑写的有问题。如除 0 错误 ArithmeticException,错误的强制类型转换错误 ClassCastException,数组索引越界错误 ArrayIndexOutOfBoundsException,操做了空对象错误 NullPointerException 等等。ide

检查异常(checked exception)
除了 Error 和 RuntimeException 的其它异常。Java 强制要求程序员为这样的异常作预备处理工做(使用 try...catch...finally 或者 throws)。在方法中要么用 try...catch 语句捕获它并处理,要么用 throws 子句声明抛出它,不然编译不会经过。这样的异常通常是由程序的运行环境致使的。由于程序可能运行被运行在各类未知的环境下,而程序员没法干预用户如何使用他编写的程序,因而程序员就应该为这样的异常时刻准备着。如 SQLExceptionIOExceptionClassNotFoundException 等。模块化


初识异常

下面的代码会演示2个异常类型:ArithmeticException 和 InputMimatchException。前者因为整数除 0 引起,后者是输入的数据不能被转化为 int 类型引起。函数

package com.example;
import java. util .Scanner ;
public class AllDemo
{
      public static void main (String [] args )
      {
            System . out. println( "----欢迎使用命令行除法计算器----" ) ;
            CMDCalculate ();
      }
      public static void CMDCalculate ()
      {
            Scanner scan = new Scanner ( System. in );
            int num1 = scan .nextInt () ;
            int num2 = scan .nextInt () ;
            int result = devide (num1 , num2 ) ;
            System . out. println( "result:" + result) ;
            scan .close () ;
      }
      public static int devide (int num1, int num2 ){
            return num1 / num2 ;
      }
}
/*****************************************
 
----欢迎使用命令行除法计算器----
0
Exception in thread "main" java.lang.ArithmeticException : / by zero
     at com.example.AllDemo.devide( AllDemo.java:30 )
     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )
     at com.example.AllDemo.main( AllDemo.java:12 )
 
----欢迎使用命令行除法计算器----
r
Exception in thread "main" java.util.InputMismatchException
     at java.util.Scanner.throwFor( Scanner.java:864 )
     at java.util.Scanner.next( Scanner.java:1485 )
     at java.util.Scanner.nextInt( Scanner.java:2117 )
     at java.util.Scanner.nextInt( Scanner.java:2076 )
     at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )
     at com.example.AllDemo.main( AllDemo.java:12 )
*****************************************/

异常是在执行某个函数时引起的,而函数又是层级调用,造成调用栈的,所以,只要一个函数发生了异常,那么它的全部 caller 都会被异常影响。当这些被影响的函数以异常信息输出时,就造成了异常追踪栈。

异常最早发生的地方,叫作异常抛出点。

从上面的例子能够看出,当 devide 函数发生除 0 异常时,devide 函数抛出 ArithmeticExcepton 异常,所以调用它的 CMDCalculate 函数也没法正常完成,所以也发送异常,而 CMDCalculate 的 caller --main 由于 CMDCalculate 抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫作异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的 caller 中找到最近的异常处理程序。因为这个例子没有使用任何异常处理机制,所以异常最终由 main 函数抛给 JRE,致使程序终止。

上面的代码不使用异常处理机制,也能够顺利编译,由于2个异常都是非检查异常。可是下面的例子就必须使用异常处理机制,由于异常是检查异常。

代码中选择使用 throws 声明异常,让函数的调用者去处理可能发生的异常。可是为何只 throws 了 IOException呢?由于 FileNotFoundException 是 IOException 的子类,在处理范围内。

@Test
public void testException() throws IOException
{
    //FileInputStream的构造函数会抛出FileNotFoundException
    FileInputStream fileIn = new FileInputStream("E:\\a.txt");
 
    int word;
    //read方法会抛出IOException
    while((word =  fileIn.read())!=-1) 
    {
        System.out.print((char)word);
    }
    //close方法会抛出IOException
    fileIn.clos
}

异常处理的基本语法

在编写代码处理异常时,对于检查异常,有2中不一样的处理方式:使用 try...catch...finally 语句块处理它。或者,在函数签名中使用 throws 声明交给函数调用者 caller 去处理。
try...catch...finally 语句块

try{
     //try块中放可能发生异常的代码。
     //若是执行完try且不发生异常,则接着去执行finally块和finally后面的代码(若是有的话)。
     //若是发生异常,则尝试去匹配catch块。
 
}catch(SQLException SQLexception){
    //每个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中能够将多个异常声明在一个catch中。
    //catch后面的括号定义了异常类型和异常参数。若是异常与之匹配且是最早匹配到的,则虚拟机将使用这个catch块来处理异常。
    //在catch块中可使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
    //若是当前try块中发生的异常在后续的全部catch中都没捕获到,则先去执行finally,而后到这个函数的外部caller中去匹配异常处理器。
    //若是try中没有发生异常,则全部的catch块将被忽略。
 
}catch(Exception exception){
    //...
}finally{
 
    //finally块一般是可选的。
   //不管异常是否发生,异常是否匹配被处理,finally都会执行。
   //一个try至少要有一个catch块,不然, 至少要有1个finally块。可是finally不是用来处理异常的,finally不会捕获异常。
  //finally主要作一些清理工做,如流的关闭,数据库链接的关闭等。 
}

注:

  1. try 块中的局部变量和 catch 块中的局部变量(包括异常变量),以及 finally 中的局部变量,它们之间不可共享使用。
  2. 每个 catch 块用于处理一个异常。异常匹配是按照 catch 块的的顺序从上往下寻找的,只有第一个匹配的 catch 块会获得执行。匹配时,不只运行精确匹配,也支持父类匹配,所以,若是一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每一个 catch 块都有存在的意义。
  3. Java 中,异常处理的任务就是将执行控制流从异常发生的地方转移到可以处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句都不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理 catch 代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的 catch 代码块” 后面接着执行。

有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫作:resumption model of exception handing(恢复式异常处理模式)
而 Java 则是让执行流恢复处处理了异常的 catch 块后接着执行,这种策略叫作:termination model of exception handing(终结式异常处理模式)

public static void main(String[] args){
        try {
            foo();
        }catch(ArithmeticException ae) {
            System.out.println("处理异常");
        }
}
public static void foo(){
        int a = 5/0;  //异常抛出点
        System.out.println("为何还不给我涨工资!!!");  //////////////////////不会执行
}

throws 函数声明
throws 声明:若是一个方法内部的代码会抛出检查异常(checked exception),而方法本身又没有彻底处理掉,则 Java 保证你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常,不然编译不经过。

throws 时另外一种处理异常的方式,它不一样于 try...catch...finally,throws 仅仅时将函数中可能出现的异常向调用者声明,而本身则不具体处理。

采起这种异常处理的缘由多是:方法自己不知道如何处理这样的异常,或者说让调用者处理更好,调用者须要为可能发生的异常负责。


finally 块

finally 块无论异常是否发生,只要对应的 try 执行了,则它必定也执行。只有一种方法可让 finally 块不执行:System.exit(0)。所以 finally 块一般用来作资源释放操做:关闭文件,关闭数据库链接等等。

注:

  1. finally 块没有处理异常的能力。处理异常的只能是 catch 块。
  2. 在同一 try...catch...finally 块中,若是 try 中抛出异常,且有匹配的 catch 块,则先执行 catch 块,再执行 finally 块。若是没有 catch 块匹配,则先执行 fianlly,而后再去外面的调用者中寻找合适的 catch 块。
  3. 在同一 try...catch...finally 块中,try 发生异常,且匹配的 catch 块中处理异常时抛出异常,那么后面的 finally 也会执行:首先执行 finally 块,而后去外围调用者中寻找合适的 catch 块。

throw 异常抛出语句

程序员也能够经过 throw 语句手动显示的抛出一个异常。throw 语句的后面必须是一个异常对象。
throw 语句必须写在函数中,执行 throw 语句的地方就是一个异常抛出点,它和有 JRE 自动造成的异常抛出点没有任何差异。

public void save(User user)
{
      if(user  == null) 
          throw new IllegalArgumentException("User对象为空");
      //......
 
}

异常的链化

在一些大型的,模块化的软甲开发中,一旦一个地方发生异常,则如骨牌效应通常,将致使一连串的异常。假设 B 模块完成本身的逻辑须要调用 A 模块中的方法,若是 A 模块发生异常,则 B 也将不能完成而发生异常,可是 B 在抛出异常时,会将 A 的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化能够将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异常对象将包含先前异常的信息。这项技术主要是异常类的一个带 Throwable 参数的函数来实现的。这个看成参数的异常,咱们叫它根源异常(cause)。
查看 Throwable 类源码,能够发现里面有一个 Throwable 字段 cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的节点类设计一模一样,所以造成链也是天然的了。

public class Throwable implements Serializable {
    private Throwable cause = this;
 
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
 
    //........
}

自定义异常

若是要自定义异常,则扩展 Exception 类便可,所以这样的自定义异常都属于检查异常。若是要自定义非检查异常,则扩展自 RuntimeException。
自定义的异常应该老是包含以下的构造函数:

  • 一个无参构造函数
  • 一个带有 String 参数的构造函数,并传递给父类的构造函数
  • 一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数
  • 一个带有 Throwable 参数的构造函数,并传递给父类的构造函数

下面是 IOException 类的完成源代码,能够借鉴。

public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException()
    {
        super();
    }
 
    public IOException(String message)
    {
        super(message);
    }
 
    public IOException(String message, Throwable cause)
    {
        super(message, cause);
    }
 
    public IOException(Throwable cause)
    {
        super(cause);
    }
}

异常的注意事项

  1. 当子类重写父类的带有 throws 声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内--用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法。这是为了支持多态。
    例如,父类方法 throws 的是2个异常,子类就不能 throws 3个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。
class Father
{
    public void start() throws IOException
    {
        throw new IOException();
    }
}
 
class Son extends Father
{
    public void start() throws Exception
    {
        throw new SQLException();
    }
}
/**********************假设上面的代码是容许的(实质是错误的)***********************/
class Test
{
    public static void main(String[] args)
    {
        Father[] objs = new Father[2];
        objs[0] = new Father();
        objs[1] = new Son();
 
        for(Father obj:objs)
        {
        //由于Son类抛出的实质是SQLException,而IOException没法处理它。
        //那么这里的try。。catch就不能处理Son中的异常。
        //多态就不能实现了。
            try {
                 obj.start();
            }catch(IOException)
            {
                 //处理IOException
            }
         }
   }
}
  1. Java 程序能够是多线程的。每个线程都是一个独立的执行流,独立的函数调用栈。若是程序只有一个线程,那么没有被任何代码处理的异常会致使程序终止。若是是多线程,那么没有被任何代码处理的异常仅仅会致使异常所在的线程结束。
    也就是说,Java 中的异常是线程独立的,线程的问题应该由线程本身来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

finally 块和 return

首先一个不容易理解的事实:在 try 块中即使有 return,break,continue 等改变执行流的语句,finally 也会执行。

public static void main(String[] args)
{
    int re = bar();
    System.out.println(re);
}
private static int bar() 
{
    try{
        return 5;
    } finally{
        System.out.println("finally");
    }
}
/*输出:
finally
*/

也就是说:try...catch.finally 中的 return 只要能执行,就都执行了,它们共同向同一个内存地址(假设地址是 0x80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。
finally 中的 return 会覆盖 try 或者 catch 中的返回值。

public static void main(String[] args)
    {
        int result;
 
        result  =  foo();
        System.out.println(result);     /////////2
 
        result = bar();
        System.out.println(result);    /////////2
    }
 
    @SuppressWarnings("finally")
    public static int foo()
    {
        trz{
            int a = 5 / 0;
        } catch (Exception e){
            return 1;
        } finally{
            return 2;
        }
 
    }
 
    @SuppressWarnings("finally")
    public static int bar()
    {
        try {
            return 1;
        }finally {
            return 2;
        }
    }

finally 中的 return 会抑制(消灭)前面 try 或者 catch 块中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
 
        try{
            result  = bar();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
    }
 
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,由于下面的finally中使用了return");
        }finally {
            return 100;
        }
    }
 
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            return 100;
        }
    }
}

finally 中的异常会覆盖(消灭)前面 try 或者 catch 中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
 
        try{
            result  = bar();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
    }
 
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,由于下面的finally中抛出了新的异常");
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
    }
 
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
 
    }
}

上面的3个例子都异于常人的编码思惟,所以建议:

  • 不要在 finally 中使用 return
  • 不要在 finally 中抛出异常
  • 减轻 finally 的任务,不要在 finally 中作一些其它的事情,finally 块仅仅用来释放资源是最合适的
  • 尽可能将全部的 return 写在函数的最后面,而不是 try...catch...finally 中
相关文章
相关标签/搜索