异常以及异常处理
目录
1 什么是异常
2 异常的分类
2.1 异常体系
2.2 异常分类
3 异常的处理
3.1 抛出异常
3.2 捕获异常
4 异常的处理流程
5 自定义异常
1 什么是异常
在Java中,异常是指在程序执行过程中发生的不正常的行为,会中断程序的正常执行。
在之前的学习中,我们也遇到了一些异常,例如:
1.算术异常
public static void main(String[] args){// 分母为 0System.out.println(10 / 0);
}// 异常信息
Exception in thread "main" java.lang.ArithmeticException: / by zero
2.数组越界异常
public static void main(String[] args){// 数组长度为5int[] array = {1,2,3,4,5};// 访问数组下标为 10 System.out.println(array[10]);
}// 异常信息
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
3.空指针异常
public static void main(String[] args){// 数组为空int[] s = null;System.out.println(s.length);
}
// 异常信息
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "s" is null
2 异常的分类
2.1 异常体系
Java内部维护了一个异常体系:
其中
- Throwable:是异常体系中的顶层类,其派生出了两个重要子类:Error和Exception
- Error:是指Java虚拟机无法解决的严重问题,称为错误。例如:JVM的内部错误,资源耗尽等,一旦发生就不能通过简单的代码进行处理。
- Exception:就是异常,在异常发生后,程序员可以通过代码进行处理,使得程序正常执行。
2.2 异常分类
上面提到的异常又可以分为两种,一种是编译的时候出现的异常,称为编译时异常,也称受查异常;另一种是程序运行时发生的异常,称为运行时异常,也称非受查异常。
1.受查异常:
- 受查异常是继承java.lang.Exception类,但是不包括java.lang.RuntimeException类的子类。
- 受查异常要求必须处理(要么使用try-catch捕获,要么使用throws声明)。
- 受查异常通常是由外部错误条件引起,这些条件在程序运行时可能经常发生,程序员预见这些异常情况并且在编译阶段就处理它们。
2.非受查异常:
- 非受查异常包括java.lang.RuntimeException的所有子类,RuntimeException是哪些可能在Java虚拟机正常操作期间抛出的异常的父类。
- 非受查异常不要求强制处理,它们要么是由编程引起的错误,要么是程序应该在运行时处理的异常。
注意:编译时出现的语法错误,不能称为异常。例如将System.out.println拼写错了,此时编译过程中就会出错,但是这不属于异常;运行时指的是程序已经编译通过得到class文件了,再由JVM执行过程中出现的错误。
3 异常的处理
在Java中处理异常最常用的两种方法就是抛出异常和捕获异常。
3.1 抛出异常
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知使用者,在Java中,可以借助throw关键字,进行抛出异常。
throw只是抛出异常,并没有进行处理。通俗来说就是,我告诉你了,这里有一个异常,你可以处理或者不处理都和我没有关系,反正我已经抛出了。
语法:
throw new 异常类型("异常产生的原因/要告知使用者的信息");
示例:
// 一个读取数组 array 中下标为 index 值的方法
public static int getKey(int[] array,int index){if(array == null){throw new NullPointerException("数组为空");}if(index < 0 || index > array.length - 1){throw new ArrayIndexOutOfBoundsException("下标越界");}return array[index];
}
注意:
- throw 必须写在方法体内部
- 抛出的异常必须是Exception或者Exception的子类
- 如果抛出的是RuntimeExcetion及其子类,就不需要处理,交给JVM处理即可
- 如果抛出的是受查异常,程序员必须进行处理,否则无法通过编译
- 异常一旦抛出,后面的代码就不会继续执行
3.2 捕获异常
捕获异常的方法有两种,一是在方法的参数列表之后使用throws进行声明,二是使用try-catch进行捕获。
1.throws声明
语法:
修饰符 返回值类型 方法名(参数列表) throws 异常类型;
throws只是声明这个方法中可能会出现某个异常,但是真的出不出现也不确定,也就是说在平时的方法中,我们可以在方法后面加上 throws ,但是如果方法中真的出现了这种异常,那么就要求必须在方法的参数列表之后进行 throws 的异常声明。
注意:
- throws 必须跟在方法的参数列表后面
- 声明的异常必须是Exception及其子类
- 方法内部如果抛出了多个异常,那么throws后也要跟多个异常,之间用逗号隔开,如果这些异常有父子关系,那么可以只声明父类的异常
- 调用声明抛出异常的方法时,调用者必须对异常进行处理,或者继续使用 throws 声明
2.try-catch捕获并处理
前面的throws其实并没有对异常进行真正的处理,只是将异常进行声明,真正要对异常处理,就需要使用try-catch。
语法:
try{// 可能出现异常的代码 } catch(出现异常的类型 e){// 如果 try 中的代码出现异常,此处catch的异常类型正好是该异常或者该异常的父类// 那么就会捕获到这个异常// 然后对异常进行处理,处理之后跳出catch继续执行后续代码 } finally{// 无论如何都会执行的代码 }
注意:
- try块中抛出异常之后的代码不会继续执行
- 如果抛出的异常类型和catch中的异常类型不匹配,即异常没有成功捕获,那么也不会对这个异常进行处理,相当于没有对这个异常进行任何的处理
- try块中的代码可能会出现多个不同的异常类型,这个时候需要使用多个catch块进行异常的捕获
如下:
try{// 异常1// 异常2// 异常3
}
catch(异常1的异常类型 e1){// 处理异常1的代码
}
catch(异常2的异常类型 e2){// 处理异常2的代码
}
catch(异常3的异常类型 e3){// 处理异常3的代码
}
finally{//一定执行的代码
}
如果这几个异常处理的方式一样,也可以写成
try{// 异常1// 异常2// 异常3
}
catch(异常1的异常类型 | 异常2的异常类型 | 异常3的异常类型 e){// 处理异常的代码
}
finally{//一定执行的代码
}
如果这几个异常之间存在父子关系,那一定要先写子类的catch,在写父类的catch
- 可以通过一个catch直接捕获所有异常,即使用Exception(不推荐,因为这样不便于看出抛出的是哪个异常)
3.finally
在写程序的时候,有些程序中会有一些不管怎么样都要执行的代码,这部分代码我们就可以写在finally块中,比如在程序中打开的资源等。
但是,事实是如果try-catch中捕获了异常并且正常进行了处理,那么try-catch-finally后的代码也会执行,那么finally存在的意义又是什么?下面我们通过一个例子来说明:
public static void main(String[] args){int a = getValue();System.out.println(a);
}
public static int getValue(){Scanner scanner = null;try{scanner = new Scanner(System.in);int a = scanner.nextInt();return a;}catch (InputMismatchException e){e.printStackTrace();}finally {System.out.println("finally中的代码...");}System.out.println("try-catch-finally后的代码...");if(scanner != null){scanner.close();}return 0;
}
// 运行结果
10
finally中的代码...
10
从上面的例子中可以看出,其中并没有执行try-catch-finally之后的代码,这就使得没有关闭scanner这个资源。
而finally的执行是在方法返回之前(try或者catch中有return时,finally也会在这个return之前执行),但是如果finally中也讯在return语句,那么就会执行finally中的人return(一般不会在finally中写return语句),所以finally中一般就会写一些资源释放的代码。
4 异常的处理流程
在处理异常的时候,如果没有找到合适的处理方式,就会沿着栈向上传递(其实就是,在方法a中,我们调用了方法b,但是进行异常处理时,调用方法b的这条语句本身没有发生异常,所以就会向上传递,在方法a中寻找异常并进行处理)。
示例:
public static void main(String[] args) {try{func();}catch(ArrayIndexOutOfBoundsException e){e.printStackTrace();}System.out.println("try-catch之后的代码...");
}
public static int func(){int array[] = {1,2,3};return array[3];
}// 运行结果
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3at exception.Demo2.func(Demo2.java:14)at exception.Demo2.main(Demo2.java:6)
try-catch之后的代码...
如果一直向上传递也没有找到合适的方法处理异常,那么最终就会交给JVM进行处理,也就是和我们没有写try-catch之前一样了。
public static void main(String[] args) {func();System.out.println("try-catch之后的代码...");
}
public static int func(){int array[] = {1,2,3};return array[3];
}// 运行结果
// 此时 System.out.println("try-catch之后的代码...") 就不会执行
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3at exception.Demo2.func(Demo2.java:18)at exception.Demo2.main(Demo2.java:5)
异常处理的流程:
- 程序先执行try中的代码
- 如果try中的代码出现异常,就会结束try中的代码,在catch中寻找异常类型是否匹配
- 如果找到匹配的catch,就执行该catch中的代码
- 如果没有找到匹配的catch,那就向上传递
- 无论有没有找到与该异常匹配的catch,finally中的代码都会在该方法结束之前执行
- 一直向上传递直到main方法中也没有找到与之匹配的异常类型的话,就交给JVM来进行处理,此时,程序也会因为异常中断
5 自定义异常
在开发中,有时候Java中已经内置的异常类不够我们使用,所以我们就需要自己进行定义。
示例:
实现一个登录系统的时候,当用户名或者密码输入错误的时候,我们需要抛出异常并且进行处理
public class UserLogin {private String userName = "admin";private String password = "123456";//定义一个密码异常类,属于受查异常,即编译时出现的异常class PasswordException extends Exception{public PasswordException(String msg){super(msg);}}//定义一个用户名异常类,属于非受查异常,即运行时出现的异常class UserNameException extends RuntimeException{public UserNameException(String msg){super(msg);}}public static void main(String[] args) {UserLogin userLogin = new UserLogin();// 进行异常的处理try{userLogin.login("admin","123456");}catch(PasswordException e){//只需要捕获受查异常即可e.printStackTrace();}}public void login(String userName,String password) throws PasswordException{if(!this.userName.equals(userName)){// 抛出但是不进行处理 受查异常throw new UserNameException("用户名错误!");}if(!this.password.equals(password)){throw new PasswordException("密码错误!");}}
}
注意:
- 自定义异常可以自己选择继承Exception还是RuntimeException
- 继承Exception的异常是受查异常,需要进行处理
- 继承RuntimeExcetion的异常是非受查异常,可以进行处理,也可以不处理