美文网首页
[Java Tutorials] 04 | Java Excep

[Java Tutorials] 04 | Java Excep

作者: 夏海峰 | 来源:发表于2018-12-25 11:05 被阅读4次

本章学习要点有Java异常、基本输入输出、并发、正则表达式和平台环境。本章所讨论的这些Java类将是最常用的基本类。

Java异常

本节重点学习什么是Java异常,Java异常的原理,如何处理errors,如何 throw/catch 异常,当异常发生时该如何处理,如何使用具有继承层级的Java异常类。Java编程语言使用异常来处理错误和一些异常事件,本节讨论的目标是什么时候应该使用Java异常,并且该如何使用Java异常。

什么是Java异常?

所谓异常,也称为“异常事件”,就是那些发生在程序运行期间、并且破坏了程序指令正常执行的事件。

当一个错误发生在方法体内部时,这个方法就会创建一个对象并把它传递到运行时环境的外部去。这个被创建的对象就被称为“异常对象”,它包含着异常类型、错误发生时的程序状态等重要信息。方法体创建异常对象并把它传递到运行时环境的外部,这个过程被称为“抛出异常”。

这个抛出异常的方法体完成异常抛出后,运行时环境就会试图寻找并调用一系列有序的方法去处理这个异常,这一系列的有序方法被称之为“回调栈”。如下图示:

The Call Stack.

运行时环境从这个回调栈中搜索可以处理当前异常的方法,被找到的这个方法就被之为“异常处理器”。运行时环境从异常发生的那个方法开始搜索,以与生成回调栈时相反的顺序遍历这个回调栈,当找到了合适的异常处理器之后,运行时环境就会把当前异常对象传递给异常处理器。什么才算是合适的异常处理器呢?要求是当前异常对象的类型与异常处理器所能处理的异常类型相匹配时,才算是合适。

运行时环境选择异常处理器的行为,被称为“异常捕获”。如果运行时环境遍历了整个回调栈,仍然没有找到合适的异常处理器时,程序因此将会被中止掉。如下图示:

Searching the call stack for the exception handler.

对比传统的异常管理技术,使用Java异常来管理Java程序中的异常有很多优势,在稍后章节中将进一步探讨。

Catch or Specify Requirement (两种处理Java异常的方式 )

有效的Java代码,必须支持以下两种方式来处理程序中的异常:

  • 使用 try 语句来捕获异常。try 语句必须提供一个异常处理器。这种方式我们称之为“Catching and Handling Exceptions”。
  • 使用 Java方法抛出异常。这个抛出异常的Java方法必须提供一个 throws 关键字,胜于罗列出所有将要抛出的异常。这种方式我们称之为“Specifying the Exceptions Thrown by a Method”。

没有使用上述两种方式来处理异常的代码,将不会被编译通过。值得注意的是,并不是所有的Java异常都可以使用上述两种方式来处理,有且仅有一种类型的Java异常可以使用上述两种方式来处理。为了理解为什么,我们需要先讨论一下Java异常的分类,Java异常分为如下三大类:

三类Java异常
  1. Checked Exception,这一类的异常,是健壮的程序必须捕获并修复的异常。比如,当程序根据用户输入的文件名去打开文件时,Java程序会使用 java.io.FileReader 来读取文件内容,当用户输入的文件名正确且存在时,程序将正确地执行;当用户输入的文件名有误时,程序将抛出 java.io.FileNotFoundException 异常。这就是 Checked Exception,一个健壮的程序必须要捕获这一类的异常,并友好地提醒用户纠正错误。所有的 Checked Exception 都隶属于 Catch or Specify Requirement。除了 Error 和 RuntimeException 及其子类以外,其它所有的异常都是 Checked Exception。

  2. Error,这类异常通常是程序外部的异常,它们通常无法被预测,也无法进行捕获和修复。比如,当程序成功地打开了一个文件,但因为硬件故障而导致文件无法被读取,此时会抛出 java.io.IOError 异常。我们的程序可以尝试捕获这个异常并提示用户发生了什么问题。这个异常会打印堆栈跟踪,但实际上这个异常仍然是存在的。Error 这一类异常,不隶属于 Catch or Specify Requirement。

  3. Runtime Exception,这一类异常是程序内部的异常,通常无法被预测,也无法被捕获和修复。这类异常发生时,即预示着程序 bug 的存在,比如逻辑错误、不正确地使用API、NullPointerException 异常等。在这种情况下,我们与其去捕获异常,不如直接去修复这个导致异常的 bug。Runtime Exception 不隶属于 Catch or Specify Requirement。

