码到功成

深入理解 Java 异常

2019-12-16  本文已影响0人  静默虚空

📓 本文已归档到:「javacore

🔁 本文中的示例代码已归档到:「javacore

1. 异常框架

1.1. Throwable

Throwable 是 Java 语言中所有错误(Error)和异常(Exception)的超类。

Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

主要方法:

1.2. Error

ErrorThrowable 的一个子类。Error 表示合理的应用程序不应该尝试捕获的严重问题。大多数此类错误都是异常情况。编译器不会检查 Error

常见 Error

1.3. Exception

ExceptionThrowable 的一个子类。Exception 表示合理的应用程序可能想要捕获的条件。

编译器会检查 Exception 异常。此类异常,要么通过 throws 进行声明抛出,要么通过 try catch 进行捕获处理,否则不能通过编译。

常见 Exception

示例:

public class ExceptionDemo {
    public static void main(String[] args) {
        Method method = String.class.getMethod("toString", int.class);
    }
};

试图编译运行时会报错:

Error:(7, 47) java: 未报告的异常错误java.lang.NoSuchMethodException; 必须对其进行捕获或声明以便抛出

1.4. RuntimeException

RuntimeExceptionException 的一个子类。RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。

编译器不会检查 RuntimeException 异常。当程序中可能出现这类异常时,倘若既没有通过 throws 声明抛出它,也没有用 try catch 语句捕获它,程序还是会编译通过。

示例:

public class RuntimeExceptionDemo {
    public static void main(String[] args) {
        // 此处产生了异常
        int result = 10 / 0;
        System.out.println("两个数字相除的结果:" + result);
        System.out.println("----------------------------");
    }
};

运行时输出:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at io.github.dunwu.javacore.exception.RumtimeExceptionDemo01.main(RumtimeExceptionDemo01.java:6)

常见 RuntimeException

2. 自定义异常

自定义一个异常类,只需要继承 ExceptionRuntimeException 即可。

示例:

public class MyExceptionDemo {
    public static void main(String[] args) {
        throw new MyException("自定义异常");
    }

    static class MyException extends RuntimeException {
        public MyException(String message) {
            super(message);
        }
    }
}

输出:

Exception in thread "main" io.github.dunwu.javacore.exception.MyExceptionDemo$MyException: 自定义异常
    at io.github.dunwu.javacore.exception.MyExceptionDemo.main(MyExceptionDemo.java:9)

3. 抛出异常

如果想在程序中明确地抛出异常,需要用到 throwthrows

如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。

throw 示例:

public class ThrowDemo {
    public static void f() {
        try {
            throw new RuntimeException("抛出一个异常");
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        f();
    }
};

输出:

java.lang.RuntimeException: 抛出一个异常

也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。

throws 示例:

public class ThrowsDemo {
    public static void f1() throws NoSuchMethodException, NoSuchFieldException {
        Field field = Integer.class.getDeclaredField("digits");
        if (field != null) {
            System.out.println("反射获取 digits 方法成功");
        }
        Method method = String.class.getMethod("toString", int.class);
        if (method != null) {
            System.out.println("反射获取 toString 方法成功");
        }
    }

