夯实Java基础系列10深入理解Java中的异常体系

2次阅读

共计 19575 个字符,预计需要花费 49 分钟才能阅读完成。

目录

  • 为什么要使用异常
  • 异常基本定义
  • 异常体系
  • 初识异常
  • 异常和错误
  • 异常的处理方式
  • “ 不负责任 ” 的 throws
  • 纠结的 finally
  • throw : JRE 也使用的关键字
  • 异常调用链
  • 自定义异常
  • 异常的注意事项
  • 当 finally 遇上 return
  • JAVA 异常常见面试题
  • 参考文章
  • 微信公众号

    • Java 技术江湖
    • 个人公众号:黄小斜

– Java 异常

本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-…

喜欢的话麻烦点下 Star 哈

文章首发于我的个人博客:

www.how2playlife.com

本文是微信公众号【Java 技术江湖】的《夯实 Java 基础系列博文》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从入门到进阶,一步步地学习 Java 基础知识,并上手进行实战,接着了解每个 Java 知识点背后的实现原理,更完整地了解整个 Java 技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。

如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java 技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

<!– more –>

为什么要使用异常

首先我们可以明确一点就是异常的处理机制可以确保我们程序的健壮性,提高系统可用率。虽然我们不是特别喜欢看到它,但是我们不能不承认它的地位,作用。

在没有异常机制的时候我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷:

1、容易混淆。如果约定返回值为 -11111 时表示出现异常,那么当程序最后的计算结果真的为 -1111 呢?

2、代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。

3、由调用函数来分析异常,这要求程序员对库函数有很深的了解。

在 OO 中提供的异常处理机制是提供代码健壮的强有力的方式。使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。

而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。

这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。(摘自《Think in java》)。

该部分内容选自 http://www.cnblogs.com/chenss…

异常基本定义

在《Think in java》中是这样定义异常的:异常情形是指阻止当前方法或者作用域继续执行的问题。在这里一定要明确一点:异常代码某种程度的错误,尽管 Java 有异常处理机制,但是我们不能以“正常”的眼光来看待异常,异常处理机制的原因就是告诉你:这里可能会或者已经产生了错误,您的程序出现了不正常的情况,可能会导致程序失败!

那么什么时候才会出现异常呢?只有在你当前的环境下程序无法正常运行下去,也就是说程序已经无法来正确解决问题了,这时它所就会从当前环境中跳出,并抛出异常。抛出异常后,它首先会做几件事。

首先,它会使用 new 创建一个异常对象,然后在产生异常的位置终止程序,并且从当前环境中弹出对异常对象的引用,这时。异常处理机制就会接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序。

总的来说异常处理机制就是当程序发生异常时,它强制终止程序运行,记录异常信息并将这些信息反馈给我们,由我们来确定是否处理异常。

异常体系

