Java 的异常与处理

关于异常

在软件开发的过程中, 开发者往往要需要考虑很多在开发过程中不会出现, 但是在实际使用时却又会实实在在出现的, 导致程序无法正常执行的情况, 比如:

1. 计算人体的BMI值, 但用户在输入身高的时候, 输入了 0, 我们知道0是不能做除数的.

2. 用户使用的时候需要网络, 却忽然没有了网络信号.

Java编程 - 应用没有没信号的异常

3. 用户的硬件中的内存不足以支撑当前软件的运行崩溃了.

所有这些现实中有概率会出现的, 影响用户使用的, 非正常的使用情况, 被归为两类:

一类叫 错误(Error)

这是系统或更高级别限制导致的, 程序无能为力应对的, 最终可能会导致程序崩溃的情况.

比如: 内存不足以支撑当前软件的运行了.

另一类叫 异常(Exception)

这是程序可以捕获辨别输入的, 但却和预想的输入情况不一致的.

比如: 需要有网络的时候, 没网络信号; 需要导航的时候, 没GPS信号; 需要用户输入数字, 用户却输入了 "abc" 字母.

在 Java 中它们均继承自 Throwable 类, 而 Exception 类又被分为两类:

一类是 运行时异常(RuntimeException)

此类异常, 是应该在获取输入的时候, 开发人员就该考虑到的, 不该忽略的, 易于检测的, 不需要确认的异常. 所以这些异常不是强制捕获或在方法生声明外抛(抛出给调用者)的.

比如:

ArithmeticException: 做除法前, 没能判断一个数字能否作为除数

IndexOutOfBoundsException: 使用数组前, 没能判断索引是否超过数组的边界

另一类是 非运行时异常(除RuntimeException以外的异常, 包括但不限于 IOExceptionDataFormatException等)

此类异常又叫 确认异常(Checked Exception), 是需要必须确认捕获或在方法上声明外抛的. 倘若不做异常捕获或抛出, 那么在执行时会报编译错误:

Exception in thread "main" java.lang.Error: Unresolved compilation problems: Unhandled exception type XXXException

比如:

FileNotFoundException: 打开文件可能不存在的异常

IOException: 输入输出异常

下面我们用一张图来理解上面的关系:

Java 异常间的关系

如何捕获异常

在 Java 中, 如果我们需要处理异常, 首先我们需要捕获 (catch) 它们, 捕获异常的通用结构如下:

try
{
   // 包含异常的代码A
} catch(异常1 e1) {
   //捕获到 异常1 类的情况下, 做如何处理
} catch(异常2 e2) {
    //捕获到 异常1 类的情况下, 做如何处理
} finally {
    //无论异常是否发生都会执行的代码
}
  • 首先需要将会抛出异常的代码A, 放到 try { } 代码块下.
  • 然后通过 catch(异常 异常变量) 的方式捕获异常, 异常可以是特定的异常类, 比如 FileNotFoundException, 也可以异常基类 Exception (但是我们不建议这么做). 然后在接下来的花括号中定义如果捕获了此类异常需要做些什么, 比如给用户一些提示, 或者留下一些开发日志啥的. 
  • 如果需要捕获多个异常, 则新增一个 catch(异常 异常变量) 的代码块
  • 最后, 无论是否捕获到异常都会执行 finally{} 中的代码, 但是块 finally{} 是不是必须的.

下面我们用个例子来看看异常如何捕获:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Yi21ExceptionDemo {
    public static void main(String[] args) {
        try {
            FileInputStream fStream = new FileInputStream(new File("21yi_not_exists.file"));
            fStream.close();
            System.out.println("Done.");
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在: " + e.getMessage());
            System.out.println("异常类: " + e.getClass());
            System.out.println("引起异常的原因: " + e.getCause());
            System.out.println("被抑制的数组: " + e.getSuppressed());
            System.out.println("出错追溯: " + e.getStackTrace());
        } catch (IOException e) {
            System.out.println("输入输出异常: " + e.getMessage());
        } finally {
            System.err.println("我总是会被执行.");
        }
    }
}

