Java异常的捕获及处理

导语

学完异常的捕获及处理就懂的情书。

// 情书
// 理解包容全部的你
try {
    we.together(time);  // 和你在一起的时间
} catch(Exception e) {  // 接收到所有在一起的问题
    i.understandYou();  // 我理解你
    i.containYou();  // 我包容你
} finally {
    we.love++;  // 不管发生什么,我们的爱都会越来越多
}

主要内容

  • 异常的产生以及对于程序的影响
  • 异常处理的格式
  • 异常的处理流程(核心)
  • throw、throws关键字的使用
  • 异常处理的使用标准(重要代码模型)
  • 自定义异常

具体内容

异常指程序运行过程中出现的非正常现象,例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。所谓异常处理,就是指程序在出现问题时依然可以正确的执行完。
异常是Java的一个重大特色,合理的使用异常处理,可以让我们程序更加的健壮。

异常的产生

异常是导致程序中断执行的一种指令流,异常一旦出现并且没有合理处理的话,那么程序就会将中断执行。

范例:产生异常的代码

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        System.out.println("2、除法计算。" + (10 / 0));  // 除数不能为0,所以会产生异常
        System.out.println("3、除法计算结束。");
    }
}

会出现异常:

java.lang.ArithmeticException

一旦产生异常之后,产生异常的语句以及之后的语句将不再执行 默认情况下是进行异常信息的输出,而后自动结束程序的执行。
我们要做的事情是:即使出现了异常,那么也应该让程序正确的执行完毕。

异常的处理

如果想要进行异常的处理,在Java之中提供了三个关键字:try、catch、finally,而这三个关键字的使用语法如下所示。

try{
    // 有可能出现异常的语句
} [catch(异常类型 对象) {
    // 处理异常
} catch(异常类型 对象) {
    // 处理异常
} catch(异常类型 对象) {
    // 处理异常
} ... ] [finally {
    // 不管是否出现异常,都执行的统一代码
}]

范例:应用异常的处理

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            System.out.println("2、除法计算:" + (10 / 0));  // 除数不能为0,所以会产生异常
            System.out.println("############");  
        } catch(ArithmeticException e) {
            System.out.println("******出现异常******");
        }
        System.out.println("3、除法计算结束。");
    }
}

输出结果:

1、除法计算开始。
******出现异常******
3、除法计算结束。

由于使用了异常处理,这样即使程序中出现了异常,发现也可以正常的执行完毕。
出现的异常的目的是为了解决异常,所以为了能够进行异常的处理,可以使用异常类中提供的printStackTrace()方法,进行异常信息的完整输出。

范例:使用printStackTrace()方法

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            System.out.println("2、除法计算:" + (10 / 0));
        } catch(ArithmeticException e) {
            e.printStackTrace();
        }
        System.out.println("3、除法计算结束。");
    }
}

输出结果:

1、除法计算开始。
java.lang.ArithmeticException: / by zero at TestDemo.main(TestDemo.java:5)
3、除法计算结束。

此时发现打印的异常信息是很完整的。

范例:使用finally

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            System.out.println("2、除法计算:" + (10 / 0));
        } catch(ArithmeticException e) {
            System.out.println("******出现异常******");
        } finally {
            System.out.println("###不管是否出现异常我都执行!###");
        }
        System.out.println("3、除法计算结束。");
    }
}

输出结果:

1、除法计算开始。
******出现异常******
###不管是否出现异常我都执行!###
3、除法计算结束。
public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            System.out.println("2、除法计算:" + (10 / 2));
        } catch(ArithmeticException e) {
            System.out.println("******出现异常******");
        } finally {
            System.out.println("###不管是否出现异常我都执行!###");
        }
        System.out.println("3、除法计算结束。");
    }
}

输出结果:

1、除法计算开始。
2、除法计算:5
###不管是否出现异常我都执行!###
3、除法计算结束。

在异常捕获的时候发现,一个try语句后同可以跟着多个catch语句。

范例:观察程序

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            int x = Integer.parseInt(args[0]);
            int y = Integer.parseInt(args[1]);
            System.out.println("2、除法计算:" + (x / y));
        } catch(ArithmeticException e) {
            e.printStackTrace();
        } finally {
            System.out.println("###不管是否出现异常我都执行!###");
        }
        System.out.println("3、除法计算结束。");
    }
}