[外链图片转存失败(img-KNxcBTK0-1569073569353)(https://images0.cnblogs.com/b…]

从上面这幅图可以看出,Throwable 是 java 语言中所有错误和异常的超类(万物即可抛)。它有两个子类:Error、Exception。

Java 标准库内建了一些通用的异常,这些类以 Throwable 为顶层父类。

Throwable 又派生出 Error 类和 Exception 类。

错误:Error 类以及他的子类的实例,代表了 JVM 本身的错误。错误不能被程序员通过代码处理,Error 很少出现。因此,程序员应该关注 Exception 为父类的分支下的各种异常类。

异常:Exception 以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。

总体上我们根据 Javac 对异常的处理要求,将异常类分为 2 类。

非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac 在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用 try…catch…finally)这样的异常,也可以不处理。

对于这些异常,我们应该修正代码,而不是去通过异常处理器处理。这样的异常发生的原因多半是代码写的有问题。如除 0 错误 ArithmeticException,错误的强制类型转换错误 ClassCastException,数组索引越界 ArrayIndexOutOfBoundsException,使用了空对象 NullPointerException 等等。

检查异常(checked exception):除了 Error 和 RuntimeException 的其它异常。javac 强制要求程序员为这样的异常做预备处理工作(使用 try…catch…finally 或者 throws)。在方法中要么用 try-catch 语句捕获它并处理,要么用 throws 子句声明抛出它,否则编译不会通过。

这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如 SQLException , IOException,ClassNotFoundException 等。

需要明确的是:检查和非检查是对于 javac 来说的,这样就很好理解和区分了。

这部分内容摘自 http://www.importnew.com/2661…

初识异常

异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的 caller 都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。

异常最先发生的地方,叫做异常抛出点。

public class 异常 {public static void main (String [] args )
    {System . out. println( "---- 欢迎使用命令行除法计算器 ----") ;
        CMDCalculate ();}
    public static void CMDCalculate ()
    {Scanner scan = new Scanner ( System. in);
        int num1 = scan .nextInt () ;
        int num2 = scan .nextInt () ;
        int result = devide (num1 , num2) ;
        System . out. println("result:" + result) ;
        scan .close () ;}
    public static int devide (int num1, int num2){return num1 / num2 ;}

//    ---- 欢迎使用命令行除法计算器 ----
//            1
//            0
//    Exception in thread "main" java.lang.ArithmeticException: / by zero
//    at com.javase. 异常. 异常.devide(异常.java:24)
//    at com.javase. 异常. 异常.CMDCalculate(异常.java:19)
//    at com.javase. 异常. 异常.main(异常.java:12)

//  ---- 欢迎使用命令行除法计算器 ----
//    r
//    Exception in thread "main" java.util.InputMismatchException
//    at java.util.Scanner.throwFor(Scanner.java:864)
//    at java.util.Scanner.next(Scanner.java:1485)
//    at java.util.Scanner.nextInt(Scanner.java:2117)
//    at java.util.Scanner.nextInt(Scanner.java:2076)
//    at com.javase. 异常. 异常.CMDCalculate(异常.java:17)
//    at com.javase. 异常. 异常.main(异常.java:12)

[外链图片转存失败(img-9rqUQJQj-1569073569354)(http://incdn1.b0.upaiyun.com/…]

从上面的例子可以看出,当 devide 函数发生除 0 异常时,devide 函数将抛出 ArithmeticException 异常,因此调用他的 CMDCalculate 函数也无法正常完成,因此也发送异常,而 CMDCalculate 的 caller——main 因为 CMDCalculate 抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。

这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的 caller 中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由 main 函数抛给 JRE,导致程序终止。

上面的代码不使用异常处理机制,也可以顺利编译,因为 2 个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。

代码中我选择使用 throws 声明异常,让函数的调用者去处理可能发生的异常。但是为什么只 throws 了 IOException 呢?因为 FileNotFoundException 是 IOException 的子类,在处理范围内。

异常和错误

下面看一个例子

// 错误即 error 一般指 jvm 无法处理的错误
// 异常是 Java 定义的用于简化错误处理流程和定位错误的一种工具。public class 错误和错误 {Error error = new Error();

    public static void main(String[] args) {throw new Error();
    }

    // 下面这四个异常或者错误有着不同的处理方法
    public void error1 (){
        // 编译期要求必须处理,因为这个异常是最顶层异常,包括了检查异常,必须要处理
        try {throw new Throwable();
        } catch (Throwable throwable) {throwable.printStackTrace();
        }
    }
    //Exception 也必须处理。否则报错,因为检查异常都继承自 exception,所以默认需要捕捉。public void error2 (){
        try {throw new Exception();
        } catch (Exception e) {e.printStackTrace();
        }
    }

    //error 可以不处理,编译不报错, 原因是虚拟机根本无法处理,所以啥都不用做
    public void error3 (){throw new Error();
    }

    //runtimeexception 众所周知编译不会报错
    public void error4 (){throw new RuntimeException();
    }
//    Exception in thread "main" java.lang.Error
//    at com.javase. 异常. 错误.main(错误.java:11)

}

异常的处理方式

在编写代码处理异常时,对于检查异常,有 2 种不同的处理方式:

使用 try…catch…finally 语句块处理它。

或者,在函数签名中使用 throws 声明交给函数调用者 caller 去解决。

下面看几个具体的例子,包括 error,exception 和 throwable

上面的例子是运行时异常,不需要显示捕获。
下面这个例子是可检查异常需,要显示捕获或者抛出。

@Test
public void testException() throws IOException
{
    //FileInputStream 的构造函数会抛出 FileNotFoundException
    FileInputStream fileIn = new FileInputStream("E:\\a.txt");

    int word;
    //read 方法会抛出 IOException
    while((word =  fileIn.read())!=-1)
    {System.out.print((char)word);
    }
    //close 方法会抛出 IOException
    fileIn.close();}

一般情况下的处理方式 try catch finally

public class 异常处理方式 {

@Test
public void main() {
    try{
        //try 块中放可能发生异常的代码。InputStream inputStream = new FileInputStream("a.txt");

        // 如果执行完 try 且不发生异常,则接着去执行 finally 块和 finally 后面的代码(如果有的话)。int i = 1/0;
        // 如果发生异常,则尝试去匹配 catch 块。throw new SQLException();
        // 使用 1.8jdk 同时捕获多个异常,runtimeexception 也可以捕获。只是捕获后虚拟机也无法处理,所以不建议捕获。}catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){System.out.println(exception.getMessage());
        // 每一个 catch 块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7 中可以将多个异常声明在一个 catch 中。//catch 后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个 catch 块来处理异常。// 在 catch 块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个 catch 块中的局部变量,其它块不能访问。// 如果当前 try 块中发生的异常在后续的所有 catch 中都没捕获到,则先去执行 finally,然后到这个函数的外部 caller 中去匹配异常处理器。// 如果 try 中没有发生异常,则所有的 catch 块将被忽略。}catch(Exception exception){System.out.println(exception.getMessage());
        //...
    }finally{
        //finally 块通常是可选的。// 无论异常是否发生,异常是否匹配被处理,finally 都会执行。//finally 主要做一些清理工作,如流的关闭,数据库连接的关闭等。}

一个 try 至少要跟一个 catch 或者 finally

    try {int i = 1;}finally {// 一个 try 至少要有一个 catch 块,否则,至少要有 1 个 finally 块。但是 finally 不是用来处理异常的,finally 不会捕获异常。}
}

异常出现时该方法后面的代码不会运行,即使异常已经被捕获。这里举出一个奇特的例子,在 catch 里再次使用 try catch finally

@Test
public void test() {
    try {throwE();
        System.out.println("我前面抛出异常了");
        System.out.println("我不会执行了");
    } catch (StringIndexOutOfBoundsException e) {System.out.println(e.getCause());
    }catch (Exception ex) {
    // 在 catch 块中仍然可以使用 try catch finally
        try {throw new Exception();
        }catch (Exception ee) { }finally {System.out.println("我所在的 catch 块没有执行,我也不会执行的");
        }
    }
}
// 在方法声明中抛出的异常必须由调用方法处理或者继续往上抛,// 当抛到 jre 时由于无法处理终止程序
public void throwE (){//        Socket socket = new Socket("127.0.0.1", 80);

        // 手动抛出异常时,不会报错,但是调用该方法的方法需要处理这个异常,否则会出错。//        java.lang.StringIndexOutOfBoundsException
//        at com.javase. 异常. 异常处理方式.throwE(异常处理方式.java:75)
//        at com.javase. 异常. 异常处理方式.test(异常处理方式.java:62)
        throw new StringIndexOutOfBoundsException();}

其实有的语言在遇到异常后仍然可以继续运行

有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式)

而 Java 则是让执行流恢复到处理了异常的 catch 块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)

“ 不负责任 ” 的 throws

throws 是另一种处理异常的方式,它不同于 try…catch…finally,throws 仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{//foo 内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。}

纠结的 finally

finally 块不管异常是否发生,只要对应的 try 执行了,则它一定也执行。只有一种方法让 finally 块不执行:System.exit()。因此 finally 块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。

良好的编程习惯是:在 try 块中打开资源,在 finally 块中清理释放这些资源。

需要注意的地方:

1、finally 块没有处理异常的能力。处理异常的只能是 catch 块。

2、在同一 try…catch…finally 块中,如果 try 中抛出异常,且有匹配的 catch 块,则先执行 catch 块,再执行 finally 块。如果没有 catch 块匹配,则先执行 finally,然后去外面的调用者中寻找合适的 catch 块。

3、在同一 try…catch…finally 块中,try 发生异常,且匹配的 catch 块中处理异常时也抛出异常,那么后面的 finally 也会执行:首先执行 finally 块,然后去外围调用者中寻找合适的 catch 块。

public class finally 使用 {public static void main(String[] args) {
        try {throw new IllegalAccessException();
        }catch (IllegalAccessException e) {// throw new Throwable();
            // 此时如果再抛异常,finally 无法执行,只能报错。//finally 无论何时都会执行
            // 除非我显示调用。此时 finally 才不会执行
            System.exit(0);

        }finally {System.out.println("算你狠");
        }
    }
}

throw : JRE 也使用的关键字

throw exceptionObject

程序员也可以通过 throw 语句手动显式的抛出一个异常。throw 语句的后面必须是一个异常对象。

throw 语句必须写在函数中,执行 throw 语句的地方就是一个异常抛出点,== 它和由 JRE 自动形成的异常抛出点没有任何差别。==

public void save(User user)
{if(user  == null) 
          throw new IllegalArgumentException("User 对象为空");
      //......
 
}

后面开始的大部分内容都摘自 http://www.cnblogs.com/lulipr…

该文章写的十分细致到位,令人钦佩,是我目前为之看到关于异常最详尽的文章,可以说是站在巨人的肩膀上了。

异常调用链

异常的链化

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设 B 模块完成自己的逻辑需要调用 A 模块的方法,如果 A 模块发生异常,则 B 也将不能完成而发生异常。

== 但是 B 在抛出异常时,会将 A 的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。==

异常链化: 以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带 Throwable 参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看 Throwable 类源码,可以发现里面有一个 Throwable 字段 cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

public class Throwable implements Serializable {
    private Throwable cause = this;
 
    public Throwable(String message, Throwable cause) {fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     public Throwable(Throwable cause) {fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
 
    //........
}

下面看一个比较实在的异常链例子哈

public class 异常链 {
    @Test
    public void test() {C();
    }
    public void A () throws Exception {
        try {
            int i = 1;
            i = i / 0;
            // 当我注释掉这行代码并使用 B 方法抛出一个 error 时,运行结果如下
//            四月 27, 2018 10:12:30 下午 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
//            信息: Discovered TestEngines with IDs: [junit-jupiter]
//            java.lang.Error: B 也犯了个错误
//            at com.javase. 异常. 异常链.B(异常链.java:33)
//            at com.javase. 异常. 异常链.C(异常链.java:38)
//            at com.javase. 异常. 异常链.test(异常链.java:13)
//            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
//            Caused by: java.lang.Error
//            at com.javase. 异常. 异常链.B(异常链.java:29)

        }catch (ArithmeticException e) {
            // 这里通过 throwable 类的构造方法将最底层的异常重新包装并抛出,此时注入了 A 方法的信息。最后打印栈信息时可以看到 caused by
            A 方法的异常。// 如果直接抛出,栈信息打印结果只能看到上层方法的错误信息,不能看到其实是 A 发生了错误。// 所以需要包装并抛出
            throw new Exception("A 方法计算错误", e);
        }

    }
    public void B () throws Exception,Error {
        try {
            // 接收到 A 的异常,A();
            throw new Error();}catch (Exception e) {throw e;}catch (Error error) {throw new Error("B 也犯了个错误", error);
        }
    }
    public void C () {
        try {B();
        }catch (Exception | Error e) {e.printStackTrace();
        }

    }

    // 最后结果
//    java.lang.Exception: A 方法计算错误
//    at com.javase. 异常. 异常链.A(异常链.java:18)
//    at com.javase. 异常. 异常链.B(异常链.java:24)
//    at com.javase. 异常. 异常链.C(异常链.java:31)
//    at com.javase. 异常. 异常链.test(异常链.java:11)
//    省略
//    Caused by: java.lang.ArithmeticException: / by zero
//    at com.javase. 异常. 异常链.A(异常链.java:16)
//            ... 31 more
}

自定义异常

如果要自定义异常类,则扩展 Exception 类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自 RuntimeException。

按照国际惯例,自定义的异常应该总是包含如下的构造函数:

一个无参构造函数
一个带有 String 参数的构造函数,并传递给父类的构造函数。
一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数
一个带有 Throwable 参数的构造函数,并传递给父类的构造函数。
下面是 IOException 类的完整源代码,可以借鉴。

public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException()
    {super();
    }
 
    public IOException(String message)
    {super(message);
    }
 
    public IOException(String message, Throwable cause)
    {super(message, cause);
    }
 
    public IOException(Throwable cause)
    {super(cause);
    }
}

异常的注意事项

异常的注意事项

当子类重写父类的带有 throws 声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内——用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法。这是为了支持多态。

例如,父类方法 throws 的是 2 个异常,子类就不能 throws 3 个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。

至于为什么?我想,也许下面的例子可以说明。

class Father
{public void start() throws IOException
    {throw new IOException();
    }
}
 
class Son extends Father
{public void start() throws Exception
    {throw new SQLException();
    }
}

/假设上面的代码是允许的(实质是错误的)*/

class Test
{public static void main(String[] args)
    {Father[] objs = new Father[2];
        objs[0] = new Father();
        objs[1] = new Son();
 
        for(Father obj:objs)
        {
        // 因为 Son 类抛出的实质是 SQLException,而 IOException 无法处理它。// 那么这里的 try。。catch 就不能处理 Son 中的异常。// 多态就不能实现了。try {obj.start();
            }catch(IOException)
            {// 处理 IOException}
         }
   }
}

==Java 的异常执行流程是线程独立的,线程之间没有影响 ==

Java 程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

也就是说,Java 中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

下面看一个例子

public class 多线程的异常 {
    @Test
    public void test() {go();
    }
    public void go () {ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0;i <= 2;i ++) {
            int finalI = i;
            try {Thread.sleep(2000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            executorService.execute(new Runnable() {
                @Override
                // 每个线程抛出异常时并不会影响其他线程的继续执行
                public void run() {
                    try {System.out.println("start thread" + finalI);
                        throw new Exception();}catch (Exception e) {System.out.println("thread" + finalI + "go wrong");
                    }
                }
            });
        }
//        结果://        start thread0
//        thread0 go wrong
//        start thread1
//        thread1 go wrong
//        start thread2
//        thread2 go wrong
    }
}

当 finally 遇上 return

首先一个不容易理解的事实:

在 try 块中即便有 return,break,continue 等改变执行流的语句,finally 也会执行。

public static void main(String[] args)
{int re = bar();
    System.out.println(re);
}
private static int bar() 
{
    try{return 5;} finally{System.out.println("finally");
    }
}
/* 输出:finally
*/

很多人面对这个问题时,总是在归纳执行的顺序和规律,不过我觉得还是很难理解。我自己总结了一个方法。用如下 GIF 图说明。

[外链图片转存失败(img-SceF4t85-1569073569354)(http://incdn1.b0.upaiyun.com/…]

也就是说:try…catch…finally 中的 return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是 0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。

finally 中的 return 会覆盖 try 或者 catch 中的返回值。

public static void main(String[] args)
    {
        int result;
 
        result  =  foo();
        System.out.println(result);     /////////2
 
        result = bar();
        System.out.println(result);    /////////2
    }
 
    @SuppressWarnings("finally")
    public static int foo()
    {
        trz{int a = 5 / 0;} catch (Exception e){return 1;} finally{return 2;}
 
    }
 
    @SuppressWarnings("finally")
    public static int bar()
    {
        try {return 1;}finally {return 2;}
    }

finally 中的 return 会抑制(消灭)前面 try 或者 catch 块中的异常

class TestException
{public static void main(String[] args)
    {
        int result;
        try{result = foo();
            System.out.println(result);           // 输出 100
        } catch (Exception e){System.out.println(e.getMessage());    // 没有捕获到异常
        }
 
        try{result  = bar();
            System.out.println(result);           // 输出 100
        } catch (Exception e){System.out.println(e.getMessage());    // 没有捕获到异常
        }
    }
 
    //catch 中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {throw new Exception("我将被忽略,因为下面的 finally 中使用了 return");
        }finally {return 100;}
    }
 
    //try 中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {return 100;}
    }
}

finally 中的异常会覆盖(消灭)前面 try 或者 catch 中的异常

class TestException
{public static void main(String[] args)
    {
        int result;
        try{result = foo();
        } catch (Exception e){System.out.println(e.getMessage());    // 输出:我是 finaly 中的 Exception
        }
 
        try{result  = bar();
        } catch (Exception e){System.out.println(e.getMessage());    // 输出:我是 finaly 中的 Exception
        }
    }
 
    //catch 中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {throw new Exception("我将被忽略,因为下面的 finally 中抛出了新的异常");
        }finally {throw new Exception("我是 finaly 中的 Exception");
        }
    }
 
    //try 中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {throw new Exception("我是 finaly 中的 Exception");
        }
 
    }
}

上面的 3 个例子都异于常人的编码思维,因此我建议:

不要在 fianlly 中使用 return。

不要在 finally 中抛出异常。

减轻 finally 的任务,不要在 finally 中做一些其它的事情,finally 块仅仅用来释放资源是最合适的。

将尽量将所有的 return 写在函数的最后面,而不是 try … catch … finally 中。

JAVA 异常常见面试题

下面是我个人总结的在 Java 和 J2EE 开发者在面试中经常被问到的有关 Exception 和 Error 的知识。在分享我的回答的时候,我也给这些问题作了快速修订,并且提供源码以便深入理解。我总结了各种难度的问题,适合新手码农和高级 Java 码农。如果你遇到了我列表中没有的问题,并且这个问题非常好,请在下面评论中分享出来。你也可以在评论中分享你面试时答错的情况。

1) Java 中什么是 Exception?
这个问题经常在第一次问有关异常的时候或者是面试菜鸟的时候问。我从来没见过面高级或者资深工程师的时候有人问这玩意,但是对于菜鸟,是很愿意问这个的。简单来说,异常是 Java 传达给你的系统和程序错误的方式。在 java 中,异常功能是通过实现比如 Throwable,Exception,RuntimeException 之类的类,然后还有一些处理异常时候的关键字,比如 throw,throws,try,catch,finally 之类的。所有的异常都是通过 Throwable 衍生出来的。Throwable 把错误进一步划分为 java.lang.Exception
和 java.lang.Error.  java.lang.Error 用来处理系统错误,例如 java.lang.StackOverFlowError 之类的。然后 Exception 用来处理程序错误,请求的资源不可用等等。

