1、概述
我们在程序的设计和运行调试的过程中,不可避免会发生错误。尽管Java提供了便于写出安全代码的方法,我们开发人员也尽量的减少错误的发生,但还是避免不了。因此Java提供了异常处理机制来帮助我们检查可能出现的错误,来保证程序的可读性和可维护性。Java中是将异常封装到一个类中的,出现错误时,就会抛出这个异常类的一个异常对象。
在程序中,错误可能产生于各种原因,比如用户的坏数据、打开一个不存在的文件等。Java中将这种在程序运行时可能出现的错误称为异常,异常是在程序执行期间发生的事件,它中断了正在执行的程序的正常指令流。
以下以一个例子说明:
public class Test {
public static void main(String[] args) {
int result = 25 / 0;
System.out.println(result);
}
}
执行,控制台输出:
可以看到程序发生了ArithmeticException算数异常,后面也给出了提示/ by zero
,因为0作除数,所以程序抛出了异常,程序并未执行完毕而提前结束了。
其实有很多种异常,比如常见的空指针异常、数组溢出异常等。Java是面向对象的语言,因此异常在Java中是作为类的实例出现的。当某个方法发生错误时,这个方法会创建一个异常对象,并且将它传递给正在运行的系统。通过异常的处理机制,可将非正常下的处理代码和主逻辑分离,在编写代码主要业务的同时在其他的地方处理异常,这就非常方便了。
2、异常体系结构
通过图示说明:
Exception的子类并不是只有上面的两种RuntimeException和IOException,它有很多的子类,不过可以大体上分为两种:运行时异常RuntimeException和非运行时异常。
Throwable类是所有异常类的顶级父类,该类有2个直接子类:Error类和Exception类。
2.1、Error类
Error类和它的子类用来描述Java运行系统中的内部错误,这种一般发生在JVM内部,属于严重错误,是无法通过程序调试来规避的,也就是说超出了我们调控的范围。比如OutOfMemoryError、ThreadDeath等,发生这种异常时,JVM会中止线程。
2.2、Exception类
Exception类称为非致命性类,这种异常程序本身是可以处理的。Exception类大体上可以分为:运行时异常和非运行时异常。这种异常我们应该尽可能的去处理,来保证代码的可读性和健壮性。
运行时异常:
运行时异常指RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等异常。这些异常属于非检查性异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
以下列出常见的非检查性异常:
非运行时异常:
非运行时异常是指除了RuntimeException以外的异常,属于Exception类的子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。这些异常属于检查性异常,因为不通过编译是运行不了的。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
常见的检查性异常如下:
我们研究的是Exception这类可处理的非致命性异常,因为研究Error异常毫无意义,它无法避免,以下都是围绕Exception类及其子类。
3、异常的声明及抛出
异常的声明和抛出一般是在方法中进行,语法特点如下:
方法名称(...) throws 异常1,异常2...{
throw 异常对象;
}
通过throws、throw关键字来声明和抛出异常。以下代码说明。
定义一个类,类中有一个抛出异常的方法:
public class Demo {
// 定义一个方法,并且抛出异常
public void d1(int a) throws Exception {
if (a < 0) {
throw new Exception("不接受负数!");
}
System.out.println("2021到了!");
}
}
测试:
public class Test {
public static void main(String[] args) throws Exception {
Demo d = new Demo();
d.d1(-5);
System.out.println("程序可以执行到这里!");
}
}
执行,控制台:
可以看到,程序执行到d1方法时,由于传入了一个负数参数,所以抛出了一个异常对象,异常对象被系统捕捉到,强制终止了程序,后面的一句代码也就无法执行了。需要说明的是,当调用会抛出异常的方法时,就像上面的d1方法,要么继续往上抛,上面就是继续往上抛,要么对异常进行捕获处理,下面介绍捕获处理。
4、异常的捕获和处理
对异常的捕获处理使用try catch块,语法如下:
try{
抛出异常的方法;
}catch(异常类型 变量名){
异常类型的处理;
}
对上面的测试代码修改一下:
public static void main(String[] args) {
Demo d = new Demo();
try {
d.d1(5);
System.out.println("程序没发生异常!");
} catch (Exception e) {
System.err.println("程序发生了异常!");
System.err.println(e.getMessage());
e.printStackTrace();
}
System.out.println("程序可以执行到这里!");
}
执行,控制台:
d1方法传入了一个非负数,不会抛出异常对象,那么走try块,不会走catch块,try catch块之后的语句还是会执行。
传入一个负数,再执行:
public static void main(String[] args) {
Demo d = new Demo();
try {
// d.d1(5);
d.d1(-5);
System.out.println("程序没发生异常!");
} catch (Exception e) {
System.err.println("程序发生了异常!");
System.err.println(e.getMessage());
e.printStackTrace();
}
System.out.println("程序可以执行到这里!");
}
控制台:
可以看到d1方法传入负数时,会抛出异常对象,捕获到了异常对象,那么走catch块,不会走try块,try catch块之后的内容还是会执行。
一般对于要强制执行的语句,比如上面try catch块之后的语句,不管发不发生异常,都希望它执行,那么使用finally块,一般这个finally块是和try catch块连用的,语法如下:
try{
抛出异常的方法;
}catch(异常类型 变量名){
异常类型的处理;
}finally{
要强制执行的语句;
}
那么上面的测试代码再做修改:
public static void main(String[] args) {
Demo d = new Demo();
try {
// d.d1(5);
d.d1(-5);
System.out.println("程序没发生异常!");
} catch (Exception e) {
System.err.println("程序发生了异常!");
System.err.println(e.getMessage());
e.printStackTrace();
} finally {
System.out.println("程序执行到了这里!");
}
}
public static void main(String[] args) {
Demo d = new Demo();
try {
d.d1(5);
// d.d1(-5);
System.out.println("程序没发生异常!");
} catch (Exception e) {
System.err.println("程序发生了异常!");
System.err.println(e.getMessage());
e.printStackTrace();
} finally {
System.out.println("程序执行到了这里!");
}
}
如果要调用多个方法,每个方法都会抛出异常,那么按照上面的处理,是分开使用try catch块处理,如下:
try{
抛出异常的方法1;
}catch(异常类型1 变量名){
异常类型的处理;
}finally{
要强制执行的语句;
}
......
try{
抛出异常的方法2;
}catch(异常类型2 变量名){
异常类型的处理;
}finally{
要强制执行的语句;
}
......
try{
抛出异常的方法3;
}catch(异常类型3 变量名){
异常类型的处理;
}finally{
要强制执行的语句;
}
......
每一个有异常的方法,都用一个try catch finally块来捕获处理,这样很麻烦,代码也很冗余,可以集中进行捕获和处理,如下:
try{
抛出异常的方法1;
抛出异常的方法2;
抛出异常的方法3;
}catch(异常类型1 变量名){
异常类型的处理;
}catch(异常类型2 变量名){
异常类型的处理;
}catch(异常类型3 变量名){
异常类型的处理;
}finally{
要强制执行的语句1;
要强制执行的语句2;
要强制执行的语句3;
}
这样使用一个try块多个catch块来处理就简洁了很多,要说明的是,如果捕获的多个异常类型之间存在继承关系的话,那么子异常类需要放到父异常类的前面进行捕获。以下用代码说明:
public class Demo {
public void d1(int a) throws Exception {
if (a < 0) {
throw new Exception("不接受负数!");
}
System.out.println("2021到了!");
}
public void d2(Person p) throws NullPointerException {
if (p == null) {
throw new NullPointerException("对象不能为空!");
}
System.out.println("参数合法!");
}
}
测试:
public static void main(String[] args) {
Demo d = new Demo();
Person p = null;
try {
d.d2(p);
d.d1(-5);
System.out.println("程序没发生异常!");
} catch (NullPointerException e1) {
e1.printStackTrace();
} catch (Exception e2) {
e2.printStackTrace();
} finally {
System.out.println("程序执行到了这里!");
}
}
尽量将抛出子异常的方法放到前面,这样就会先捕获到子类异常,因为不可能同时进入到多个catch块的。
5、常用方法
异常类提供的常用方法如下:
其中用的比较多的是getMessage()和printStackTrace()。
6、自定义异常类型
基本上是以Java内置的异常类可以描述编程时的大部分异常情况,一般在Java的内置异常类不能满足要求时才会创建自定义异常类,因此非必要的情况下不建议自定义异常类,自定义异常类必须要直接或间接继承Exception类。
自定义异常类的步骤大概如下:
- 自定义类型,继承Exception类或它的子类。
- 在方法中使用throw关键字抛出自定义的异常。
- 调用该方法时,使用try catch块进行捕获处理或者向上抛出。
以下用一个例子说明。
自定义异常类:
/*
* 自定义异常类,直接继承Exception类
*/
public class MyException extends Exception {
private static final long serialVersionUID = 1L;
// 定义2个常量
public static final int MAX_NUM = 1000;
public static final String MESSAGE = "集合中元素超量!";
// 构造方法
public MyException() {
}
// 覆盖父类的getMessage方法
@Override
public String getMessage() {
String res = MESSAGE + "最大元素个数是:" + MAX_NUM;
return res;
}
// 覆盖父类的printStackTrace方法
@Override
public void printStackTrace() {
System.err.println("自定义异常类中的printStackTrace方法!");
super.printStackTrace();
}
}
定义异常抛出类:
/*
* 此类中定义有异常的方法,及抛出自定义异常
*/
public class UseMyException {
// 定义方法,抛出异常
public List<Integer> listAdd(int size) throws MyException {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= size; i++) {
// 抛出自定义异常
if (i > MyException.MAX_NUM) {
throw new MyException();
}
list.add(i);
}
return list;
}
}
测试:
public static void main(String[] args) {
UseMyException ume = new UseMyException();
try {
List<Integer> list = ume.listAdd(1000);
System.out.println("集合中的元素个数是:" + list.size());
} catch (MyException e) {
e.printStackTrace();
}
}
执行,控制台:
将参数改为1001:
List<Integer> list = ume.listAdd(1000);
再执行:
7、总结
掌握异常类型的抛出、捕获和处理,并且了解异常类的层级结构,能够自定义异常类型,开发中可能遇到各种各样的异常,熟练掌握异常的处理机制很有必要。