Throwing Exceptions in Constructors – 在构造函数中抛出异常

最后修改: 2021年 8月 11日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Exceptions provide separation of error handling code from the normal flow of the application. It’s not uncommon to throw an exception during the instantiation of an object.

异常提供了错误处理代码与应用程序正常流程的分离。在对象的实例化过程中抛出一个异常并不罕见。

In this article, we’ll examine all the details about throwing exceptions in constructors.

在这篇文章中,我们将研究关于在构造函数中抛出异常的所有细节。

2. Throwing Exceptions in Constructors

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

Constructors are special types of methods invoked to create an object. In the following sections, we’ll look into how to throw exceptions, which exceptions to throw, and why we would throw exceptions in constructors.

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

2.1. How?

2.1.如何?

Throwing exceptions in the constructor is no different from doing so in any other method. Let’s start by creating an Animal class with a no-arg constructor:

在构造函数中抛出异常与在其他方法中抛出异常没有什么不同。让我们先创建一个Animal类,它有一个无条件的构造函数。

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

Here, we’re throwing InstantiationException, which is a checked exception.

在这里,我们要抛出实例化异常,这是一个检查过的异常

2.2. Which Ones?

2.2.哪些人?

Even though throwing any type of exception is allowed, let’s establish some best practices.

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

First, we don’t want to throw “java.lang.Exception”. This is because the caller cannot possibly identify what kind of exception and thereby handle it.

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

Second, we should throw a checked exception if the caller has to forcibly handle it.

第二,如果调用者必须强行处理,我们应该抛出一个检查过的异常。

Third, we should throw an unchecked exception if a caller cannot recover from the exception.

第三,如果调用者不能从异常中恢复,我们应该抛出一个未检查的异常。

It’s important to note that these practices are equally applicable for both methods and constructors.

需要注意的是,这些做法对方法和构造函数都同样适用

2.3. Why?

2.3.为什么?

In this section, let’s understand why we might want to throw exceptions in the constructor.

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

Argument validation is a common use case for throwing exceptions in the constructor. Constructors are mostly used to assign values of variables. If the arguments passed to the constructor are invalid, we can throw exceptions. Let’s consider a quick example:

参数验证是在构造函数中抛出异常的一个常见用例。构造函数大多用于分配变量的值。如果传递给构造函数的参数是无效的,我们就可以抛出异常。让我们考虑一个简单的例子。

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");
}

In the above example, we’re performing argument validation before initializing the object. This helps to ensure that we’re creating only valid objects.

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

Here, if the id passed to the Animal object is null, we can throw NullPointerException For arguments that are non-null but still invalid, such as a negative value for age, we can throw an IllegalArgumentException.

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

Security checks are another common use case for throwing exceptions in the constructor. Some of the objects need security checks during their creation. We can throw exceptions if the constructor performs a possibly unsafe or sensitive operation.

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

Let’s consider our Animal class is loading attributes from a user input file:

让我们考虑一下我们的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");
    }
}

In our example above, we prevented the Path Traversal attack. This is achieved by not allowing absolute paths and directory traversal. For example, consider file “a/../b.txt”. Here, the canonical path and the absolute path are different, which can be a potential Directory Traversal attack.

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

3. Inherited Exceptions in Constructors

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

Now, let’s talk about handling superclass exceptions in constructors.

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

Let’s create a child class, Bird, that extends our Animal class:

让我们创建一个子类,Bird,它扩展了我们的Animal类。

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

Since super() has to be the first line in the constructor, we can’t simply insert a try-catch block to handle the checked exception thrown by the superclass.

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

Since our parent class Animal throws the checked exception InstantiationException, we can’t handle the exception in the Bird constructor. Instead, we can propagate the same exception or its parent exception.

由于我们的父类Animal抛出了被检查的异常InstantiationException,我们不能在Bird构造函数中处理这个异常。相反,我们可以传播相同的异常或其父级异常。

It’s important to note that the rule for exception handling with respect to method overriding is different. In method overriding, if the superclass method declares an exception, the subclass overridden method can declare the same, subclass exception, or no exception, but cannot declare a parent exception.

值得注意的是,关于方法覆盖的异常处理规则是不同的。在方法重载中,如果超类方法声明了一个异常,子类重载方法可以声明相同的、子类的异常,或者没有异常,但不能声明父类的异常。

On the other hand, unchecked exceptions need not be declared, nor can they be handled inside subclass constructors.

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

4. Security Concerns

4.安全方面的担忧

Throwing an exception in a constructor can lead to partially initialized objects. As described in Guideline 7.3 of Java Secure Coding Guidelines, partially initialized objects of a non-final class are prone to a security concern known as a Finalizer Attack.

在构造函数中抛出异常会导致部分初始化的对象。正如Java安全编码指南的指南7.3所描述的那样,非最终类的部分初始化对象容易出现被称为 “最终者攻击 “的安全问题。

In short, a Finalizer attack is induced by subclassing partially initialized objects and overriding its finalize() method, and attempts to create a new instance of that subclass. This will possibly bypass the security checks done inside the constructor of the subclass.

简而言之,Finalizer攻击是通过子类化部分初始化的对象并重写其finalize()方法,并试图创建该子类的新实例而引起的。这可能会绕过子类构造函数中的安全检查。

Overriding the finalize() method and marking it final can prevent this attack.

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

However, the finalize() method has been deprecated in Java 9, thus preventing this type of attack.

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

5. Conclusion

5.总结

In this tutorial, we’ve learned about throwing exceptions in constructors, along with the associated benefits and security concerns. Also, we took a look at some best practices for throwing exceptions in constructors.

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

As always, the source code used in this tutorial is available over on GitHub.

一如既往,本教程中所使用的源代码可在GitHub上获得