2) Java 中的检查型异常和非检查型异常有什么区别?

这又是一个非常流行的 Java 异常面试题,会出现在各种层次的 Java 面试中。检查型异常和非检查型异常的主要区别在于其处理方式。检查型异常需要使用 try, catch 和 finally 关键字在编译期进行处理,否则会出现编译器会报错。对于非检查型异常则不需要这样做。Java 中所有继承自 java.lang.Exception 类的异常都是检查型异常,所有继承自 RuntimeException 的异常都被称为非检查型异常。

3) Java 中的 NullPointerException 和 ArrayIndexOutOfBoundException 之间有什么相同之处?

在 Java 异常面试中这并不是一个很流行的问题,但会出现在不同层次的初学者面试中,用来测试应聘者对检查型异常和非检查型异常的概念是否熟悉。顺便说一下,该题的答案是,这两个异常都是非检查型异常,都继承自 RuntimeException。该问题可能会引出另一个问题,即 Java 和 C 的数组有什么不同之处,因为 C 里面的数组是没有大小限制的,绝对不会抛出 ArrayIndexOutOfBoundException。

4)在 Java 异常处理的过程中,你遵循的那些最好的实践是什么?

这个问题在面试技术经理是非常常见的一个问题。因为异常处理在项目设计中是非常关键的,所以精通异常处理是十分必要的。异常处理有很多最佳实践,下面列举集中,它们提高你代码的健壮性和灵活性:

