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 对象的 id 为 null ,我们可以抛出 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 上获取。