Error 和 RuntimeException 异常,共同地被称为 Unchecked Exceptions。一些程序员认为 Catch or Specify Requirement 在异常机理上有一些瑕疵,并尝试使用 Checked Exceptions 来代替 Unchecked Exceptions。一般来说,这种做法是不推荐的。在接下来的课程中,我们将分享如何恰当地使用 Unchecked Exceptions。

捕获并处理异常

本部分讲解如何使用上述三种类型的异常处理组件。使用 try/catch/finally 写一个异常处理器。try 非常适合那些可关闭的资源声明,如 Stream 流。我们将通过一个例子来分析在不同场景下到底发生了什么。示例代码如下:

import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {
    private List<Integer> list;
    private static final int SIZE = 10;
    // 初始化
    public ListOfNumbers() {
        list = new ArrayList<Integer>(SIZE);
        for (int i = 0; i<SIZE; i++) {
            list.add(new Integer(i));
        }
    }
    public void writeList() {
        // FileWriter构造器会抛出一个 IOException,必须要捕获它
        PrintWriter out = new PrintWriter(new FileWriter("outfile.txt"));
        for(int i=0; i<SIZE; i++) {
            // list.get() 会抛出一个 IndexOutOfBoundsException,必须要捕获它
            out.print("value at: " + i + "=" + list.get(i));
        }
        out.close();
    }
}
编译与报错

当我们编译上述文件时,编译器会打印出一条错误信息,提示我们必须对 IOException 进行捕获。但是并没有提示 IndexOutOfBoundsException。原因是,IOException 是 CheckedExceptions,而 IndexOutOfBoundsException 是 UncheckedExceptions 。现在我们已经知道上述代码中可能发生异常的地方的,接下来我们将编写相应的异常处理器。

1)使用 try 把可能发生异常的代码块包裹起来,再使用一个或多个 catch 与这个 try 进行关联,每一个 catch 块都将是一个异常处理器。catch 中的参数即是 异常对象的类型,这个异常类型继承自 Throwable 。当 try 块中的异常发生时,catch 块中的代码才会被调用执行。运行时环境会调用所有 catch 块中的第一个与之匹配的异常处理器。

2)catch 异常处理器,不仅仅可以打印异常信息,还可以做更多事情,甚至可以让程序停止。catch 异常处理器胜于修复错误,或者提示用户选择一个决定,或者使用异常链把错误传递给更高级的异常处理器(这将在异常链中进行讲解)。

在JavaSE7 以后,一个 catch 块可以同时处理一个或多个异常类型,使用竖线 | 来分割多个不同类型的异常对象,这将有利于减少代码的冗余。基本语法格式如下:

try {
    // code
} catch (IOException | SQLException e) {
    // 当同时处理多个异常对象时,e 对象将默认是 final 的,你无法对其进行修改
    // logger.log(e);
    // throw e;
} finally {
    // do something
}

3)通常情况下,finally 块中的代码总会被执行,无论 try 中的预期的异常是否会发生(有一点需要注意,当 JVM 在执行 try 或 catch 块时发生了中断或者线程被杀死,那么 finally 块将不会被执行)。但是需注意,finally 块并不是用来处理异常的。在 finally 块中允许我们使用 return / continue / break 进行代码清理。无论是否有异常发生,都把代码清理工作放在 finally 块中,将是一种最佳实践。

基于这种最佳实践,我们可以把 PrintWriter 的关闭工作放在 finally 块中执行。从上述代码,我们可以看到 IOException 和 IndexOutOfBoundsException 都有可能发生。无论异常是否会发生,我们都应该把已经存在的 PrintWriter 对象关闭掉。最终完整的 try/catch/finally 代码如下:

import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {
    private List<Integer> list;
    private static final int SIZE = 10;
    // 初始化
    public ListOfNumbers() {
        list = new ArrayList<Integer>(SIZE);
        for (int i = 0; i<SIZE; i++) {
            list.add(new Integer(i));
        }
    }
    public void writeList() {
        // FileWriter构造器会抛出一个 IOException,必须要捕获它
        PrintWriter out = null;
        try {
            new PrintWriter(new FileWriter("outfile.txt"));
            for(int i=0; i<SIZE; i++) {
                // list.get() 会抛出一个 IndexOutOfBoundsException,必须要捕获它
                out.print("value at: " + i + "=" + list.get(i));
            }
        } catch (IOException e) {
            System.err.println("IOException:" + e.getMessage());
        } catch (IndexOutOfBoundsException e) {
            System.err.println("IndexOutOfBoundsException:" + e.getMessage());
        } finally {
            if (out != null) {
                out.close();
            } else {
                System.out.println("PrintWriter not open");
            }
        }
    }
}

