1. 异常的概念与体系结构
1.1 异常的概念
在我们的生活中,一个人如果表情痛苦,我们可能会问: 你是生病了吗? 需要我陪你去看医生吗?
程序也和人是一样的,均会发生一些"生病"的行为,比如: 数据格式不对, 数组越界,网络中断等, 我们把这种程序出现的"生病"行为称为"异常".比如在我们之前写代码经常遇到的"报红",大多数都是异常.
- 算数异常
- 数组越界异常
- 空指针异常
从上述的展示中可以看出,异常也分不同的类型,都有与其对应的类来进行描述,总的来说,异常本质上是一种类.
1.2 异常的体系结构
异常的种类很多,为了对不同类型的异常或错误进行更好的管理,Java内部制定了异常的体系结构:
从上图中我们可以看到:
- Throwable是异常体系的顶层类,由它派生出了两个子类:Error和Exception,即错误和异常.
- Error:指的是Java虚拟机无法解决的严重问题,如JVM内部错误,资源耗尽等,典型代表就是StackOverflowError(栈溢出错误).
- Exception: 异常产生后程序员可以通过修改代码进行处理,可以使得程序继续执行.
1.3 异常的分类
根据异常发生的时间不同,我们可以将异常分为:
- 编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受查异常,我们在用idea编译器时,编译器会在出现异常的语句下面划红线.
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
protected Object clone() {
return super.clone();
}
}
从上述的代码我们可以看出,在clone方法之后我们没有对发生的异常进行声明,所以在放重写的方法那里产生了编译时异常,在clone下面划了红线.
- 运行时异常
在程序执行的期间发生的异常,称为运行时异常,也称为非受查异常,RunTimeException以及子类对应的异常,都叫运行时异常,比如我们上面提到的: 算数异常(ArithmeticException),空指针异常(NullPointerException),数组越界异常(ArraryIndexOutOfBoundsException).
2. 异常的处理
2.1 防御式编程
错误在代码中是客观存在的.此时就需要把出现的错误和异常及时通知程序员,主要有以下两种方式:
- LBYL: look before your leap. 在操作之前就进行充分的检查,即:事前防御,语法格式如下:
boolean ret = false;
ret = 操作1;
if(!ret){
处理异常1;
return;
}
ret = 操作2;
if(!ret){
处理异常2;
return;
}
......
缺点: 正常的流程和错误的处理混在一起,代码显得比较凌乱.
2. EAFP: It’s Easier to ask forgiveness than permission. 事后获取原谅比事前获取许可更容易.也就是先操作,遇到的问题放在最后一起处理,即:事后认错型.
try{
操作1;
操作2;
......
}catch(异常1){
处理异常1;
}catch(异常2){
处理异常2;
}
.......
优势: 正常的流程和错误的处理是分开的,代码更加清晰,容易理解.
异常处理的核心思想就是EAFP
在Java中,处理异常主要有5个关键字: throw, try, catch, finally , throws.
2.2 异常的抛出
在编写程序时, 如果程序中出现错误, 此时就需要将错误的信息报告给调用者.
在Java中,可以使用throw关键字,抛出一个指定的异常对象,来将错误信息来报告给调用者.具体语法格式如下:
throw new xxxException ("产生异常的原因");
我们举一个例子来说明:例如我们要获取数组任意位置的元素和方法.
public static int getElement(int[] array,int index){
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
public static void main(String[] args) {
int[] array = {1,2,3};
getElement(array,3);
int[] array2 = null;
getElement(array2,2);
}
[注意事项]:
- throw必须写在方法内部
- 抛出的对象必须是Exception或者Exception的子类对象
- 如果抛出的是RunTimeException或者RunTimeException的子类,则可以不处理,直接交给jvm来处理
- 如果抛出的是编译时异常,用户必须处理,否者无法通过编译
- 异常一旦抛出,其后的代码就不会被执行
2.3 异常的捕获
异常的捕获,通常有两种: 异常的throws声明以及try-catch捕获处理
2.3.1 异常throws声明
处于方法声明的参数表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给调用者来处理,即当前方法不处理异常,提醒方法的调用者处理该异常.若调用者没有处理或throws,就会报错,语法格式如下:
修饰符号 返回值类型 方法名(参数列表)throws 异常类型1,异常类型2......{
}
下面我们拿我们上面提到的clone方法来举例
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class MyException {
public static void main(String[] args)throws CloneNotSupportedException {
Person p1 = new Person(12,"zhangsan");
Person p2 = (Person) p1.clone();
}
}
上述的代码我们可以看出如果我们在main方法调用clone方法的时候也没有直接处理异常,把异常throws了,该异常就会交给jvm来处理,若处理失败,程序会在出现异常的地方立即终止.
我们对上述代码进行一定地修正
public class Person implements Cloneable {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class MyException {
public static void main(String[] args) throws CloneNotSupportedException{
Person p1 = new Person(12,"zhangsan");
Person p2 = (Person) p1.clone();
}
}
上述代码我们对Person实现了Cloneable接口,jvm处理异常成功,程序运行之后便不会报错.
[注意事项]
- throws必须跟在方法的参数列表之后.
- 声明的异常必须是Exception或者是Exception的子类.
- 方法内部如果有多个异常,throws之后必须跟上多个异常,之间用逗号隔开,若抛出的异常具有父子关系,直接声明父类即可.
public static int getElement(int[] array,int index) throws RuntimeException{//声明运行时异常即可
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
- 调用抛出异常的方法时,必须对异常进行处理,或者继续使用throws抛出.
public static int getElement(int[] array,int index) throws RuntimeException{
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
public static void main(String[] args) throws RuntimeException{
int[] array = {1,2,3};
getElement(array,3);
int[] array2 = null;
getElement(array2,2);
}
2.3.2 捕获并处理
throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch.
语法格式如下:
try{
操作1;
操作2;
}catch(异常1){
处理异常1;
}catch(异常2){
处理异常2;
}finally{
此处的代码一定会被执行
}
//后续代码
如果捕获异常成功,并且异常被处理成功,会跳出try-catch结构,之后的代码也会被执行,若有异常没有被捕获到,后续的代码就不会被执行.我们下面举个例子:
public static int getElement(int[] array,int index){
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
public static void main(String[] args) {
try {
int[] array = {1,2,3};
getElement(array,3);
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("处理了数组越界异常");
}
System.out.println("异常处理结束,后续代码被执行");
}
关于异常的处理方式
异常的种类有很多, 我们要根据不同的业务场景来决定.
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.
[注意事项]
- try代码块中,如果有一个地方抛出了异常,这个地方之后的代码不会被执行.
public static int getElement(int[] array,int index) throws RuntimeException{
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
public static void main(String[] args) {
try {
int[] array = {1,2,3};
getElement(array,3);
int[] array2 = null;
getElement(array2,2);
}catch (NullPointerException e){
System.out.println("处理了空指针异常");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("处理了数组越界异常");
}
System.out.println("异常处理结束,后续代码被执行");
}
由此可见,运行结果中并,没有打印空指针异常.即可说明上述一点.
2. 如果异常类型与catch时的异常类型不匹配,异常就不会被捕获成功,也不会被处理,继续抛出,知道JVM收到后终止.
ublic static void main(String[] args) {
try {
int[] array = {1,2,3};
getElement(array,3);
}catch (NullPointerException e){
System.out.println("处理了空指针异常");
}
System.out.println("异常处理结束,后续代码被执行");
}
public static int getElement(int[] array,int index) throws RuntimeException{
if (array == null){
throw new NullPointerException("element of array is null");
}
if (index > array.length-1||index < 0){
throw new ArrayIndexOutOfBoundsException("index is out of array");
}
return array[index];
}
3. try中可能会有多个异常,必须用多个catch来捕获
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
} catch (NullPointerException e) {
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
若多个异常的处理方式完全相同,也可以写成这样:
catch (ArrayIndexOutOfBoundsException|NullPointerException){
......
}
- 如果异常之间有父子关系,一定是子类在前,父类在后.
public static void main(String[] args) {
try {
int[] array = {1,2,3};
getElement(array,3);
}catch (Exception e){
System.out.println("处理了异常");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("处理了数组越界异常");
}
System.out.println("异常处理结束,后续代码被执行");
}
数组越界异常已经被第一个catch处理了,该异常的捕获类型属于数组越界处理的父类,而后面的子类没有处理到,所以会报错.
2.3.3 finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收.另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的.
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public static void main(String[] args) {
try {
int[] array = {1,2,3};
getElement(array,3);
}catch (NullPointerException e){
System.out.println("处理了空指针异常");
}finally {
System.out.println("一定会被执行");
}
System.out.println("异常处理结束,后续代码被执行");
}
我们从上述执行结果可以看到,无论异常有没有被捕获到,finally一定会被执行到.
finally的代码一般用来进行一些资源清理的扫尾工作.例如:
public class TestFinally {
public static int getData(){
Scanner sc = null;
try{
sc = new Scanner(System.in);
int data = sc.nextInt();
return data;
}catch (InputMismatchException e){
e.printStackTrace();
}finally {
if(null != sc){
sc.close();
}
return 0;
}
从上述代码中,我们知道如果sc被成功调用,直接返回了,若没有finally语句,sc的资源没有进行关闭,造成了资源泄露.
面试题
- throw 和 throws 的区别?throw用来抛出异常,throws用来声明异常,提醒调用者.
- finally中的语句一定会执行吗?一定会.
2.4 异常处理的流程 (总结)
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
3. 自定义异常
Java出了本身给出的异常类之外,我们还可以自定义异常,在重写的构造方法中,我们可以自定义异常原因.
ublic class PassWordException extends RuntimeException{//继承于运行时异常
public PassWordException(String message) {//构造方法输入出现异常的原因
super(message);
}
}
public static void main(String[] args) {
String password = "123457";
if (password != "123456"){
throw new PassWordException("password is false");
}
}
[注意事项]
继承自Exception时默认是编译时异常.