在Java程序设计中,Java异常处理是非常关键和重要的一部分,程序异常处理的好坏直接影响到整个代码质量。
Java异常
异常的英文单词是exception,字面翻译就是“意外、例外”的意思,也就是非正常情况。
异常本质上是程序上的错误,包括程序逻辑错误和系统错误。比如使用空的引用、数组下标越界、内存溢出错误等,这些都是意外的情况,背离我们程序本身的意图。
Java异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,从而让程序尽最大可能恢复正常并继续执行。
异常分类
Java 所有的异常都是从Throwable继承而来的,其下有2个子接口,Error和Exception。
1.Error
Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序,因此我们编写程序时不需要关心这类异常。
2.Exception
Exception,是另外一个非常重要的异常子类,比如NullPointerException、IndexOutOfBoundsException、ClassCastException,这些异常是我们可以处理的异常。
3.Error和Exception的区别
异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
4.可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)
1)红色为可查的异常(checked exceptions):编译器要求必须处置的异常
2)绿色为不可查的异常(unchecked exceptions):编译器不要求强制处置的异常
异常实现
Java异常机制用到的几个关键字:try、catch、finally、throw、throws。
1.try(监听异常)
用于监听,将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
2.catch(捕获异常)
catch用来捕获try语句块中发生的异常。
3.finally (总是会被执行)
finally语句块总是会被执行,它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。
只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
4.throw (抛出异常)
throw用于抛出异常。
5. throws(声明异常)
用在方法签名中,用于声明该方法可能抛出的异常,主方法上也可以使用throws抛出。如果在主方法上使用了throws抛出,就表示在主方法里面可以不用强制性进行异常处理,如果出现了异常,就交给JVM进行默认处理,则此时会导致程序中断执行
异常捕获
异常捕获处理的方法通常有:
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
1. try-catch
捕获单个异常
try{ //程序代码 }catch (ExceptionName e1){ //Catch块 }
捕获多个异常
try{ //程序代码 }catch (异常类型1 异常的变量名1){ ...... }catch (异常类型2 异常的变量名2){ ...... }catch (异常类型3 异常的变量名3){ ...... }
对于多个catch的情况,当try中程序发生异常,会按照从上往下的顺序与catch进行匹配,一旦与其中一个匹配后就不会再与后面的catch进行匹配了。
所以,在书写catch语句的时候,一定要把范围小的放在前面,范围大的放在后面!
2. try-catch-finally
finally 关键字用来创建在 try 代码块后面执行的代码块无论是否发生异常,finally 代码块中的代码总会被执行,在 finally 代码块中,可以运行清理类型等收尾善后性质的语句,finally 代码块出现在 catch 代码块最后。
语法如下:
try{ ...... }catch(异常类型1 异常的变量名1){ ...... }catch(异常类型2 异常的变量名2){ ...... }finally{ // 无论是否发生异常,都会执行的程序代码 }
3.try-finally
try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句。 try块没有引发异常,则执行完try块就执行finally语句。 try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等。
//以Lock加锁为例,演示try-finally ReentrantLock lock = new ReentrantLock(); try { //需要加锁的代码 } finally { lock.unlock(); //保证锁一定被释放 }
finally遇见如下情况不会执行
- 在前面的代码中用了System.exit()退出程序
- finally语句块中发生了异常
- 程序所在的线程死亡
- 关闭CPU
4.try-with-resource
在 JDK1.7 前,通常使用 try{}catch() 来捕获异常的,如果遇到类似 IO 流的处理,一般是在 finally 部分关闭 IO 流。
但在 JDK1.7 后,Java 7 的编译器和运行环境支持新的 try-with-resources 语句,称为 ARM 块。
为了能够配合try-with-resource,资源必须实现 AutoClosable 接口,该接口的实现类需要重写 close 方法:
public class Connection implements AutoCloseable { public void sendData() { System.out.println("正在发送数据"); } @Override public void close() throws Exception { System.out.println("正在关闭连接"); } }
调用实现
public class TryWithResource { public static void main(String[] args) { try (Connection conn = new Connection()) { conn.sendData(); } catch (Exception e) { e.printStackTrace(); } } }
异常实践
异常不仅仅是一个错误控制机制,也是一个通信媒介。
因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。
1.在 catch 块中,始终按从最特定到最不特定的顺序对异常排序
try { ... } catch(InvalidOperationException ex) { ... } catch(Exception ex) { ... }
2.不要忽略异常
public void doNotIgnoreExceptions() { try { // do something } catch (NumberFormatException e) { // this will never happen } }
捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,这样会导致外面无法知晓该方法发生了错误,无法确定定位错误原因。
3.异常不要用来做流程控制
举一个例子:
public static void CheckProductExists(int ProductId) { //... search for Product if (ProductId == 0) // no record found throw error { throw (new Exception("Product is Not found in inventory")); } else { Console.WriteLine("Product is available"); } }
异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
4.在你的方法里抛出定义具体的检查性异常
public void foo() throws Exception { //错误方式 }
一定要避免出现上面的代码示例,它简单地破坏了检查性异常的整个目的。
声明你的方法可能抛出的具体检查性异常,如果只有太多这样的检查性异常,你应该把它们包装在你自己的异常中,并在异常消息中添加信息, 如果可能的话,你也可以考虑代码重构。
public void foo() throws SpecificException1, SpecificException2 { //正确方式 }
5.永远不要捕获Throwable类
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // 错误方式 } }
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。
所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。
6.始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //错误方式 }
这破坏了原始异常的堆栈跟踪,并且始终是错误的。 正确的做法是:
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //正确方式 }
7.尽可能使用标准异常
如果使用内建的异常可以解决问题,就不要定义自己的异常。
Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。
尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
8.优先明确的异常
你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。
因此需要保证提供给他们尽可能多的信息,这样你的 API 更容易被理解。
因此,总是尝试寻找最适合你的异常事件的类。例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException ,避免抛出一个不明确的异常。
public void doNotDoThis() throws Exception {//错误方式 ... } public void doThis() throws NumberFormatException {//正确方式 ... }
9.捕获具体的子类而不是捕获 Exception 类
try { someMethod(); } catch (Exception e) { //错误方式 LOGGER.error("method has failed", e); }
捕获异常的问题是,如果稍后调用的方法为其方法声明添加了新的检查性异常,则开发人员的意图是应该处理具体的新异常,如果你的代码只是捕获异常(或 Throwable),永远不会知道这个变化。
10.要么记录异常要么抛出异常,但不要一起执行
catch (NoSuchMethodException e) { //错误方式 LOGGER.error("Some information", e); throw e; }
正如上面的代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且对尝试分析日志的同事很不友好。
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》