1) 调用方法的时候返回布尔值来代替返回 null,这样可以 NullPointerException。由于空指针是 java 异常里最恶心的异常

2) catch 块里别不写代码。空 catch 块是异常处理里的错误事件,因为它只是捕获了异常,却没有任何处理或者提示。通常你起码要打印出异常信息,当然你最好根据需求对异常信息进行处理。

3)能抛受控异常(checked Exception)就尽量不抛受非控异常(checked Exception)。通过去掉重复的异常处理代码,可以提高代码的可读性。

4) 绝对不要让你的数据库相关异常显示到客户端。由于绝大多数数据库和 SQLException 异常都是受控异常,在 Java 中,你应该在 DAO 层把异常信息处理,然后返回处理过的能让用户看懂并根据异常提示信息改正操作的异常信息。

5) 在 Java 中,一定要在数据库连接,数据库查询,流处理后,在 finally 块中调用 close()方法。

5) 既然我们可以用 RuntimeException 来处理错误,那么你认为为什么 Java 中还存在检查型异常?

这是一个有争议的问题,在回答该问题时你应当小心。虽然他们肯定愿意听到你的观点,但其实他们最感兴趣的还是有说服力的理由。我认为其中一个理由是,存在检查型异常是一个设计上的决定,受到了诸如 C ++ 等比 Java 更早编程语言设计经验的影响。绝大多数检查型异常位于 java.io 包内,这是合乎情理的,因为在你请求了不存在的系统资源的时候,一段强壮的程序必须能够优雅的处理这种情况。通过把 IOException 声明为检查型异常,Java 确保了你能够优雅的对异常进行处理。另一个可能的理由是,可以使用 catch 或 finally 来确保数量受限的系统资源(比如文件描述符)在你使用后尽早得到释放。Joshua
Bloch 编写的 Effective Java 一书 中多处涉及到了该话题,值得一读。