    public static void f2() {
        try {
            // 调用 f1 处,如果不用 try catch ,编译时会报错
            f1();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        f2();
    }
};

输出:

反射获取 digits 方法成功
java.lang.NoSuchMethodException: java.lang.String.toString(int)
    at java.lang.Class.getMethod(Class.java:1786)
    at io.github.dunwu.javacore.exception.ThrowsDemo.f1(ThrowsDemo.java:12)
    at io.github.dunwu.javacore.exception.ThrowsDemo.f2(ThrowsDemo.java:21)
    at io.github.dunwu.javacore.exception.ThrowsDemo.main(ThrowsDemo.java:30)

throw 和 throws 的区别:

4. 捕获异常

使用 try 和 catch 关键字可以捕获异常。try catch 代码块放在异常可能发生的地方。

它的语法形式如下:

try {
    // 可能会发生异常的代码块
} catch (Exception e1) {
    // 捕获并处理try抛出的异常类型Exception
} catch (Exception2 e2) {
    // 捕获并处理try抛出的异常类型Exception2
} finally {
    // 无论是否发生异常,都将执行的代码块
}

此外,JDK7 以后,catch 多种异常时,也可以像下面这样简化代码:

try {
    // 可能会发生异常的代码块
} catch (Exception | Exception2 e) {
    // 捕获并处理try抛出的异常类型
} finally {
    // 无论是否发生异常,都将执行的代码块
}

示例:

public class TryCatchFinallyDemo {
    public static void main(String[] args) {
        try {
            // 此处产生了异常
            int temp = 10 / 0;
            System.out.println("两个数字相除的结果:" + temp);
            System.out.println("----------------------------");
        } catch (ArithmeticException e) {
            System.out.println("出现异常了:" + e);
        } finally {
            System.out.println("不管是否出现异常,都执行此代码");
        }
    }
};

运行时输出:

出现异常了:java.lang.ArithmeticException: / by zero
不管是否出现异常,都执行此代码

5. 异常链

异常链是以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息。

通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。

我们有两种方式处理异常,一是 throws 抛出交给上级处理,二是 try…catch 做具体处理。try…catch 的 catch 块我们可以不需要做任何处理,仅仅只用 throw 这个关键字将我们封装异常信息主动抛出来。然后在通过关键字 throws 继续抛出该方法异常。它的上层也可以做这样的处理,以此类推就会产生一条由异常构成的异常链。

示例:

public class ExceptionChainDemo {
    static class MyException1 extends Exception {
        public MyException1(String message) {
            super(message);
        }
    }

    static class MyException2 extends Exception {
        public MyException2(String message, Throwable cause) {
            super(message, cause);
        }
    }

    public static void f1() throws MyException1 {
        throw new MyException1("出现 MyException1");
    }

    public static void f2() throws MyException2 {
        try {
            f1();
        } catch (MyException1 e) {
            throw new MyException2("出现 MyException2", e);
        }
    }

    public static void main(String[] args) throws MyException2 {
        f2();
    }
}

输出:

Exception in thread "main" io.github.dunwu.javacore.exception.ExceptionChainDemo$MyException2: 出现 MyException2
    at io.github.dunwu.javacore.exception.ExceptionChainDemo.f2(ExceptionChainDemo.java:29)
    at io.github.dunwu.javacore.exception.ExceptionChainDemo.main(ExceptionChainDemo.java:34)
Caused by: io.github.dunwu.javacore.exception.ExceptionChainDemo$MyException1: 出现 MyException1
    at io.github.dunwu.javacore.exception.ExceptionChainDemo.f1(ExceptionChainDemo.java:22)
    at io.github.dunwu.javacore.exception.ExceptionChainDemo.f2(ExceptionChainDemo.java:27)
    ... 1 more

扩展阅读:https://juejin.im/post/5b6d61e55188251b38129f9a#heading-10

这篇文章中对于异常链讲解比较详细。

6. 异常注意事项

6.1. finally 覆盖异常

Java 异常处理中 finally 中的 return 会覆盖 catch 代码块中的 return 语句和 throw 语句,所以 Java 不建议在 finally 中使用 return 语句

此外 finally 中的 throw 语句也会覆盖 catch 代码块中的 return 语句和 throw 语句。

示例:

public class FinallyOverrideExceptionDemo {
    static void f() throws Exception {
        try {
            throw new Exception("A");
        } catch (Exception e) {
            throw new Exception("B");
        } finally {
            throw new Exception("C");
        }
    }

    public static void main(String[] args) {
        try {
            f();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

输出:C

6.2. 覆盖抛出异常的方法

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

示例:

public class ExceptionOverrideDemo {
    static class Father {
        public void start() throws IOException {
            throw new IOException();
        }
    }

    static class Son extends Father {
        @Override
        public void start() throws SQLException {
            throw new SQLException();
        }
    }

    public static void main(String[] args) {
        Father obj1 = new Father();
        Father obj2 = new Son();
        try {
            obj1.start();
            obj2.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面的示例编译时会报错,原因在于:

因为 Son 类抛出异常的实质是 SQLException,而 IOException 无法处理它。那么这里的 try catch 就不能处理 Son 中的异常了。多态就不能实现了。

6.3. 异常和线程

如果 Java 程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止。如果 Java 程序是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

7. 最佳实践

扩展阅读:

8. 小结

img img

9. 参考资料

上一篇下一篇

猜你喜欢

热点阅读