1. 概述

异常将错误处理代码与应用程序的正常流程分开。 在对象实例化期间抛出异常并不罕见。

在本文中,我们将研究有关在构造函数中引发异常的所有详细信息。

2. 在构造函数中抛出异常

构造函数是为创建对象而调用的特殊类型的方法。 在下面的部分中,我们将研究如何引发异常、引发哪些异常以及为什么要在构造函数中引发异常。

2.1.如何?

在构造函数中抛出异常与在任何其他方法中抛出异常没有什么不同。让我们首先创建一个带有无参数构造函数的 Animal 类:

public Animal() throws InstantiationException {
    throw new InstantiationException("Cannot be instantiated");
}

在这里,我们抛出 InstantiationException ,这是一个受检查的异常

2.2.哪个?

尽管允许抛出任何类型的异常,但让我们建立一些最佳实践。

首先,我们不想抛出“ java.lang.Exception” 。这是因为调用者不可能识别出哪种异常并从而处理它。

其次,如果调用者必须强制处理它,我们应该抛出一个已检查的异常。

第三,如果调用者无法从异常中恢复,我们应该抛出未经检查的异常。

值得注意的是, 这些实践同样适用于方法和构造函数

2.3.为什么?

在本节中,让我们了解为什么我们可能想在构造函数中抛出异常。

参数验证是构造函数中引发异常的常见用例。 构造函数主要用于为变量赋值。如果传递给构造函数的参数无效,我们可以抛出异常。让我们考虑一个简单的例子:

public Animal(String id, int age) {
    if (id == null)
        throw new NullPointerException("Id cannot be null");
    if (age < 0)
        throw new IllegalArgumentException("Age cannot be negative");
}

在上面的示例中,我们在初始化对象之前执行参数验证。这有助于确保我们只创建有效的对象。

在这里,如果传递给 Animal 对象的 idnull ,我们可以抛出 NullPointerException 对于非 null 但仍然无效的参数,例如 age 的负值,我们可以抛出 IllegalArgumentException

安全检查是在构造函数中引发异常的另一个常见用例。 有些对象在创建过程中需要进行安全检查。如果构造函数执行可能不安全或敏感的操作,我们可以抛出异常。

让我们考虑我们的 Animal 类正在从用户输入文件加载属性:

public Animal(File file) throws SecurityException, IOException {
    if (file.isAbsolute()) {
        throw new SecurityException("Traversal attempt");
    }
    if (!file.getCanonicalPath()
        .equals(file.getAbsolutePath())) {
        throw new SecurityException("Traversal attempt");
    }
}

在上面的示例中,我们阻止了路径遍历攻击。这是通过不允许绝对路径和目录遍历来实现的。例如,考虑文件“a/../b.txt”。这里,规范路径和绝对路径不同,这可能是潜在的目录遍历攻击。

3. 构造函数中继承的异常

现在,我们来谈谈在构造函数中处理超类异常。

让我们创建一个子类 Bird 来扩展我们的 Animal 类:

public class Bird extends Animal {
    public Bird() throws ReflectiveOperationException {
        super();
    }
    public Bird(String id, int age) {
        super(id, age);
    }
}

由于 super() 必须是构造函数中的第一行,因此我们不能简单地插入一个 try-catch 块来处理超类抛出的已检查异常。

由于我们的父类 Animal 抛出了已检查异常 InstantiationException ,因此我们无法在 Bird 构造函数中处理该异常。 相反,我们可以传播相同的异常或其父异常。

需要注意的是,关于方法重写的异常处理规则是不同的。在方法重写中,如果超类方法声明了异常,则子类重写的方法可以声明相同、子类异常或不声明异常,但不能声明父类异常。

另一方面,未经检查的异常不需要声明,也不能在子类构造函数内处理。

4. 安全问题

在构造函数中引发异常可能会导致对象部分初始化。如《Java 安全编码指南》指南 7.3 中所述,非最终类的部分初始化对象容易出现称为终结器攻击的安全问题。

简而言之,Finalizer 攻击是通过对部分初始化的对象进行子类化并覆盖其 Finalize() 方法而引发的,并尝试创建该子类的新实例。这可能会绕过子类构造函数内完成的安全检查。

重写 finalize() 方法并将其标记为 final 可以防止这种攻击。

然而, finalize() 方法在 Java 9 中已被弃用,从而防止了此类攻击。

5. 结论

在本教程中,我们了解了如何在构造函数中引发异常,以及相关的好处和安全问题。此外,我们还了解了在构造函数中抛出异常的一些最佳实践。

与往常一样,本教程中使用的源代码可以在 GitHub 上获取。