6)  throw 和 throws 这两个关键字在 java 中有什么不同?

一个 java 初学者应该掌握的面试问题。throw 和 throws 乍看起来是很相似的尤其是在你还是一个 java 初学者的时候。尽管他们看起来相似,都是在处理异常时候使用到的。但在代码里的使用方法和用到的地方是不同的。throws 总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常, 你也可以申明未检查的异常,但这不是编译器强制的。如果方法抛出了异常那么调用这个方法的时候就需要将这个异常处理。另一个关键字  throw 是用来抛出任意异常的,按照语法你可以抛出任意 Throwable (i.e. Throwable
或任何 Throwable 的衍生类) , throw 可以中断程序运行,因此可以用来代替 return . 最常见的例子是用 throw 在一个空方法中需要 return 的地方抛出 UnSupportedOperationException 代码如下 :

123 private`static void show() {thrownew UnsupportedOperationException("Notyet implemented");`}

可以看下这篇 文章查看这两个关键字在 java 中更多的差异。

7) 什么是“异常链”?

“异常链”是 Java 中非常流行的异常处理概念,是指在进行一个异常处理时抛出了另外一个异常,由此产生了一个异常链条。该技术大多用于将“受检查异常”(checked exception)封装成为“非受检查异常”(unchecked exception)或者 RuntimeException。顺便说一下,如果因为因为异常你决定抛出一个新的异常,你一定要包含原有的异常,这样,处理程序才可以通过 getCause()和 initCause()方法来访问异常最终的根源。

) 你曾经自定义实现过异常吗?怎么写的?

