开发Java几年了,每天都在跟bug打交道
而解决bug的一大利器就是打印的异常了,今天来一起复习一下异常处理吧!
起源 |
万物皆有起源,研究一个事物要从为什么开始,为什么会有异常处理呢?
举一个例子
不知道你做项目时敲过登录模块没有,在登陆模块经常需要完成各种用户名密码校验,如下
if(password==null || password.length()<this.minLength || password.length()>this.maxLength){ // 长度不符合 return false; }
这个只是诸多验证的一部分,你会发现其实很多验证是写不完的,因为你不知道用户到底会做怎样的操作,但是程序员又尽可能的想要解决这个问题,就把这种可能会出问题的情况抽象出了一个类,异常类
如果正常就执行,如果不正常就处理,这就是我们的try catch finally了。
java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并且提高程序健壮性。异常类型保证了什么被抛出,异常堆栈跟踪保证了在哪抛出,异常信息回答了为什么会抛出
概念 |
异常的概念
- Java异常是Java提供的用于处理程序中错误的一种机制
- 所谓错误是指在程序中运行的过程中发生的一些异常事件:除0溢出、数组下标越界、所读取的文件不存在
- 设计良好的程序应在异常发生时提供处理这些错误的方法,使得程序不会因为异常的发生而阻断或产生不可预见的错误
- Java程序的执行过程中如出现异常事件,可以生成一个异常类对象,该对象封装了异常事件的信息,并将被提交给Java运行时系统,这个过程称为抛出
- 当Java运行时系统接收到异常对象时,会寻找这一异常的代码,并把当前异常对象交给其处理,这一过程称为捕获
异常的分类
1.Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
2.Error: 系统错误 无法处理,由Java虚拟机生成并抛出,包括动态链接失败、虚拟机错误等,程序对其不做处理
3.exception:所有异常类的父类,不知道你有没有发现,程序运行时一旦报错,都会有这个单词呢,其子类对应了各种各样可能出现的异常事件,一般需要用户显示的声明或捕获。
4.runtime exception:一类特殊的异常,如被0除、数组下标超范围等,其产生比较频繁,处理麻烦,如果用户显示的声明或捕捉将会对程序可读性和运行效率影响很大,因此由系统自动检测并将它们交给缺省的异常处理程序,用户可不必处理。
实现 |
case
public class Test{ public static void main(String[] args){ int[] a={1,2,3}; System.out.println(a[1]) try{ System.out.println(a[3]) }catch(ArithmeticException ae){ System.out.println("系统维护中,请联系管理员"); } } }
case:解析
a[]数组中只有3个元素,下标从0开始到2,所以打印a[1]没有问题,但是打印a[3]就会出现数组下标越界的错误,此时就会报错,这个错误属于ArithmeticException 算术运算异常。
处理异常一般使用try-catch语句,try为试运行,如果有异常就根据catch的条件分别进行处理,上述例子为算数异常,所以打印"系统维护中,请联系管理员"语句。
另外,catch不限制个数,可以写成
try{ System.out.println(a[3]) }catch(ArithmeticException ae){ System.out.println("系统维护中,请联系管理员"); }catch(Exception ae){ System.out.println("未知错误,请联系管理员"); }
深入 |
finally关键字
概念:
- finally语句为异常处理提供一个统一的出口,使得在控制流程转到程序的其他部分以前,能够对程序的状态作统一的管理
- 无论try所指定的程序块中是否抛出例外,finally所指定的代码都要被执行
写法:
try{ System.out.println(a[3]) }catch(ArithmeticException ae){ System.out.println("系统维护中,请联系管理员"); }finally{ return null; }
作用:
通常可以在finally语句中进行资源的清除工作,因为在try语句中一旦在某一行发生错误,此行之后的语句将不再执行,所以需要finally进行处理,可以用来关闭打开的文件、删除临时文件等。
throws和throw关键字
区别:
- throw用于抛出java.lang.Throwable类的一个实例化对象,意思是说你可以通过关键字throw抛出一个Exception,如:throw new lllegalArgumentException(“XXXXXXX”)
抛出异常
public void getFile(String fileName){ if(fileName == null){ throw new IOException();//意味着这里会抛出一个异常。 }
- 而throws的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。Java中任何未处理的受检查的异常强制在throws子句中声明
方法抛出
public void getFile(String fileName)throws IOException{//意味着这个复方法制会抛出百异常,并且由调度用这个方法的方法进行处理。知也就是把异道常向上抛了。 }
受检异常与非受检异常
Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
受检异常编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
非受检异常
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)
JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
常见的 RuntimeException
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针)
ArrayStoreException(数据存储异常,操作数组时类型不一致)
还有IO操作的BufferOverflowException异常
Java常见异常有哪些
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
异常处理-阿里巴巴Java开发手册
【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。正例:if (obj != null) {…} 反例:try { obj.method(); }catch (NullPointerException e) {…}
【强制】异常不要用来做流程控制,条件控制。说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。说明:如果JDK7及以上,可以使用try-with-resources方式。
【强制】不要在finally块中使用return。说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。反例:
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}
【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。
【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。
【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景:1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。反例:public int f() { returnInteger对象}, 如果为null,自动解箱抛NPE。2) 数据库的查询结果可能为null。3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。正例:使用JDK8的Optional类来防止NPE问题。
【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。
【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。说明:关于RPC方法返回方式使用Result方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是new自定义异常,加入自己的理解的errormessage,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto) {…}
小结
异常处理虽然强大,但是并不推荐滥用异常处理,因为异常处理不仅仅是抛出异常,在我看来更重要的还有,抓住异常之后,catch
代码块中的异常处理。try的代码块也不宜太长。