特点提示:finally 块在避免资源浪费、资源泄露方面非常重要。我们应该把文件关闭、资源修复的工作放在 finally 块中去做,以确保无论发生什么,资源总是能够被关闭掉。

try-with-resources Statement

使用 try 块包裹一个或多个资源声明。所谓的“资源”就是一种必须在使用完成后及时关闭的对象。所有实现了 Closeable / AutoCloseable 接口的对象,都可以被认为是“资源”。把这种可关闭的“资源”声明放在 try 块中,我们就必须在使用完成后,对其进行关闭。

如下示例,我们使用 BufferedReader 读取文件中的首行数据。BufferedReader 就是一种在使用完成后必须关闭的“资源”。

static String readFirstLineFromFile(String filePath) throws IOException {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(filePath));
        return br.readLine();
    } catch (IOException e) {
        System.err.println(e.getMessage());
    } finally {
        if (br != null) {
            br.close();
        } else {
            System.out.println("BufferedReader not open");
        }
    }
}

在 Java 文档中,我们可以看到有很多实现了 AutoCloseable / Closeable 接口的类,其中 Closeable 接口继承自 AutoCloseable 接口。当 AutoCloseable 接口的 close() 抛出异常时,Closeable接口的 close() 方法就会抛出 IOException 异常。因此,那些继承了 AutoCloseable 接口的子类,都可以覆写其 close() 方法并抛出特定的异常,或者什么异常都不抛出。

使用Java方法抛出异常

在前面我们讲解了如何编写一个异常处理器。在一些场景中,非常适合在可能发生异常的代码中直接捕获异常。但,在另一些场景中,让Java方法在“调用栈”的更上一层去处理异常。比如,当你编写了一个Java包时并分享给别人使用时,你很难去满足所有用户的需求,在这种情况下,更好的解决文案不是去 catch 捕获异常,而是允许 Java方法向更高一层抛出异常。下面,我们将修改上述代码,让它抛出一个特定的异常,基本语法如下:

public void writeList() throws IOException, IndexOutOfBoundsException {}

需要注意是,对于 IndexOutOfBoundsException 这样的 UncheckedExceptions 无须强制抛出。但对于 IOException 这样的 CheckedExceptions 是必须要抛出的。

How To Throw Exceptions

Java平台提供了一系列的异常类。所有的这些异常类都是 Throwable 的后代,以便于我们在Java程序运行期间能够区分多种不同类型的异常。除此之外,你还可以创建自己的异常类,用来表示发生在你的业务中的异常。事实上,如果你是一个 Java包 开发者,你可以创建一系列自定义的异常类,进而与其它包、Java平台中的异常区分开来。在接下来的课程中,你还可以看到,你甚至可以创建自己的异常链。

所有的Java方法中都可以使用 throw 语句来抛出异常,throw 语句要求必须有一个 Throwable 的对象作为参数,这个Throwable 对象必须是 Throwable 类及其子类的实例。如下示例模拟“出栈”操作:

public Object pop() {
    Object obj;
    if (size == 0) {
        // throw 语句
        throw new EmptyStackException();
    }
    obj = objectAt(size - 1);
    setObjectAt(size - 1, null);
    size--;
    return obj;
}

在上述代码中,我们并不是在 Java方法后使用 throws 关键字来抛出异常,而是使用 throw 语句,因此这种方式的异常我们称之为 UncheckedException。

Throwable类及其子类的继承关系,如下图示。从图中可以看出,Throwable 有两大系列的子类,分别是 Error 和 Exception。

exceptions-throwable

1)当JVM中的动态链接失败或者其它硬件失败时,JVM就会抛出Error。简单程序一般不会捕获或抛出Error的。
2)大多数的程序会抛出和捕获那些来自Exception类的异常对象。一个 Exception预示着程序中发生了问题,但不是系统问题。Java平台中定义了很多异常类,比如IllegalAccessException 指的是没有找到某个Java方法;NegativeArraySizeException 指的是程序正在创建一个长度为负数的数组。RuntimeException 指的是对 Java API 的错误使用,最常见的运行时异常是NullPointerException,当我们访问一个引用为null的对象时就会报这个空指针异常。在下一部分的课程中,我们将强调不要在程序中抛出 RuntimeException 异常及其子类异常。

