异常的概念与体系结构
概念
异常简单理解就是不正常,Java中的异常就是程序在执行过程中出现不正常的行为称之为异常
常见的异常
1. 算数异常(常见的是除数为0)
System.out.println(10/0); //执行结果: Exception in thread "main" java.lang.ArithmeticException: / by zero
2.数组越界异常
int[] arr = {1,2,3}; System.out.println(arr[10]); //执行结果: Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
3. 空指针异常
int[] arr = null; System.out.println(arr.length); //执行结果: Exception in thread "main" java.lang.NullPointerException
从上述例子可以看出:不同的异常对应的异常描述不同,也就是每个异常都有对应的类来进行描述。
异常的体系结构
异常的种类非常的多,为了易于区分和管理,Java内部维护了一个异常的体系结构。见下图:
对上面图进行说明:
1.Throwable:是异常的顶层类,其派生出两个子类:Error和Exception
2. Error:指的是Java虚拟机无法解决的问题,此类问题比较严重,例如:JVM内部错误,资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,该类异常一但发生,就无法解决,就像人得了癌症。
3.Exception:该类异常产生后可由程序员进行分析及处理,使得程序继续运行,就比如我们人感冒咳嗽一样。
异常的分类
因为异常发生的时机是不一样的,我们根据此将异常分为:编译时异常,运行时异常。
1. 编译时的异常
它指的是在程序编译期间发生的异常,也可称之为受检查异常(Checked Exception)
2. 运行时的异常
它指的是在程序执行期间发生的异常,也称之为非受检查异常(Unchecked Exception)
RuntimeException以及其子类对应的异常都称为运行时异常,例如:NullPointerException等
注意:编译时出现的语法错误不能称为异常,比如将某个单词拼写错误,此时编译过程中就会出错,这是“编译期”出错,而运行时异常指的是,编译已经完成得到class文件,再由JVM执行过程中出现的错误。
异常的处理
防御式编程
1.LBYL:Look Before You Leap :指的是在对代码进行操作之前做充分的检查,即:事前防御
例:
boolean ret = false; ret = 登录游戏(); if(!ret){ 处理登录游戏错误; return; } ret = 开始匹配(); if(!ret){ 处理匹配错误; return; } ret = 游戏确认(); if(!ret){ 处理游戏确认错误; return; } ...... 这种处理方式的缺陷:正确的流程和处理错误的流程混在一起了,整体比较混乱,不易阅读 2.EAFP:It's Easier to Ask Forgiveness than Permission.指的是先进行操作,遇到了问题在处理,即:事后认错型 try{ 登录游戏(); 开始匹配(); 游戏确认(); ...... }catch(登录游戏异常){ 处理登录游戏异常; }catch(开始匹配异常){ 处理开始匹配异常; }catch(游戏确认异常){ 处理游戏确认异常; }
优势:正常和错误流程分开,代码清晰,易于阅读理解
异常处理的5个主要关键字:throw try catch final throws
异常的抛出
在写程序时,如果程序出现错误,就要将错误的信息告诉给调用者,在Java中,借助throw关键字,抛出一个指定的异常对象,将错误信息告知给调用者,具体语法如下:
throw new XXXException("异常产生的原因");
举一个数组传参的例子进行说明:
public static int getElement(int[] array,int index){ if(null==array){ throw new NullPointerException("传递数组为null"); } if(index<0||index>=array.length){ throw new ArrayIndexOutOfBoundsException("传递的数组下标越界"); } return array[index]; } public static void main(String[] args) { int[] array = {1,2,3}; getElement(array,3); }
运行结果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 传递的数组下标越界
注意事项:
1.throw必须写在方法体内部
2. 抛出的对象必须是Exception或者Exception的子类对象
3. 如果抛出的是RuntimeException或者它的子类,可以不用处理,JVM会帮我们处理
4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
5. 异常一旦抛出,其后的代码就不会执行
异常的捕获
异常的捕获也就是异常的具体处理方式,主要有两种:用throws进行异常声明以及try-catch捕获处理。
异常声明(使用关键字throws)
当程序抛出编译时异常时而且用户不想处理该异常,就可以借助throws将异常抛给方法的调用者来处理。
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{ }
注意事项:
1. throws必须跟在方法的参数列表之后
2. 声明的异常必须是Exception或者其子类
3. 方法内部如果抛出多个异常,throws之后跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,则直接声明父类即可
4.调用抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
try-catch捕获处理
throws对异常并没有处理,而是交给调用者处理,如果要对异常真正进行处理,就需要用try-catch
语法格式:
try{ //这里为可能出现异常的代码 }catch(要捕获的异常类型 e){ //当抛出异常与catch捕获异常类型相同时,异常就会被捕获到 //对异常进行处理完后,会跳出try-catch,继续执行后序代码 }[catch(异常类型 e){ //对异常处理 }finally{ //此处代码一定会被执行 }] //后序代码,异常被捕获处理了,后序代码就一定会执行 //异常没有捕获到,后序代码就不会执行
注意:[ ]中表示可选项,可以添加,也可以不添加
关于异常的处理方式:
1.异常的种类非常多,我们要根据不同的场景环境来决定
2. 对于更严重的问题(例如栈相关场景),应该让程序直接崩溃,防止造成更严重的后果
3.对于不太严重的问题,可以记录错误日志,并通过监控报警及时通知程序员
4. 对于可能会恢复的问题(网络相关场景),可尝试进行重试
注意事项:
1. try块内抛出异常位置之后的代码将不会被执行
2. 如果抛出异常类型与catch内的异常类型不匹配,即异常不会被成功捕获,也不会被处理,继续往外抛,直到JVM收到后中断程序---异常是按照类型来捕获的
3. try中可能会抛出多种类型异常,则必须用多个catch来捕获多种类型
4. 如果异常之间具有父子关系,一定是子类异常的catch在前,父类异常的catch在后,否则语法错误
由于Exception是所有异常类的父类,所以可以用这个类型表示捕捉所有异常
关键字finally
在写代码时,有些特定的代码无论程序是否发生异常都需要执行,比如程序中的打开资源,网络连接,数据库连接,IO等,在程序正常或者异常退出时,必须要对资源进行回收,另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这一问题的。
语法格式:
try{ //可能发生异常的代码 }catch(异常类型 e){ //对捕获的异常进行处理 }finally{ //这里的代码无论异常是否被捕获到,都会执行 } //如果没有异常或者异常被捕获处理了,这里后序的代码也执行
举数组的例子进行说明:
public static void main(String[] args) { try{ int[] array = {1,2,3}; array[10] = 10; array[0] = 10; }catch(ArrayIndexOutOfBoundsException e){ System.out.println(e); }finally{ System.out.println("这里代码一定执行"); } System.out.println("如果没有异常,或者异常被捕获处理,这里代码也将执行"); }
执行结果:
java.lang.ArrayIndexOutOfBoundsException: 10
这里代码一定执行
如果没有异常,或者异常被捕获处理,这里代码也将执行
这里存在了一个问题:既然finally和try-catch-finally后的代码都会执行,为什么还要有finally的?看完下面的代码,这个问题那就会迎刃而解。
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{ System.out.println("finally中的代码"); } System.out.println("try-catch-finally之后的代码"); if(null!=sc){ sc.close(); } return 0; } public static void main(String[] args) { int data = getData(); System.out.println(data); } 输入:10 运行结果: finally中的代码 10
上述程序,正常输入,return data后,方法就结束了,try-catch-finally后的代码根本没有执行,即输入流没有释放,造成资源泄露。
注意:finally中的代码一定会执行的,一般在finally中进行一些资源清理的扫尾工作。
如果finally中也存在return语句,则执行finally中的return语句,就不会执行try中的return语句
异常的处理流程
1. 程序先执行try中的代码
2. 如果try中的代码出现异常,就会结束try中的代码,与catch中的异常类型是否匹配
3. 如果找到匹配的异常类型,就会执行catch中的代码
4.如果没有找到匹配的类型,就会将异常向上传递到上层调用者
5. 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)
6.如果上层调用者也没有处理异常,就会继续向上传递,一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止