Java异常处理详解(非常详细)

在Java程序设计中,Java异常处理是非常关键和重要的一部分,程序异常处理的好坏直接影响到整个代码质量。

Java异常

异常的英文单词是exception,字面翻译就是“意外、例外”的意思,也就是非正常情况。

异常本质上是程序上的错误,包括程序逻辑错误和系统错误。比如使用空的引用、数组下标越界、内存溢出错误等,这些都是意外的情况,背离我们程序本身的意图。

Java异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,从而让程序尽最大可能恢复正常并继续执行。

 

异常分类

Java 所有的异常都是从Throwable继承而来的,其下有2个子接口,Error和Exception。

Java异常处理详解(非常详细)-mikechen

1.Error

Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序,因此我们编写程序时不需要关心这类异常。

2.Exception

Exception,是另外一个非常重要的异常子类,比如NullPointerException、IndexOutOfBoundsException、ClassCastException,这些异常是我们可以处理的异常。

3.Error和Exception的区别

异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。

4.可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)

Java异常处理详解(非常详细)-mikechen

1)红色为可查的异常(checked exceptions):编译器要求必须处置的异常

2)绿色为不可查的异常(unchecked exceptions):编译器不要求强制处置的异常

 

异常实现

Java异常机制用到的几个关键字:try、catch、finally、throw、throws。

Java异常处理详解(非常详细)-mikechen

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

Java异常处理详解(非常详细)-mikechen

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面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法