在程序设计和运行的过程中,发生错误是不可避免的,比如程序试图打开一个不存在的文件,又或者是网络异常等。我们将这种在程序运行时可能出现的一些错误称之为异常。异常是一个程序在执行期间发生的事件,它中断了正在执行的程序的正常指令流。

尽管 Java 语言的设计已经极大地保证了代码的安全性和稳定性,并且程序员本身也会尽量减少错误的产生,但程序被迫停止的情况仍旧无法避免。不光是 JAVA,几乎每一种编程语言都提供了异常处理机制来帮助程序员检查可能出现的错误,从而保证程序的可读性和可维护性。

下面一段程序可以正常编译,但是一旦运行就出现了ArithmeticException算术异常,该程序不再继续往下执行,提前结束。

  1. public class Baulk {
  2. public static void main(String[] args) {
  3. int result = 3 / 0;
  4. System.out.println(result);
  5. }
  6. }

针对于上面示例,我们需要在程序中进行异常处理,因为这个 0 很可能就是用户的错误输入,不可能用户每一次的错误输入,我们都不得不人为地重启程序,显然最好的处理方式就是程序知道遇到这种情况时如何处理,从而保持一直正常运行的状态。

1. 异常的继承结构

Java 是一门面向对象的编程语言,因此,异常在 Java 语言中也是作为类的实例出现的。当某一方法发生错误时,这个方法会创建一个对象,并且把它传递给正在运行的系统,这个对象就是异常对象。

首先介绍一下Throwable类:

  • Throwable类是 Java 中所有错误或异常的超类,只有当对象是Throwable类或其子类的实例时,才能通过 JVM 或者使用throw抛出。
  • ExceptionError都继承了Throwable类,它们是异常处理机制的基本组成类型。
  • 通常将不同异常情况分为ExceptionError,这一点很多语言都采用相同的机制。
    • Exception是程序正常运行中,可以预料到的意外情况,应该在程序中进行捕获和处理
    • Error是指在正常的情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常、不可恢复的状态。虽然是非正常情况,但程序不便于捕获,比如常见的OutOfMemoryError之类,都是Error的子类

      2. Error

      ErrorThrowable的子类,用于指示合理的应用程序不应该试图捕获的严重问题。在 Java 程序中通常不捕获错误,错误一般发生在严重故障的时候,并不在处理范围之内。比如,正在运行的程序,突然内存爆了,这个问题我们无法在程序中解决。

| ```java public class OutOfMemoryErrorDemo { public static void main(String[] args) { byte[] bytes = new byte[1024 * 1024]; } }

  1. | ```java
  2. public class StackOverflowErrorDemo {
  3. public static void fun() {
  4. fun();
  5. }
  6. public static void main(String[] args) {
  7. fun();
  8. }
  9. }

| | —- | —- |

2.1 常见Error

错误 描述
OutOfMemoryError 内存溢出错误
StackOverflowError 栈内存溢出错误
NoClassDefFoundError 找不到class定义的错误

3. Exception

在 Java 语言中,将程序执行中发生的不正常情况称之为异常。通常 Exception 主要分为两类:

  • 检查性异常(CheckedException):可检查异常在源代码中必须显式地进行捕获,通常发生在编译期
  • 运行时异常(RuntimeException):通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求

    3.1 常见的检查性异常

    | 异常 | 描述 | | —- | —- | | IOException | IO异常,流操作时有可能出现的异常 | | SQLException | SQL 异常,操作数据库时会出现 | | ClassNotFoundException | 找不到某个类时,就会抛出该异常,使用反射时会遇到 | | InterruptedException | 当阻塞方法收到中断请求时就会抛出中断异常 | | NoSuchMethodException | 方法未找到抛出的异常 | | InstantiationException | 指定类对象无法被实例化 | | IllegalAcessException | 非法访问某类的异常 |

1) ClassNotFoundException

Java 支持使用Class.forName方法来动态地加载类,任意一个类的类名如果作为参数传递给这个方法,都将导致该类被加载到 JVM 内存中。如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。

  1. public class ClassNotFoundExceptionDemo {
  2. public static void main(String[] args) {
  3. try {
  4. Class.forName("com.ydlclass.Dog");
  5. } catch (ClassNotFoundException e) {
  6. e.printStackTrace();
  7. System.out.println(123);
  8. }
  9. }
  10. }

