异常处理
Table of Contents
Java 提供了异常处理机制来帮助程序员检查可能出现的错误,以保证程序的可读性和可维护性。Java 将异常封装到一个类中,出现错误时就会拋出异常。
异常处理
异常(exception)是在运行程序时产生的一种异常情况,已经成为了衡量一门语言是否成熟的标准之一。
异常简介
Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。 为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。
在 Java 中一个异常的产生,主要有如下三种原因:
- Java 内部错误发生异常,Java 虚拟机产生的异常;
- 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等;
- 通过
throw
语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。
我们把生成异常对象,并把它提交给运行时系统的过程称为 抛出(throw)异常
;运行时系统在方法的调用栈中查找,直至找到能够处理该类型的对象,这一个过程称为 捕获(catch)异常
。
异常类型
为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。
在 Java 中所有异常类型都是内置类 java.lang.Throwable
类的子类,即 Throwable
位于异常类层次结构的顶层。

由上图可知, Throwable
类是所有异常和错误的超类,下面有 Error
和 Exception
现个子类分别表示错误和异常:
Exception
类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类;Error
定义了在通常环境下不希望被程序捕获的异常,一般指的是 JVM 错误,如堆栈溢出。
其中异常类 Exception
又分为运行时异常(aka.不检查异常)和错误异常(aka.检查异常),这两种异常有很大的区别。
运行时异常都是 RuntimeException
类及其子类异常,如 NullPointerException、IndexOutOfBoundsException
等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。
异常类型 | 说明 |
---|---|
ArithmeticException |
算术错误异常,如以零做除数 |
ArrayIndexOutOfBoundException |
数组索引越界 |
ArrayStoreException |
向类型不兼容的数组元素赋值 |
ClassCastException |
类型转换异常 |
IllegalArgumentException |
使用非法实参调用方法 |
IllegalStateException |
环境或应用程序处于不正确的状态 |
IllegalThreadStateException |
被请求的操作与当前线程状态不兼容 |
IndexOutOfBoundsException |
某种类型的索引异常 |
NullPointerException |
尝试访问 null 对象成员,空指针异常 |
NegativeArraySizeException |
再负数范围内创建的数组 |
NumberFormatException |
数学转化格式异常,比如字符串到 float 型数字转换无效 |
TypeNotPresentException |
类型未找到 |
非运行时异常是指 RuntimeException
以外的异常,类型上都属于 Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException
等以及用户自定义的 Exception
异常(一般情况下不自定义检查异常)。
异常类型 | 说明 |
---|---|
ClassNotFoundException |
没有找到类 |
IllegalAccessException |
访问类被拒绝 |
InstantiationException |
试图创建抽象类或接口的对象 |
InterruptedException |
线程被另一个线程中断 |
NoSuchFieldException |
请求的域不存在 |
NoSuchMethodException |
请求的方法不存在 |
ReflectiveOperationException |
与反射有关的异常的超类 |
Error VS Exception
Error(错误)和 Exception(异常)都是 java.lang.Throwable
类的子类,在 Java 代码中只有继承了 Throwable
类的实例才能被 throw
或者 catch
。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获,进行相应的处理;Error 是指正常情况下不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态,所以不需要被开发者捕获。
Error 错误是任何处理技术都无法恢复的情况,肯定会导致程序非正常终止,并且 Error 错误属于未检查类型,大多数发生在运行时。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源码里必须显示的进行捕获处理,这里是编译期检查的一部分。不检查异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。
Error | 说明 |
---|---|
NoClassDefFoundError |
找不到 class 定义异常 |
StackOverflowError |
深递归导致栈被耗尽而抛出的异常 |
OutOfMemoryError |
内存溢出异常 |
异常处理机制
前面介绍了异常的产生是不可避免的,那么为了保证程序有效地执行,需要对发生的异常进行相应的处理。
Java 的异常处理通过 5 个关键字来实现: try、catch、throw、throws
和 finally
,具体如下:
try catch
语句用于捕获并处理异常;finally
语句用于在任何情况下(除特殊情况外)都必须执行的代码;throw
语句用于抛出异常;throws
语句用于声明可能会出现的异常。
Java 的异常处理机制提供了一种结构性和控制性的方式来处理程序执行期间发生的事件。异常处理的机制如下:
- 在方法中用
try catch
语句捕获并处理异常,catch
语句可以有多个,用来匹配多个异常; - 对于处理不了的异常或者要转型的异常,在方法的声明处通过
throws
语句抛出异常,即由上层的调用方法来处理。
以下代码是异常处理程序的基本结构:
1: try { 2: // 逻辑程序块 3: } catch (ExceptionType1 e) { 4: // 处理代码块1 5: } catch (ExceptionType2 e) { 6: // 处理代码块2 7: throw(e); // 再抛出这个异常 8: } finally { 9: // 释放资源代码块 10: }
try catch 语句
try catch
语句用于捕获并处理异常, finally
语句用于在任何情况下(除特殊情况外)都必须执行的代码, throw
语句用于拋出异常, throws
语句用于声明可能会出现的异常。
在 Java 中通常采用 try catch
语句来捕获异常并处理,语法格式如下:
1: try { 2: // 可能发生异常的语句 3: } catch (ExceptionType e) { 4: // 处理异常语句 5: }
在以上语法中,把可能引发异常的语句封装在 try
语句块中,用以捕获可能发生的异常。 catch
后的 ()
里放匹配的异常类,指明 catch
语句可以处理的异常类型,发生异常时产生异常类的实例化对象。
在上面语法的处理代码块 1 中,可以使用以下 3 个方法输出相应的异常信息:
printStackTrace()
方法:指出异常的类型、性质、栈层次及出现在程序中的位置;getMessage()
方法:输出错误的性质;toString()
方法:给出异常的类型与性质。
如果 try
语句块中发生异常,那么一个相应的异常对象就会被抛出,然后 catch
语句就会依据所抛出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try
语句块中剩余的语句,转到 catch
语句块后面的第一条语句开始执行。
如果 try
语句块没有异常发生,那么 try
块正常结束,后面的 catch
语句块被跳过,程序将从 catch
语句块后的第一条语句开始执行。
多重 catch 语句
如果 try 代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在 try 后面跟有多个 catch 代码块。多 catch 代码块语法如下:
1: try { 2: // 可能会发生异常的语句 3: } catch(ExceptionType e) { 4: // 处理异常语句 5: } catch(ExceptionType e) { 6: // 处理异常语句 7: } catch(ExceptionType e) { 8: // 处理异常语句 9: ... 10: }
在多个 catch 代码块的情况下,当一个 catch 代码块捕获到一个异常时,其它的 catch 代码块就不再进行匹配。
*注:当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类,否则异常会被父类拦截,导致子类捕获不到。
try catch finally 语句
在实际开发中,根据 try catch
语句的执行过程, try 语句块和 catch 语句块有可能不被完全执行,而有些处理代码则要求必须执行。例如,程序在 try 块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。
Java 的垃圾回收机制不会回收任何物理资源,垃圾回收机制只回收内存中对象所占用的内存。
所以为了确保一定回收 try 块中打开的物理资源,异常处理机制提供了 finally 代码块,并且 Java 7 之后提供了自动资源管理(Automatic Resource Management)技术。
finally
语句可以与前面介绍的 try catch
语句块匹配使用,语法格式如下:
1: try { 2: // 可能会发生异常的语句 3: } catch(ExceptionType e) { 4: // 处理异常语句 5: } finally { 6: // 清理代码块 7: }
使用 try-catch-finally
语句需注意以下几点:
- try 块是必需的;
- catch 块和 finally 块至少出现其中之一。
一般情况下,无论是否有异常抛出,都会执行 finally 语句块中的语句。
除非在 try 块、catch 块中调用了退出虚拟机的方法 System.exit(int status)
,否则不管在 try 块或者 catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会执行。
通常情况下不在 finally 代码块中作用 return
或 throw
等导致方法终止的语句,否则将会导致 try 和 catch 代码块中的 return
和 throw
语句失效。
来看一段代码吧:
1: import java.util.Scanner; 2: 3: public class Test { 4: public static void main(String[] args) { 5: Scanner input = new Scanner(System.in); 6: System.out.println("Windows 系统已启动!"); 7: String[] pros = {"记事本", "计算器", "浏览器"}; 8: 9: try { 10: // 循环输出 pros 数组中的元素 11: for (int i = 0; i < pros.length; i++) { 12: System.out.println(i + 1 + ":" + pros[i]); 13: } 14: System.out.println("是否运行程序:"); 15: String answer = input.next(); 16: if (answer.equals("y")) { 17: System.out.println("请输入程序编号:"); 18: int no = input.nextInt(); 19: System.out.println("正在运行程序[" + pros[no - 1] + "]"); 20: } 21: } catch (Exception e) { 22: e.printStackTrace(); 23: } finally { 24: System.out.println("谢谢使用!"); 25: } 26: } 27: }
TODO 自动资源管理
throws 和 throw
Java 中的异常处理除了捕获异常和处理异常之外,还包括声明异常和抛出异常。
实现声明和抛出异常的关键字非常相似,它们是 throws
和 throw
。可以通过 throws
关键字在方法上声明该方法要抛出的异常,然后在方法内部通过 throw
抛出异常对象。
throws 声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws
声明的方法表示不处理异常,具体格式如下:
returnType method_name(paramList) throws Exception 1, Exception2, ... {...}
如果有多个异常类,它们之间用逗号分隔。这些异常类可以是方法中调用了可能抛出异常的方法而产生的异常,也可以是方法体中生成并抛出的异常。
使用 throws
声明抛出异常的思路是:
- 当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;
- 如果
main
方法也不知道如何处理这种类型的异常,也可以使用throws
声明抛出异常,该异常将交给 JVM 处理; - JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
来看一个例子吧。
假设,创建一个 readFile()
方法,该方法用于读取文件内容,在读取的过程中可能会产生 IOException
异常,但是在该方法中不做任何处理,而将可能发生的异常交给调用者处理。在 main()
方法中使用 try catch
捕获异常,并输出异常信息,代码如下:
1: import java.io.FileInputStream; 2: import java.io.IOException; 3: 4: public class Test { 5: public void readFile() throws IOException { 6: // 定义方法时声明异常 7: FileInputStream file = new FileInputStream("read.txt"); // 创建 FileInputStream 实例对象 8: int f; 9: while ((f = file.read()) != -1) { 10: System.out.println((char) f); 11: f = file.read(); 12: } 13: file.close(); 14: } 15: 16: public static void main(String[] args) { 17: Throws t = new Test(); 18: try { 19: t.readFile(); // 调用 readFile() 方法 20: } catch (IOException e) { 21: // 捕获异常 22: System.out.println(e); 23: } 24: } 25: }
以上代码,首先在定义 readFile()
方法时用 throws
关键字声明在该方法中可能产生的异常,然后在 main()
方法中调用 readFile()
方法,并使用 catch
语句捕获产生的异常。
方法重写时声明抛出异常的限制:
- 子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同;
- 子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
throw 抛出异常
与 throws
不同的是, throw
语句用来直接抛出一个异常,后接一个可抛出的异常类对象,语法格式如下:
throw ExceptionObject;
其中, ExceptionObject
必须是 Throwable
类或其子类的对象。
当 throw
语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch
语句,执行相应的异常处理程序。如果没有找到相匹配的 catch
语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。
throw
关键字不会单独使用,它的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类实例,而往往会抛出程序中已经产生的异常类的实例。