很显然,我们绝大多数都写过自定义或者业务异常,像 AccountNotFoundException。在面试过程中询问这个 Java 异常问题的主要原因是去发现你如何使用这个特性的。这可以更准确和精致的去处理异常,当然这也跟你选择 checked 还是 unchecked exception 息息相关。通过为每一个特定的情况创建一个特定的异常,你就为调用者更好的处理异常提供了更好的选择。相比通用异常(general exception),我更倾向更为精确的异常。大量的创建自定义异常会增加项目 class 的个数,因此,在自定义异常和通用异常之间维持一个平衡是成功的关键。

9) JDK7 中对异常处理做了什么改变?

这是最近新出的 Java 异常处理的面试题。JDK7 中对错误 (Error) 和异常 (Exception) 处理主要新增加了 2 个特性,一是在一个 catch 块中可以出来多个异常,就像原来用多个 catch 块一样。另一个是自动化资源管理(ARM), 也称为 try-with-resource 块。这 2 个特性都可以在处理异常时减少代码量,同时提高代码的可读性。对于这些特性了解,不仅帮助开发者写出更好的异常处理的代码,也让你在面试中显的更突出。我推荐大家读一下 Java 7 攻略,这样可以更深入的了解这 2 个非常有用的特性。

10) 你遇到过 OutOfMemoryError 错误嘛?你是怎么搞定的?