:::info Exceptiontry代码块传递给catch代码块的变量类型,e是变量名。通常有 3 中常用方法:

  • getMessage()函数:输出错误性质
  • toString()函数:给出异常的类型与性质
  • printStackTree()函数:指出异常的类型、性质、栈层次以及出现在程序中的未知 :::

    2) InterruptException

    1. try {
    2. Thread.sleep(123);
    3. } catch (InterruptException e) {
    4. e.printStackTrace();
    5. }

    3.2 常见非检查性异常

    | 异常 | 描述 | | —- | —- | | NullPointException | 空指针引用异常,试图调用空对象或者属性时,抛出该异常 | | ArithmeticException | 算术运算异常 | | ArrayStoreException | | | ClassCastException | 类型转换异常,试图将对象强制转换为不是实例的子类时,抛出该异常 | | ArrayIndexOutOfBoundsException | 数组下标越界异常 | | NumberFormatException | 数字格式异常 | | NoSuchFieldException | 字段未找到异常 | | NegativeArraySizeException | 数组元素个数为负数抛出的异常 | | StringIndexOutOfBoundsException | 字符串索引下标越界 | | EOFException | 文件已结束异常 | | FileNotFoundException | 文件未找到异常 | | SecurityException | 安全性异常 |

1) NullPointException

| ```java String str = null; System.out.println(str.equals(“abc”));

byte[] bytes = str.getBytes();

  1. | ```java
  2. System.out.println("abc".equals(str));
  3. if (Objects.nonNull)(str) {
  4. byte[] bytes = str.getBytes();
  5. }

| | —- | —- |

2) ClassCastException

| ```java Animal animal = new Dog(); Cat cat = (Cat) animal;

  1. | ```java
  2. if (animal instanceOf Cat) {
  3. Cat cat = (Cat) animal;
  4. }

| | —- | —- |

3) IndexOutOfBoundsException

| ```java int[] nums = new int[3]; int i = 3; nums[i] = 4;

  1. | ```java
  2. if (i > -1 && i < nums.length) {
  3. nums[i] = 4;
  4. }

| | —- | —- |

4) NumerFormatException

| ```java String str = “abc”; int num = Integer.parseInt(“abc”);

  1. | ```java
  2. Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
  3. if (pattern.match(str).matches()) {
  4. int num = Integer.parseInt(str);
  5. System.out.println(num);
  6. }

| | —- | —- |

3.3 抛出异常

我们在设计 API 时,通常会将一些异常抛出,从而提醒开发者当前使用方式存在问题。举个最简单的例子,我们设计一个操作 Impala 数据库的 JAR 包,使用前肯定需要验证用户的账号和密码,但如果用户提供的是错误的登录信息,那么程序应该抛出异常来告诉开发者当前账号密码不正确。

Java 中使用throw关键字在程序中抛出一个异常:

  1. public class ThrowException {
  2. public static void main(String[] args) {
  3. int[] nums = {1, 2, 3, 4, 5};
  4. Scanner scanner = new Scanner(System.in);
  5. int index = scanner.nextInt();
  6. if (index < 5) {
  7. System.out.println("数组中第" + index + "个元素为: " + nums[index]);
  8. } else {
  9. throw new IndexOutOfBoundsException("数组下标越界了");
  10. }
  11. }
  12. }

另外一种方式就是直接在方法中指定可能抛出的异常,多个异常使用逗号分隔。

  1. public class ThrowException {
  2. public static void main(String[] args) throws IOException{
  3. int[] nums = {1, 2, 3, 4, 5};
  4. Scanner scanner = new Scanner(System.in);
  5. int index = scanner.nextInt();
  6. if (index < 5) {
  7. System.out.println("数组中第" + index + "个元素为: " + nums[index]);
  8. } else {
  9. throw new IndexOutOfBoundsException("数组下标越界了");
  10. }
  11. }
  12. }

3.4 自定义异常