以上的程序将由用户输入操作数据,于是可能存在有如下的情况出现:

  • 用户执行的时候不输入参数(java.lang.ArrayIndexOutOfBoundsException数组越界)。
  • 用户输入的参数不是数字(java.lang.NumberFormatException数字格式)。
  • 被除数为0(java.lang.ArithmeticException计算)。

范例:加入多个catch

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            int x = Integer.parseInt(args[0]);
            int y = Integer.parseInt(args[1]);
            System.out.println("2、除法计算:" + (x / y));
        } catch(ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        } catch(NumberFormatException e) {
            e.printStackTrace();
        } catch(ArithmeticException e) {
            e.printStackTrace();
        } finally {
            System.out.println("###不管是否出现异常我都执行!###");
        }
        System.out.println("3、除法计算结束。");
    }
}

程序现在的确很健壮,所有可能出现的异常都处理完了。
以上的异常都已经知道了,还让它出现,这绝对是技术问题。

异常的处理流程(核心)

通过以上的分析应该已经掌握了异常的处理格式了,但是遗憾的是,以上的操作并不是最好的异常处理方法,所以我们必须清楚整个Java中异常的处理流程。
首先来观察两个异常类的继承结构:

ArithmeticException NumberFormatException
java.lang.Object
--java.lang.Throwable
----java.lang.Exception
------java.lang.RutimeException
--------java.lang.ArithmeticException
java.lang.Object
--java.lang.Throwable
----java.lang.Exception
------java.lang.RuntimeException
--------java.lang.IllegalArgumentException
----------java.lang.NumberFormatException

经过异常类的观察可以发现所有的异常类都是Throwable的子类。而在Throwable下有两个子类:

  • Error:指的是JVM错误,指此时的程序还没有执行,如果没有执行用户无法处理。
  • Exception:指的是程序运行中产生的异常,用户可以处理。

也就是所谓的异常处理指的就是所有Exception以及它的子类异常。

异常处理流程

异常处理流程:
1、当程序在运行的过程之中出现了异常后,那么会由JVM自动根据异常的类型实例化一个与之类型匹配的异常类对象(此处用户不用去关心new,由系统自动负责处理)。
2、产生了异常对象之后会判断当前的语句上是否存在有异常处理,如果现在没有异常处理,那么就交给JVM进行默认的异常处理,处理的方式:输出异常信息,而后结束程序的调用。
3、如果此时存在有异常的捕获操作,那么会由try语句来捕获产生的异常类实例化对象,而后与try语句之后的每一个catch进行比较,如果现在有符合的捕获类型,则使用当前catch的语句来进行异常的处理,如果不匹配,则向下继续匹配其它的catch。
4、不管最后异常处理是否能够匹配,那么都要向后执行,如果此时程序中存在有finally语句,那么就先执行finally中的代码,但是执行完毕后需要根据之前的catch匹配结果来决定如何执行,如果之前已经成功的捕获了异常,那么就继续执行,finally之后的代码,如果之前没有成功的捕获异常,那么就将此异常交给JVM默认处理。
整个过程就好比方法重载一样。根据catch后面的参数类型进行匹配,但是所有Java对象都存在有自动的向上转型的操作支持,也就是说如果要真的匹配类型,简单的做法是匹配Exception就够了。

范例:使用Exception处理异常

public class TestDemo {
    public static void main(String args[]) {
        System.out.println("1、除法计算开始。");
        try {
            int x = Integer.parseInt(args[0]);
            int y = Integer.parseInt(args[1]);
            System.out.println("2、除法计算:" + (x / y));
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("###不管是否出现异常我都执行!###");
        }
        System.out.println("3、除法计算结束。");
    }
}

此时所有的异常都使用了Exception进行处理,所以在程序之中不用再去关心到底使用哪一个异常。
两点说明:

  • 在编写多个catch捕获异常的时候,捕获范围大的异常一定要放在捕获范围小的异常之后,否则程序编译错误。
  • 虽然直接捕获Exception比较方便,但是这样也不好,因为所有的异常都会按照同样一种方式进行处理。所以在一些要求严格的项目里面,异常一定要分开处理会更好。

throws关键字

throws关键字主要用于方法的声明上,指的是当我们方法之中出现异常后交由被调用处来处理。

