Java异常:概念区别

前段时间读了Effective Java第三版中的异常,读了之后也没总结,很多知识点都是囫囵吞枣地理解。尤其是一些概念上的区别会直接影响如何使用Java异常,因此个人认为有必要了解相关的概念区别。文章很多问题来自Hollis的星球,有一些问题则是我自己在阅读过程中产生的。一些答案来自Hollis在星球里给出的解答或者是StackOverlow、WikiBook等网站上的解答,我则以自己的理解来综合总结。

1. Error和Exception的区别

@georgios-gousios的在Exception Handling Bug Hazards on Android中的一张幻灯片准确地解释了错误和异常在Java中的区别。

Java异常:概念区别_第1张图片
The Java Exception Hierarchy

Throwable有三个重要的子类:

  • 错误:发生了非常严重的问题,大部分应用都应该推出执行而不是尝试这去解决这个问题。
  • 非受检异常:通常指NullPointerException这一类的编程错误或者是非法的参数。应用可以处理非受检异常并恢复,然后继续执行,或者至少在线程的run()方法中捕获并记录该非受检异常。
  • 受检异常:应用需要去捕获并处理这类异常,比如FileNotFoundExceptionTimeoutException

错误不应该被捕获或者处理(除了极少数特殊情况)。异常则是异常处理中的关键部分。Java文档对错误做了很好的解释:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.
错误是Throwable的子类,一个合理的应用不应该捕获指示了严重问题的错误,大部分这类错误都是非正常情况。

一起来看几个Error的子类和它们在Java文档中的注释:

  • AnnotationFormatError: 当注解分析器从一个类文件中读取注解并发现注解格式错误时,抛出AnnotationFormatError
  • AssertionError:抛出该异常用来表明Java断言错误。
  • LinkageErrorLinkageError的子类通常表明一个类和另一个类有着某种依赖关系;但是,在依赖类被编译以后,被依赖类被修改了以致无法与依赖类兼容(破坏了依赖关系)。
  • VirtualMachineError: 抛出并表明Java虚拟机宕机,或者表明Java虚拟机无法获取继续计算所必需的资源。

Effective Java 3中关于AssertionError的例子:
代码清单1:AssertionError禁止用户去初始化一个类:

public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
    throw new AssertionError();
}
     ...  // Remainder omitted
}

参考

  • StackOverflow: Differences between Exception and Error

2. 受检异常(Checked Exception)与非受检异常(Unchecked Exception)

2.1 受检异常

受检异常是一种我们必须捕获或在抛出方法中声明的异常。比如,java.io.IOException就是一个受检异常。为了更好地明白什么是受检异常,请参考下列代码:
代码清单2: 未处理的受检异常

public void ioOperation(boolean isResourceAvailable) {
    if (!isResourceAvailable) {
        throw new IOException();
    }
}

以上代码将无法通过编译,因为代码抛出了受检异常。产生的编译错误可以通过以下两种方式解决:

  • 捕获并处理该异常;
    代码清单3
    public void ioOperation(boolean isResourceAvailable) {
        try {
            if (!isResourceAvailable) {
                throw new IOException();
            }
        } catch(IOException e) {
            // Handle caught exceptions.
        }
    }
    
  • 在方法中声明抛出异常;
    代码清单4
    public void ioOperation(boolean isResourceAvailable) throws IOException {
        if (!isResourceAvailable) {
            throw new IOException();
        }
    }
    

在Java类结构中,如果一个异常继承自java.lang.Throwable而非java.lang.RuntimeException或者java.lang.Error,则该异常被认为是受检异常。所有应用和业务逻辑异常都应该是受检异常。

即使一个方法声明抛出一个异常,但实际上却没有抛出的情况是不可能的。调用者依然要处理该异常。受检异常的声明会产生多米诺骨牌效应,即调用了已定义方法的任何方法都要处理受检异常。

所以在编译期间,Java语言的编译器会会通过分析每一个方法的主体来检查一段程序是否包含了所有应用异常的处理器。假如通过执行该方法的主体,该方法会向调用者抛出一个异常,那么该异常必须被声明。编译器是如何知晓一个方法的主体是否能抛出异常的呢?这就简单了。在方法的主体中包含有对其他方法的调用,编译器会通过检查每个方法的签名来知晓它们抛出了什么异常。

2.1.1 为什么强制提供异常处理?

也许程序员会觉得强制异常处理很无聊,但是这迫使他们去思考所有的受检异常并提升代码的质量。编译期间对异常处理器是否出现的检查就是为了使应用开发人员的工作变得更简单。调试检查一个特定的异常是否有对应的捕获是一个漫长的过程。在传统的编程语言中,如C和C++,必须要有一个分开的错误处理调试。在Java中,我们可以肯定当一个应用异常被抛出,在程序的某个地方该异常一定会被处理。在C和C++中就需要去测试(译注:异常和错误是否被处理),在Java中这就无需测试,所以节约的时间就可以用于更有意义的测试,比如测试业务特征。

2.1.2 覆盖一个方法时可以声明什么异常?

关键字throws后声明的特定受检异常是实现者和用户之间合同的一部分。一个覆盖方法可以声明相同的异常,该异常的子类或者没有异常。

2.1.3 实现一个接口时可以声明什么异常?

涉及到接口时,接口的实现可能跳过throws语句,但是你如果想保留那就必须与接口声明兼容。换而言之,接口的实现必须抛出相同的异常或者是异常的子类,又或者是没有异常。

2.2 非受检异常

非受检、不用捕获或运行时异常可以在不被捕获和声明的情况下被抛出。
代码清单5

public void futureMethod() {
    throw new RuntimeException("This method is not yet implemented");
}

但是,你依然可以声明并捕获这类异常。运行时异常并非业务异常,它们通常和硬编码的问题相关联,比如数据错误、算术溢出、除以0等等。换而言之,这些错误无法被解决也无法预测。最凑名昭著的运行时异常就是NullPointerException。

一个运行时异常必须是RuntimeException或者Error的子类。

有时为了记录异常而去捕获所有异常是值得的,之后再将异常抛出。比如在servlet编程中,

你可能感兴趣的:(Java异常:概念区别)