【Java基础系列】Java异常
本文目录:
1、异常是什么、为什么需要异常
2、如何使用异常
3、异常使用原则
4、阿里巴巴Java规约关于异常部分
全文内容:
1、异常是什么、为什么需要异常
C语言以及其他早期的编程语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接受者将对这个返回值或标志进行检查,以判定是否发生了错误。然而随着时间的推移,人们发现,高傲的程序员在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”,所以程序员不去检查错误情形也就不足为奇了。如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读,更别提维护了。
“异常”这个词有“我对此感到意外”的意思,问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,这个时候你需要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里作出正确的决定。使用异常所带来的另外一个好处是,它往往能够降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那么就不需要在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且只需要在一个地方处理错误,即所谓的“异常处理程序”中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使得代码的阅读、编写和调试工作更加井井有条。
总结为:通过异常处理错误,结构不佳的代码不能运行,充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性。但是如果使用不当也会带来负面影响。
2、如何使用异常
2.1、异常的参数
与使用Java中其它对象一样,我们总是用new关键字在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器,一个是默认的无参数构造器,另外一个是接受字符串作为参数,以便能够把相关信息放入异常对象的构造器。举例为:“throw new NullPointerException("t == null")”。关键字throw将产生许多有趣的结果,在使用new创建了异常对象之后,此对象的引用将传给throw。能够抛出任意类型的Throwable对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者使用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,异常对象中仅仅有的信息就是异常类型,除此之外不包含任何有意义的内容。)
2.2、捕获异常
要明白异常如何被捕获的,必须首先理解“监控区域(guarded region)”的概念,它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常,因为在这个块里“尝试”各种可能产生异常的方法调用,所以称之为try块。它是跟在try关键字之后的普通程序块。
try{
// code that might generate exceptions
}
总结为:有了异常处理机制,可以把所有动作放在try块里,然后只需要一个地方就可以捕获所有的异常,这意味着代码将更容易编写和阅读,因为完成任务的代码没有与错误检查的代码混在一起。
当然,抛出的异常必须在某处得到处理,这个“地点”就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在try块之后,以关键字catch表示:
try{
// code that might generate exceptions
}catch(Type1 id1){
// handle exceptions of type1
}catch(Type2 id2){
// handle exceptions of type2
}
总结为:当异常被抛出的时候,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch子句执行,此时认为异常得到了处理,一旦catch子句处理完成,则处理程序的查找过程结束。
关于“终止与恢复”概念为异常处理理论上有两种基本模型,Java支持终止模型,在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已经无法挽回,也不能回来继续执行。另外一种模型为恢复模型,意思是说异常处理程序的工作是修复错误,然后重新尝试调用出问题的方法,并认为第二次能成功,对于恢复模型,通常希望异常处理之后能继续执行程序。如果想要用Java实现类似恢复的行为,那么在遇见错误的时候,就不能抛出异常,而是调用方法来修正该错误。或者,把try块放在while循环里边,这样就不断地进入try块,直到得到满意的结果为止。
总结为:长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但是最终还是选择了“终止模型“的异常处理,并且忽略恢复行为。因为恢复模型虽然很诱人,但是不实用,其中的主要原因为可能是它所导致的耦合,增加了代码的编写和维护的困难。
2.3、创建自定义异常
要自定义异常,必须从已有的异常类继承,最好是选择意思相近的异常类继承,建立新的异常类型最简单的方法就是让编译器为你产生默认的构造器,所以这几乎不用写多少代码。
class SimpleException extends Exception{
}
public class InheritingExceptions{
public void f() throws SimpleException{
System.out.println("Throw SimpleException from f()");
throw new SimpleException();
}
public static void main(String[] args){
InheritingExceptions sed = new InheritingExceptions();
try{
sed.f();
}catch(SimpleException e){
System.out.println("Caught it!");
}
}
}
总结为:代码必须与异常说明保持一致,如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你存在编译错误,要么处理这个异常,要么就在异常说明中表明这个方法将会产生异常。
2.4、捕获所有异常
实际编码中可以只写一个异常处理程序来捕获所有类型的异常,通过捕获异常类型的基类Exception就可以做到这一点,代码为:
catch(Exception e){
// handle exceptions all
}
总结为:这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把这个异常捕获了。原因为Exception是与编程有关的所有异常类的基类,所以它不会含有太多具体的信息,不过可以调用它从基类Throwable继承的方法:
String getMessage() 获取异常message内容 String getLocalizedMessage() 用来获取更详细信息或用本地语言表示的详细信息 String toString() 返回对Throwable的简单描述,要是有更详细信息的话,也会把它包含在内 void printStackTrace() void printStackTrace(PrintStream) void printStackTrace(java.io.PrintWriter) 打印Throwable和Throwable的调用栈轨迹,调用栈显示了“把你带到异常抛出地点”的方法调用序列 Throwable fillInStackTrace() 用于在Throwable对象的内部记录栈帧的当前状态,这在程序重新抛出错误或异常时候很有用,另外也可以使用getClass()或者getSimpleName()方法
2.5、Java标准异常
Throwable这个Java类被用来表示任何可以作为异常被抛出的类,分为两种类型(指从Throwable继承而得到的类型):Error用来表示编译时和系统错误(除特殊情况之外,一般不用你关心)。Exception是可以被抛出的基本类型,在Java类库,用户方法以及运行时故障中都可能抛出Exception型异常。这里边需要特殊关注的“RuntimeException”运行期异常,这类异常会自动被Java虚拟机抛出,所以不必要在异常说明中把它们列出来。这类异常又称之为“不受检查异常”,unchecked exception。
总结为:Java程序设计语言提供了三种可抛出结构throwable:受检查的异常、运行时异常以及错误error。在决定使用受检查的异常或是不受检查的异常时候,主要原则为“如果期望调用者能够适当地恢复,对于这种情况就应该使用受检查的异常”,通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。因此,方法中声明要抛出的每个受检的异常,都是对API用户的一种潜在指示,与异常相关联的条件是调用这个方法的一种可能的结果。
关于异常复用的好处为:
第一:它使你的API更加易于学习和使用。
第二:对于用到这些API的程序而言,它们的可读性更好,因为它们不会出现很多程序员不熟悉的异常。
第三:异常类越少,意味着内存印迹footprint就越小,装载这些类的时间开销也越小。
最常见的异常类为:
IllegalArgumentException 非null的参数值不正确 IllegalStateException 对于方法调用而言,对象状态不合适 NullPointerException 在禁止使用null的情况下参数值为null IndexOutOfBoundsException 下标参数值越界 ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改 UnsupportedOperationException 对象不支持用户请求的方法,这个比较少见
3、异常使用原则
Java编程思想作者提到的9点使用异常条件:
1) 在恰当的级别处理问题,在知道该如何处理的情况下采捕获异常。
2) 解决问题并且重新调用产生异常的方法。
3) 进行少许修补,然后绕过异常发生的地方继续执行。
4) 用别的数据进行计算,以代替方法预计会返回的值。
5) 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
6) 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
7) 终止程序。
8) 进行简化,如果你的异常模式使得问题变得太复杂,那用起来会非常痛苦也很烦人。
9) 让类库和程序更加完全,为调试做短期投资,也是在为程序的健壮性做长期投资。
Effective Java作者提到的9条建议:
1) 只针对异常的情况才使用异常。一句话为:异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。
2) 对可恢复的情况使用受检查异常,对编程错误使用运行时异常。一句话:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检查的异常,通过抛出受检查异常,强迫调用者在一个catch子句中处理该异常,或者将它传播到更高层。
3) 避免不必要地使用受检查异常。一句话为:如果使用API的程序员无法做的比这个更好,那么未受检查的异常可能会更合适。}catch(TheCheckedException e){ System.exit(1) // Oh well, we lose}。
4) 优先使用标准的异常。一句话为:专家级程序员与缺乏经验的程序员一个最主要的区别在于,专家程序员追求并且通常也能够实现高度的代码重用。
5) 抛出与抽象相对应的异常。一句话为:如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。
6) 每个方法抛出的异常都要有文档。一句话为:如果没有为可能抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类或者接口。
7) 在细节消息中包含能捕获失败的信息。一句话为:为了捕获失败,异常的细节消息应该包含所有“对该异常有贡献”的参数和域的值,例如:IndexOutOfBoundsException异常的细节消息应该包含下界、上界以及没有落在界内的下标值。
8) 努力使失败保持原子性。一句话为:作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态。如果违反这条规则,API文档就应该清楚地指明对象将会处于什么样的状态。
9) 不要忽略异常。一句话为:当API的设计者声明一个方法将抛出某个异常的时候,他们等于正在试图说明某些事情,所以,请不要忽略它。
4、阿里巴巴Java规约关于异常部分
1. 【强制】Java 类库中定义的一类RuntimeException可以通过预先检查进行规避,而不应该通过catch来处理,比如:IndexOutOfBoundsException,NullPointerException等等。说明:无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过catch NumberFormatException来实现。 正例:if (obj != null) {...} 反例:try { obj.method() } catch (NullPointerException e) {...}
2. 【强制】异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
3. 【强制】对大段代码进行try-catch,这是不负责任的表现。catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
5. 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
6. 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。
7. 【强制】不能在finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。
8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
9. 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。调用方需要进行null判断防止NPE问题。 说明:本规约明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回null的情况。
10. 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景: 1) 返回类型为包装数据类型,有可能是null,返回int值时注意判空。 反例:public int f() { return Integer对象}; 如果为null,自动解箱抛NPE。 2) 数据库的查询结果可能为null。 3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。 4) 远程调用返回对象,一律要求进行NPE判断。 5) 对于Session中获取的数据,建议NPE检查,避免空指针。 6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。正例:可以使用JDK8的Optional类来防止NPE问题。
11. 【推荐】在代码中使用“抛异常”还是“返回错误码”,对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess、“错误码”、“错误简短信息”。 说明:关于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
12. 【推荐】定义时区分unchecked / checked 异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。
13. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto) {...}
posted on 2017-03-25 13:27 pinefantasy 阅读(229) 评论(0) 收藏 举报
浙公网安备 33010602011771号