Java 异常机制中定义的所有异常不可能遇见所有可能出现的错误,某些特定情境下,需要我们自定义异常类型来向上报告某些错误信息。为了更好地处理预料之中的意外情况,Java 允许我们可以自定义异常,编写自定义异常有以下注意点:

  • 所有异常都必须是Throwable子类
  • 如果希望写一个检查性异常类,则需要继承Exception
  • 如果想写一个运行时异常类,则需要继承RuntimeException类 | ```java public class UserValidationErrorException extends RuntimeException{ private String message;

    // 空构造 public UserValidationErrorException() {}

    public UserValidationErrorException(String message) {

    1. this.message = message;
    2. System.out.println(this.message);

    } }

// 右侧输出结果 // 登录成功! // 当前账号和密码不正确! // 必须提供账号和密码来进行验证 // Exception in thread “main” …

  1. | ```java
  2. public class UserValidation {
  3. private String username = "yumingmin";
  4. private String password = "yumingmin";
  5. public UserValidation() {
  6. throw new UserValidationErrorException("必须提供账号和密码来进行验证");
  7. }
  8. public UserValidation(String username, String password) {
  9. if (this.username.equals(username) && this.password.equals(password)) {
  10. System.out.println("登录成功!");
  11. } else {
  12. throw new UserValidationErrorException("当前账号和密码不正确!");
  13. }
  14. }
  15. public static void main(String[] args) {
  16. UserValidation userValidation = new UserValidation("yumingmin", "yumingmin");
  17. UserValidation userValidation2 = new UserValidation("yumingmin", "yu");
  18. UserValidation userValidation3 = new UserValidation();
  19. }
  20. }

| | —- | —- |

3. 异常链

一个异常被抛出后会继续调用这个方法的的捕获或抛出,也就是异常会扩散。方法在栈空间被调用,多个方法互相调用形成了调用链,从而生成一个StackTree(也称为栈轨迹、堆栈信息),JVM 在抛出异常时会打印这个堆栈信息。

为了打印这个堆栈信息,Java 每实例化一个Exception,都会对当时的栈进行快照。这显然是一个较重的操作,如果发生很频繁的话,这个开销就不可被忽略,建议只捕获必要的代码段,尽量不要使用try语句捕获整段的代码。

| ```java public class UserErrorException extends RuntimeException{ public UserErrorException() {}

  1. public UserErrorException(Throwable cause) {
  2. super(cause);
  3. }

}

  1. | ```java
  2. public class PasswordErrorException
  3. extends RuntimeException{
  4. public PasswordErrorException() {}
  5. public PasswordErrorException(Throwable cause) {
  6. super(cause);
  7. }
  8. }

| | —- | —- |

我们可以在一个异常处理中继续抛出另一个异常,并通过initCause方法,或者直接使用throw new PasswordErrorException(e);,将整个异常链打印出来。

  1. package com.ydlclass.exceptionexamples;
  2. public class UserValidation {
  3. private String username;
  4. private String password;
  5. public UserValidation() {}
  6. public UserValidation(String username, String password) {
  7. this.username = username;
  8. this.password = password;
  9. }
  10. public void login() {
  11. if ("yumingmin".equals(this.username) && "abc".equals(this.password)) {
  12. System.out.println("登录成功!");
  13. }
  14. if (!"yumingmin".equals(this.username)) {
  15. throw new UserErrorException();
  16. }
  17. if (!"abc".equals(this.password)) {
  18. throw new PasswordErrorException();
  19. }
  20. }
  21. public static void main(String[] args) {
  22. UserValidation userValidation = new UserValidation("yumingmi", "ab");
  23. try {
  24. userValidation.login();
  25. } catch (UserErrorException e) {
  26. PasswordErrorException passwordErrorException = new PasswordErrorException();
  27. passwordErrorException.initCause(e);
  28. throw passwordErrorException;
  29. // 或者直接使用
  30. // throw new PasswordErrorException(e);
  31. } catch (PasswordErrorException e) {
  32. System.out.println("密码不正确");
  33. // 打印堆栈信息
  34. e.printStackTrace();
  35. }
  36. }
  37. }

image.png