上述代码中, 我们尝试访问一个不存在的文件 21yi_not_exists.file , 将这块代码放于 try {} 块下.

我们需要捕获的异常有, 初始化 File 对象会抛出的异常 FileNotFoundException 以及 stream 关闭时会抛出的异常 IOException.

实际上 FileNotFoundException IOException 的一个子类, 这里我们为了捕获更加精确的异常, 所以优先去捕获 FileNotFoundException .

如果我们 捕获到了 FileNotFoundException 会展示, 异常的各种方法.

最终, 无论我们是否捕获到了异常, 总是会输出一句 "我总是会被执行.".

代码执行的结果如下:

文件不存在: not_exists.file (系统找不到指定的文件。)
异常类: class java.io.FileNotFoundException
引起异常的原因: null
被抑制的数组: [Ljava.lang.Throwable;@6f75e721
出错追溯: [Ljava.lang.StackTraceElement;@782830e
我总是会被执行.

如何外抛异常

我们在写代码的时候, 并不总是需要在当下解决异常的处理, 我们可以将异常外抛出去, 交给方法的调用者去处理.

比如, 我们定义了处理一些文件的方法, 调用这个方法的有的是命令行调用, 有的需要封装一套程序界面来处理这个异常, 因此我们可以延后处理这个异常, 将异常处理的选择权交给调用者. 

我们将这种把异常交由外部调用者处理的方式, 称为外抛(throws).

throws 声明在方法的签名后, 他的使用示例如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Yi21ExceptionThrows {

    public static void initFile() throws FileNotFoundException, IOException {
        FileInputStream fStream = new FileInputStream(new File("21yi_not_exists.file"));
        fStream.close();
        System.out.println("Done.");
    }

    public static void main(String[] args) {
        try {
            initFile();
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("输入输出异常: " + e.getMessage());
        } finally {
            System.err.println("我总是会被执行.");
        }
    }
}

在上述代码中, 我们定义了一个 initFile() 方法来初始化那个不存在的文件, 但是我们不急于处理其中的异常, 于是我们将代码中会出现的各种异常 FileNotFoundException IOException 外抛.

然后我们在 main() 中调用了  initFile() 方法, 这时候我们就可以针对这些异常来做特别的处理了. 代码执行结果如下:

文件不存在: 21yi_not_exists.file (系统找不到指定的文件。)
我总是会被执行.

如何自定义异常与抛出异常

所有异常都是 Exception 的基类, 因此我们只要继承 Exception 类即可.

同样的如果我们想定义一些不是必须捕获的异常, 可以继承 RuntimeException.

此外我们可以根据具体的需求继承其他的异常类.

下面我们还是以代码示例来展示自定义异常与抛出异常:

import java.io.IOException;

public class Yi21ExceptionSub {
    
    private static class Yi21Exception extends IOException {
        public Yi21Exception(String message) {
            super(message);
        }
    }

    private static void demo() throws Yi21Exception {
        int i = 10;
        if (i > 1) {
            throw new Yi21Exception("i > 1");
        }
    }

    public static void main(String[] args) {
        try {
            demo();
            System.out.println("Done.");
        } catch(Yi21Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

在上述代码中, 我们通过继承 IOException 声明了一个新的异常  Yi21Exception 同时实现了 Yi21Exception 的构造方法, 通过提供一个字符串, 作为异常的消息来初始化这个异常.

然后定义了外抛 Yi21Exception 的方法 demo(), 在这个方法中 我们通过 throw 初始化的 Yi21Exception 对象, 抛出了异常.

最后在 main() 方法中捕获  Yi21Exception 这个异常, 来在异常捕获的处理.

以上就是, Java 的异常与处理的全部内容了.

从下一节开始, 我们将介绍 Java 自带的一些常用的类型了, 首先我们先看看: Java 的String类