异常链

Java程序经常响应一个异常的方式是抛出另一个异常。事实上,第二个异常是由第一个异常造成的。这就是异常链,充分地理解它是非常有必要的。下面示例演示了如何使用异常链:

try {
    // do something
} catch (IOException e) {
    // 抛出另一个异常,即产生了 异常链
    throw new SampleException("Other IOException", e);
}

上述代码,当IOException发生时,新的 SampleException 异常将创建,并附着在异常链上,被抛给更高层次的异常处理器。

自定义异常类

当面对选择抛出何种类型的异常时,你可以使用Java平台提供的异常类型,也可以使用别人写的异常类型,还可以使用自已创建的异常类型。在如下情景中,我们可以自定义异常类:
1)你所需要的异常类型,Java平台没有提供时。
2)能有助于用户更好区分来自其它供应商抛出的异常时。
3)当你的程序需要抛出多种连带的异常时。
4)当你希望你的Java包可以独立隔离时。

举例说明,当你的Java包中定义了一系列的Java方法如下:
objectAt(int n),返回对象在列表中指定位置处的对象,它会抛出参数越界的异常。
firstObject(),返回列表中的第一个对象,它会抛出空列表的异常。
indexOf(Object o),返回指定对象在列表中的索引号,它会抛出对象不存在的异常。
在这个Java包中,有可能抛出多种类型的异常。为了方便地管理这些不同类型的异常,我们需要自定义异常类,并实现它们之间的内存继承关系,设计如下图示:

exceptions-hierarchy

在编写这几个自定义的异常类时,我们让 LinkedListException 再继承自 Exception 即可。

Unchecked Exceptions

Java语言并没有强制要求Java方法去捕获或者处理 UncheckedExceptions,如 RuntimeException 和 Error 及其子类。如果一种异常是用户无法捕获或者修复的,就把它定义成 UncheckedException吧。

使用Java异常的优势

现在我们知道了什么是异常,并且知道了该如何处理异常。那么接下来,让我们了解一下使用Java异常有哪些优势?
1)优势1:使用Java异常,可以把异常处理逻辑和正常的业务逻辑进行分离,避免异常逻辑和业务逻辑之间的混杂,从而友好地避免了错误处理逻辑对业务逻辑的干扰。伪代码示例如下:

readFile {
    try {
        open the file;
        determine its size;
        allocate that much memory;
        read the File into memory;
        close the File;
    } catch (FileOpenException e) {
        doSomething(e);
    } catch (SizeDetermineException e) {
        doSomething(e);
    } catch (MemoryException e) {
        doSomething(e);
    } catch (ReadException e) {
        doSomething(e);
    } catch (FileCloseException e) {
        doSomething(e);
    } finally {
        // always do
    }
}

2)优势2:使用Java异常可以把异常传递至"回调栈"中去。举例说明,有一组嵌套调用的方法,在 method1 中调用 method2,在 method2 中调用 method3,在 method3 中调用 readFile 方法。假设 readFile 方法发生了异常,按照传统的异常处理方式,我们需要 method2 和 method3 协助传递异常,直到 method1 ,然后在 method1 中作出相应的异常处理。但使用了 Java异常后,代码将变得更加整洁,Java异常会自动地把异常传递到更高层次的“回调栈”中去。伪代码如下:

method1 {
    try {
        call method2;
    } catch (ReadFileException e) {
        handleException(e);
    }
}

method2 throws exception {
    call method3;
}

method3 throws exception {
    call readFile;
}

3)优势3:使用Java异常可以方便地帮助我们对错误类型进行区分和分组。因为所有被抛出的异常都是对象,根据Java继承关系,这些异常对象会自然地被分组、分类。比如,IOException 及其子类异常,就代表了 I/O 操作时的系列异常,越往子级的异常类代表更加具体的异常。对一个异常处理器来讲,你们可捕获更加具体的异常类,也可以捕获更加模糊的父级异常类。但是,我们更推荐Java方法去捕获更加具体的异常类,而不是更加通用的异常类。因为捕获更加具体的异常类,能帮助我们准确地定位问题所在。

Java异常小结


End 2018-12-20

相关文章

网友评论

      本文标题:[Java Tutorials] 04 | Java Excep

      本文链接:https://www.haomeiwen.com/subject/hngxlqtx.html