如何优雅地在构造函数抛异常?
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”。在这里,canonical 路径和 absolute 路径是不同的,后者可能引发目录遍历攻击。
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. 安全问题
在构造函数中抛出异常可能导致未完全初始化的对象。非 final 类的未完全初始化对象容易出现称为 Finalizer 攻击的安全问题。
简而言之,Finalizer 攻击是通过子类化部分初始化的对象并覆盖其finalize()方法,并尝试创建该子类的新实例而引起的。这可能会绕过在子类的构造函数中完成的安全检查。
覆盖finalize()方法并将其标记为final可以防止这种攻击。
好消息是,Java 9 中已弃用finalize()方法,从而防止了此类攻击。
5. 结论
在本文中,我们学习了在构造函数中抛出异常以及相关的好处和安全问题。此外,我们还研究了在构造函数中抛出异常的一些最佳实践。
#学习路径#