范例:使用throws

class MyMath {
    // 由于存在有throws,那么就表示此方法里产生的异常交给被调用处处理
    public static int div(int x, int y) throws Exception {
        return x / y;
    }
}

调用以上的方法(错误示范)

public class TestDemo {
    public static void main(String args[]) {
        System.out.println(MyMath.div(10, 2));
    }
}

代码改为

public class TestDemo {
    public static void main(String args[]) {
        try {
            System.out.println(MyMath.div(10, 2));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

如果调用了具有throws声明的方法之后,那么不管操作是否出现异常,都必须使用try...catch来进行异常的处理。

在程序之中主方法也属于方法,那么主方法上能否继续使用throws来抛出异常呢?

public class TestDemo {
    public static void main(String args[]) throws Exception {
        // 表示此异常产生之后会直接通过主方法抛出
        System.out.println(MyMath.div(10, 0));
    }
}

在主方法上如果继续抛出了异常,那么这个异常就将交给JVM进行处理,也就是默认处理方法,输出异常信息,而后结束程序调用 。
主方法上不要加上throws,因为程序如果出错了,也希望可以正常的结束调用。

throw关键字

在程序之中可以直接使用throw手工的抛出一个异常类的实例化对象。

范例:手工抛出异常

public class TestDemo {
    public static void main(String args[]) {
        throw new Exception("自己定义的异常!");
    }
}

编译错误

错误:未报告的异常错误Exception,必须对其进行捕获或声明以便抛出throw new Exception("自己定义的异常!");

修改代码

public class TestDemo {
    public static void main(String args[]) {
        try {
            throw new Exception("自己定义的异常!");
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

java.lang.Exception:自己定义的异常!

异常肯定应该尽量回避,为什么要自己抛出异常呢?
throw和throws的区别:

  • throw:指的是在方法之中人为抛出一个异常类对象。
  • throws:在方法的声明上使用,表示此方法在调用时必须处理异常。

异常处理标准格式(重要代码模型)

现在要求定义一个div()方法,要求,这个方法在进行计算之前打印提示信息,在计算结束完毕也打印提示信息,如果在计算之中产生了异常,刚交给被调用处进行处理。

范例:上面要求
先写出不出错的情况

class MyMath {
    public static int div(int x, int y) {
        int result = 0;
        System.out.println("***1、除法计算开始***");
        result = x / y;
        System.out.println("***2、除法计算结束***");
        return result;
    }
}
public class TestDemo {
    public static void main(String args[]) {
        System.out.println(MyMath.div(10, 2));
    }
}

输出结果:

***1、除法计算开始***
***2、除法计算结束***
5

对于以上给出的除法操作不可能永远都正常的完成,所以应该进行一些合理的处理,首先某个方法出现异常了必须交给被调用处处理,那么应该在方法上使用throws抛出。
修改代码

class MyMath {
    // 此时表示div()方法上如果出现了异常交给被调用处处理
    public static int div(int x, int y) throws Exception {
        int result = 0;
        System.out.println("***1、除法计算开始***");
        result = x / y;
        System.out.println("***2、除法计算结束***");
        return result;
    }
}
public class TestDemo {
    public static void main(String args[]) {
        try {
            System.out.println(MyMath.div(10, 2));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

***1、除法计算开始***
***2、除法计算结束***
5

如果代码出错了呢

class MyMath {
    // 此时表示div()方法上如果出现了异常交给被调用处处理
    public static int div(int x, int y) throws Exception {
        int result = 0;
        System.out.println("***1、除法计算开始***");
        result = x / y;
        System.out.println("***2、除法计算结束***");
        return result;
    }
}
public class TestDemo {
    public static void main(String args[]) {
        try {
            System.out.println(MyMath.div(10, 0));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

***1、除法计算开始***
java.lang.ArithmeticException...

如果代码出错了呢?程序有些内容就不执行了,这样明显不对。
修改代码

class MyMath {
    // 此时表示div()方法上如果出现了异常交给被调用处处理
    public static int div(int x, int y) throws Exception {
        int result = 0;
        System.out.println("***1、除法计算开始***");
        try {
            result = x / y;
        } catch(Exception e) {
            throw e;
        } finally {
            System.out.println("***2、除法计算结束***");
        }
        return result;
    }
}
public class TestDemo {
    public static void main(String args[]) {
        try {
            System.out.println(MyMath.div(10, 0));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

***1、除法计算开始***
***2、除法计算结束***
java.lang.ArithmeticException...

实际上以上的代码还可以缩写(不建议使用)。

class MyMath {
    // 此时表示div()方法上如果出现了异常交给被调用处处理
    public static int div(int x, int y) throws Exception {
        int result = 0;
        System.out.println("***1、除法计算开始***");
        try {
            result = x / y;
        } finally {
            System.out.println("***2、除法计算结束***");
        }
        return result;
    }
}
public class TestDemo {
    public static void main(String args[]) {
        try {
            System.out.println(MyMath.div(10, 0));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

如果现在你直接使用了try...finally,那么表示你连处理一下的机会都没有,就直接抛出了。

RuntimeException类

先观察一个程序代码

public class TestDemo {
    public static void main(String args[]) {
        int temp = Integer.parseInt("100");  // 将字符串变为整型数据
    }
}

现在来观察一下parseInt()方法的定义

public static int parseInt(String s) throws NumberFormatException {
    return parseInt(s,10);
}

此时parseInt()方法抛出了NumberFormatException,按照道理来讲,应该进行强制性的异常捕获,可是现在并没有这种强制性的要求。
来观察一下NumberFormatException的继承结构

java.lang.Object
--java.lang.Throwable
----java.lang.Exception
------java.lang.RuntimeException  // 运行时异常
--------java.lang.IllegalArgumentException
----------java.lang.NumberFormatException

在Java里面为了方便用户代码的编写,专门提供了一种RuntimeException类,这种异常类的最大特征在于:程序在编译的时候不会强制性的要求用户处理异常,用户可以根据自己的需要选择性进行处理,但是如果没有处理又发生异常了,将交给JVM默认处理。也就是说RuntimeException的子异常类,可以由用户根据需要选择进行处理。

解释Exception与RuntimeException的区别:

  • Exception是RuntimeException的父类。
  • 使用Exception定义的异常必须要被处理,而RuntimeException的异常可以选择性处理。

常见的RuntimeException:

  • ArithmeticException
  • NullPointerException
  • ClassCastException

assert关键字(了解)

assert关键字是在JDK1.4的时候引入的,其主要功能是进行断言。
在Java中的断言指的是程序执行到某行代码处于一定是预期的结果。

范例:观察断言

public class TestDemo {
    public static void main(String args[]) {
        int num = 10;
        // 中间可能经过了20行代码来操作num的内容
        期望中的内容应该是20
        assert num == 20 : "num的内容不是20";
        System.out.println("num = " + num);
    }
}

输出结果:

num = 10

默认情况下断言是不应该影响程序的运行的,也就是说在java解释程序的时候,断言是默认不起作用的。

启用断言

java -ea TestDemo

输出结果:

Exception in thread "main" java.lang.AssertionError: num的内容不是20...

在Java里面断言的设计要比C++强的很多,它不会影响到程序的执行,但是使用的意义不大。

自定义异常

Java本身已经提供了大量的异常,但是这些异常在实际的工作之中往往并不够去使用,例如:当你要执行数据增加操作的时候,有可能会出现一些错误的数据,而这些错误的数据一旦出现就应该抛出异常,例如:AddException,这样的异常Java并没有,所以就需要由用户自己去开发一个自己的异常类。
如果想要开发自定义的异常类可以选择继承Exception或者是RuntimeException。

范例:定义AddException

class AddException extends Exception {
    public Addexception(String msg) {
        super(msg);
    }
}
public class TestDemo {
    public static void main(String args[]) {
        int num = 20;
        try {
            if(num > 10) { // 出现了错误,应该产生异常
                throw new AddException("数值传递的过大!");
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果:

AddException:数值传递的过大!...

这种代码只能介绍自定义异常的形式,但是并不能说明自定义异常的实际使用。

总结

  • Exception的父类是Throwable,但是在编写代码的时候尽量不要使用Throwable,因为Throwable下面还包含了一个Error子类,我们能够处理的只有Exception子类。
  • 异常处理的标准格式:try、catch、finally、throw、throws。
  • RuntimeException与Exception的区别。

更多内容戳这里(整理好的各种文集)

推荐阅读更多精彩内容