这个面试题会在面试高级程序员的时候用,面试官想知道你是怎么处理这个危险的 OutOfMemoryError 错误的。必须承认的是,不管你做什么项目,你都会碰到这个问题。所以你要是说没遇到过,面试官肯定不会买账。要是你对这个问题不熟悉,甚至就是没碰到过,而你又有 3、4 年的 Java 经验了,那么准备好处理这个问题吧。在回答这个问题的同时,你也可以借机向面试秀一下你处理内存泄露、调优和调试方面的牛逼技能。我发现掌握这些技术的人都能给面试官留下深刻的印象。

11) 如果执行 finally 代码块之前方法返回了结果,或者 JVM 退出了,finally 块中的代码还会执行吗?

这个问题也可以换个方式问:“如果在 try 或者 finally 的代码块中调用了 System.exit(),结果会是怎样”。了解 finally 块是怎么执行的,即使是 try 里面已经使用了 return 返回结果的情况,对了解 Java 的异常处理都非常有价值。只有在 try 里面是有 System.exit(0)来退出 JVM 的情况下 finally 块中的代码才不会执行。

12)Java 中 final,finalize,finally 关键字的区别

这是一个经典的 Java 面试题了。我的一个朋友为 Morgan Stanley 招电信方面的核心 Java 开发人员的时候就问过这个问题。final 和 finally 是 Java 的关键字,而 finalize 则是方法。final 关键字在创建不可变的类的时候非常有用,只是声明这个类是 final 的。而 finalize()方法则是垃圾回收器在回收一个对象前调用,但也 Java 规范里面没有保证这个方法一定会被调用。finally 关键字是唯一一个和这篇文章讨论到的异常处理相关的关键字。在你的产品代码中,在关闭连接和资源文件的是时候都必须要用到 finally 块。

参考文章

https://www.xuebuyuan.com/324…
https://www.jianshu.com/p/49d…
http://c.biancheng.net/view/1…
https://blog.csdn.net/Lisilua…
https://blog.csdn.net/michael…

微信公众号

Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

个人公众号:黄小斜

作者是 985 硕士,蚂蚁金服 JAVA 工程师,专注于 JAVA 后端技术栈:SpringBoot、MySQL、分布式、中间件、微服务,同时也懂点投资理财,偶尔讲点算法和计算机理论基础,坚持学习和写作,相信终身学习的力量!

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。

正文完
 0