异常概念
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException的异常。
异常发生的原因有很多,通常包含以下几大类:
用户输入了非法数据。
要打开的文件不存在。
网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
异常类的继承体系
从上面可知,所有的异常类都是Throwable的子类,Throwable派生了Error和Exception这两个子类
Error(错误):所有的子类及本身的实例,代表了虚拟机相关的错误,比如系统崩溃、虚拟机错误等等。此类错误是不能被开发者通过代码捕获处理的,正常情况下Error是很少出现的。
Exception(异常):所有的子类及本身,代表程序运行时发生了各种不期待的事件,这些事件能被java异常处理机制捕获处理,是异常处理的核心。
异常处理的基本语法
在捕获处理异常的编码时,有两种处理方式:
- 使用try…catch…finally语句处理
- 在函数名中使用throws声明交给函数调用者解决
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主要做一些清理工作,如流的关闭,数据库连接的关闭等。
}
在异常捕获语法结构上,try块是必需的,而catch和finally必须选择其中某一个进行组合,否则编译会报错的。
try {
} catch (Exception e) {
}
try {
}finally{
}
例子:
public class ExceptionTest {
public String test;
public static void main(String[] args) {
try {
//空指针异常
ExceptionTest exceptionTest=null;
System.out.println(exceptionTest.test);
//可能数组越界异常……
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println("两个数相除的结果:"+c);
} catch(IndexOutOfBoundsException ie){
System.out.println("数组越界:运行时输入参数个数不足");
}catch (NumberFormatException ne) {
System.out.println("数字格式异常:接受的不是数字格式的参数");
}catch (ArithmeticException ae) {
System.out.println("算术异常:比如6/0");
}catch (NullPointerException nue) {
System.out.println("空指针异常:对象为null");
}catch (Exception e) {
System.out.println("未知异常");
}
}
}
上面的代码存在两处异常,在执行运行时只会抛出第一个异常点,也就是说当try中的代码只要有一处出现了异常,该处后面的代码是不会在执行了,同时需要注意的是,异常捕获时,不仅要把Exception类对应的catch块放在最后,而且还需把Exception的子类排放在Exception类的前面,否则会报编译错误,总之在异常捕获时,要先捕获小异常,再捕获大异常。在java 7中开始引入了多异常捕获的功能,如下:
try {
//空指针异常
ExceptionTest exceptionTest=null;
System.out.println(exceptionTest.test);
//可能数组越界异常……
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println("两个数相除的结果:"+c);
//此处是多异常捕获,类型之间用"|"隔开
} catch(IndexOutOfBoundsException | NullPointerException ine){
System.out.println("数组越界或者空指针异常");
}
finally块
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally总会被执行的,一般情况finally块不会被执行,就是System.exit()被调用了或者try块没有被执行。try块一般是业务代码的实现以一些资源的打开(网络的链接,磁盘文件的读取),通常资源的关闭会在finally块中执行。
public static void testFinally() {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
//资源的回收工作
try {
if (fileInputStream != null) {
//关闭资源
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("finally块里的资源回收");
}
}
}
使用throws声明异常
使用throws声明抛出异常的 基本思路是,当前方法不处理或者不知道如何处理这种类型的异常,会把该类型异常抛给调用者处理,如果调用者还是无法处理,会继续把异常抛给main方法,若main方法也不知道如何处理,最终会交给虚拟机JVM处理,而JVM处理的方式,就是中止程序运行。
public static void main(String[] args) throws FileNotFoundException {
testThrows();
}
public static void testThrows() throws FileNotFoundException{
//因为FileInputStream的构造函数声明了抛出FileNotFoundException
//所以FileInputStream的代码需要放在try……catch中,要么使用throws声明抛出异常
//这里使用throws声明
FileInputStream fileInputStream = new FileInputStream("a.txt");
}
使用throw抛出异常语句
使用者可以在可能出现的异常处,通过throw语句手动抛出一个异常,throw语句后面不是一个异常类,是一个异常对象。
private static void testThrow(User user) {
if (user==null) {
throw new NullPointerException("用户不存在");
}
}
Exception in thread "main" java.lang.NullPointerException: 用户不存在
at exception.ExceptionTest.testThrow(ExceptionTest.java:48)
at exception.ExceptionTest.main(ExceptionTest.java:42)
自定义异常类
自定义异常类都会继承于Exception基类进行扩展,如果希望自定义运行时的异常,则需要继承于RuntimeException为基类。一般自定义异常类会提供两个构造函数,一个无参的,另外一个带字符串形参,字符串主要是描述具体异常信息,可以try…catch的方式捕获异常在catch块中通过getMessage()得到异常信息具体内容。
//自定义异常类,继承于运行时RuntimeException类
public static class ActionException extends RuntimeException{
private static final long serialVersionUID = 1L;
//无参构造函数
public ActionException() {
super();
}
//带字符串形参的构造函数,message主要是对异常的描述
public ActionException(String message) {
super(message);
}
}
public static void testCustomException(User user){
if (user==null) {
throw new ActionException("用户信息不能为null");
}
}
public class User{
}
try {
testCustomException(null);
} catch (Exception e) {
String message = e.getMessage();
System.out.println(message);
}
finally块与return
1.finally块何时执行
前面提到了只要有两种情况finally块是不执行的,一种是JVM退出了,另外一种是try块没有被执行到,就是提前结束了。也就是说在没有出现上面两种情况的前提下,无论如何不管是否存在异常的抛出,finally代码块都会被执行。
public static void test1(){
try {
System.out.println("test1:我在try块中");
System.exit(0);
} catch (Exception e) {
System.out.println("test1:我在catch块中");
//在没有异常抛出的前提下,System.exit()方法放在catch块是没有效果的
// System.exit(0);
}finally{
System.out.println("test1:我在finally块中");
}
// 输出:test1:我在try块中
}
public static void test2(){
test2_2();
try {
System.out.println("test2:我在try块中");
System.exit(0);
} catch (Exception e) {
System.out.println("test2:我在catch块中");
}finally{
System.out.println("test2:我在finally块中");
}
//没有输出结果
}
public static void test2_2(){
return;
}
2. finally块与return的执行顺序
finally与returm的使用,会使整个代码逻辑变的相对复杂些,如果稍微不注意可能会达不到自己想要的效果,下面是自己测试总结的几个点,可能不是很全面。
2.1. 在没有返回结果的方法,try块中使用return时,finally块中修改会影响改变值(也是最终值),同时try…catch…finally块后面的代码是不会被执行。
public static void test3(){
int a=0;
try {
a=10;
System.out.println("try: "+a);
return;
} catch (Exception e) {
e.printStackTrace();
}finally{
a=20;
System.out.println("finally: "+a);
}
System.out.println("end: "+a);
/**
* 输出结果:
* try: 10
finally: 20
*/
}
2.2. 在有返回结果的方法中,在finally块中会改变其值,但未必就是最终的返回值,主要要看return方法的调用位置是如何。
测试1: 在finally代码块中没有return返回值
public static void main(String[] args) {
System.out.println("result: "+test4());
}
//使用@SuppressWarnings阻止警告提示
@SuppressWarnings("finally")
public static int test4(){
int a=0;
try {
a=10;
System.out.println("try: "+a);
return a;
} catch (Exception e) {
e.printStackTrace();
}finally{
a=20;
System.out.println("finally: "+a);
}
System.out.println("end: "+a);
return a;
}
输出结果:
try: 10
finally: 20
result: 10
测试2:在finally代码块中有return返回值
public static void main(String[] args) {
System.out.println("result: "+test5());
}
//使用@SuppressWarnings阻止警告提示
@SuppressWarnings("finally")
public static int test5(){
int a=0;
try {
a=10;
System.out.println("try: "+a);
return a;
} catch (Exception e) {
e.printStackTrace();
}finally{
a=20;
System.out.println("finally: "+a);
return a;
}
// System.out.println("end: "+a);
// return a;
}
输出结果:
try: 10
finally: 20
result: 20
从上面的结果可以看出,也验证了2.2的结论,在测试2用例,需要注意一点就是,如果在finally使用了return,异常代码块后面试不允许存在代码的,否则会编译报错。
2.3.当发生异常后,catch块中的return和未发生异常的try块中的return返回情况是完全一样的
public static void test6() {
int a = 0;
try {
a=10/0;
System.out.println("try: " + a);
} catch (Exception e) {
a = 10;
System.out.println("catch: " + a);
return;
} finally {
a = 20;
System.out.println("finally: " + a);
}
System.out.println("end: " + a);
/**
* 输出结果: catch: 10 finally: 20
*/
}
关于finally与return执行顺序,不单单只有上面这几种情况。基于于此个人总结如下
- 尽量不要把return放在try和finally块中,最好把return放在try…catch…finally后面
- 在finally块尽量不要做太多的工作,最好是用来释放资源。
参考了如下