关于java:使构造函数抛出异常是一种好习惯吗?

Is it good practice to make the constructor throw an exception?

本问题已经有最佳答案,请猛点这里访问。

让构造函数抛出异常是一个好的实践吗?例如,我有一个类Person,我将age作为它的唯一属性。现在我提供的课程是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
  int age;
  Person(int age) throws Exception{
   if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }

  public void setAge(int age) throws Exception{
  if (age<0)
       throw new Exception("invalid age");
   this.age = age;
  }
}


在构造函数中抛出异常并不是一个糟糕的实践。事实上,对于一个构造函数来说,这是表示存在问题的唯一合理的方法;例如,参数无效。

然而,明确声明或抛出java.lang.Exception几乎总是不好的做法。

您应该选择一个与发生的异常条件相匹配的异常类。如果抛出Exception,则调用方很难将此异常与任何其他可能已声明和未声明的异常分开。这使得错误恢复变得困难,如果调用者选择传播异常,问题就会扩散。

有人建议用assert来核对论点。问题在于,可以通过jvm命令行设置打开和关闭对assert断言的检查。使用断言来检查内部不变量是可以的,但是使用它们来实现JavaDoc中指定的参数检查不是一个好主意…因为这意味着您的方法将只在启用断言检查时严格实现规范。

assert的第二个问题是,如果一个断言失败,那么AssertionError就会被抛出,得到的智慧是试图抓住Error及其任何子类型是一个坏主意。


我一直认为在构造函数中抛出检查过的异常是一种糟糕的实践,或者至少是应该避免的事情。

原因是您不能这样做:

1
private SomeObject foo = new SomeObject();

相反,您必须这样做:

1
2
3
4
5
6
7
8
private SomeObject foo;
public MyObject() {
    try {
        foo = new SomeObject()
    } Catch(PointlessCheckedException e) {
       throw new RuntimeException("ahhg",e);
    }
}

当我构建某个对象时,我知道它的参数是什么。那么,为什么要期望我把它包装在一个试捕获中呢?你说,但是如果我用动态参数构造一个对象,我不知道它们是否有效。好吧,你可以…在将参数传递给构造函数之前先验证这些参数。那是个很好的练习。如果您只关心参数是否有效,那么可以使用IllegalArgumentException。

所以不要抛出选中的异常,只要

1
2
3
4
public SomeObject(final String param) {
    if (param==null) throw new NullPointerException("please stop");
    if (param.length()==0) throw new IllegalArgumentException("no really, please stop");
}

当然,在某些情况下,抛出检查过的异常可能是合理的。

1
2
3
public SomeObject() {
    if (todayIsWednesday) throw new YouKnowYouCannotDoThisOnAWednesday();
}

但这种可能性多久一次?


正如在这里的另一个答案中提到的,在Java安全编码准则的准则7-3中,在非最终类的构造函数中抛出异常会打开潜在的攻击向量:

Guideline 7-3 / OBJECT-3: Defend against partially initialized
instances of non-final classes When a constructor in a non-final class
throws an exception, attackers can attempt to gain access to partially
initialized instances of that class. Ensure that a non-final class
remains totally unusable until its constructor completes successfully.

From JDK 6 on, construction of a subclassable class can be prevented
by throwing an exception before the Object constructor completes. To
do this, perform the checks in an expression that is evaluated in a
call to this() or super().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    // non-final java.lang.ClassLoader
    public abstract class ClassLoader {
        protected ClassLoader() {
            this(securityManagerCheck());
        }
        private ClassLoader(Void ignored) {
            // ... continue initialization ...
        }
        private static Void securityManagerCheck() {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkCreateClassLoader();
            }
            return null;
        }
    }

For compatibility with older releases, a potential solution involves
the use of an initialized flag. Set the flag as the last operation in
a constructor before returning successfully. All methods providing a
gateway to sensitive operations must first consult the flag before
proceeding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public abstract class ClassLoader {

        private volatile boolean initialized;

        protected ClassLoader() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();

            // Last action of constructor.
            this.initialized = true;
        }
        protected final Class defineClass(...) {
            checkInitialized();

            // regular logic follows
            ...
        }

        private void checkInitialized() {
            if (!initialized) {
                throw new SecurityException(
                   "NonFinal not initialized"
                );
            }
        }
    }

Furthermore, any security-sensitive uses of such classes should check
the state of the initialization flag. In the case of ClassLoader
construction, it should check that its parent class loader is
initialized.

Partially initialized instances of a non-final class can be accessed
via a finalizer attack. The attacker overrides the protected finalize
method in a subclass and attempts to create a new instance of that
subclass. This attempt fails (in the above example, the
SecurityManager check in ClassLoader's constructor throws a security
exception), but the attacker simply ignores any exception and waits
for the virtual machine to perform finalization on the partially
initialized object. When that occurs the malicious finalize method
implementation is invoked, giving the attacker access to this, a
reference to the object being finalized. Although the object is only
partially initialized, the attacker can still invoke methods on it,
thereby circumventing the SecurityManager check. While the initialized
flag does not prevent access to the partially initialized object, it
does prevent methods on that object from doing anything useful for the
attacker.

Use of an initialized flag, while secure, can be cumbersome. Simply
ensuring that all fields in a public non-final class contain a safe
value (such as null) until object initialization completes
successfully can represent a reasonable alternative in classes that
are not security-sensitive.

A more robust, but also more verbose, approach is to use a"pointer to
implementation" (or"pimpl"). The core of the class is moved into a
non-public class with the interface class forwarding method calls. Any
attempts to use the class before it is fully initialized will result
in a NullPointerException. This approach is also good for dealing with
clone and deserialization attacks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public abstract class ClassLoader {

        private final ClassLoaderImpl impl;

        protected ClassLoader() {
            this.impl = new ClassLoaderImpl();
        }
        protected final Class defineClass(...) {
            return impl.defineClass(...);
        }
    }

    /* pp */ class ClassLoaderImpl {
        /* pp */ ClassLoaderImpl() {
            // permission needed to create ClassLoader
            securityManagerCheck();
            init();
        }

        /* pp */ Class defineClass(...) {
            // regular logic follows
            ...
        }
    }


您不需要抛出选中的异常。这是程序控制范围内的一个bug,因此您希望抛出一个未经检查的异常。使用Java语言已经提供的未检查异常之一,如EDCOX1×9,EDCOX1,10,或EDCOX1,11。

你也可能想摆脱二传手。您已经提供了通过构造函数启动age的方法。实例化后是否需要更新?如果没有,跳过setter。一个好的规则,不要把事情公开化。从private或default开始,用final保护您的数据。现在大家都知道,Person的构造是正确的,是不变的。它可以自信地使用。

很可能这就是你真正需要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {

  private final int age;  

  Person(int age) {    

    if (age < 0)
       throw new IllegalArgumentException("age less than zero:" + age);

    this.age = age;  
  }

  // setter removed


这是完全有效的,我一直这样做。如果是参数检查的结果,我通常使用illegalarguemntexception。

在这种情况下,我不会建议断言,因为它们在部署构建中被关闭,并且您总是希望阻止这种情况的发生,但是如果您的组在断言打开的情况下进行所有测试,并且您认为在运行时丢失参数问题的机会比抛出可能更容易接受的异常可能导致运行时崩溃。

另外,断言对于调用者来说更难陷阱,这很容易。

您可能希望在方法的javadocs中将其列为一个"throw",并列出原因,这样调用方就不会感到惊讶了。


我从未认为在构造函数中抛出异常是一种糟糕的实践。当类被设计时,您有一个关于该类的结构应该是什么的概念。如果其他人有不同的想法并试图执行该想法,那么您应该相应地出错,并向用户反馈错误是什么。在你的情况下,你可以考虑

1
2
if (age < 0) throw new NegativeAgeException("The person you attempted" +
                      "to construct must be given a positive age.");

其中NegativeAgeException是您自己构建的一个异常类,可能扩展了另一个异常,如IndexOutOfBoundsException或类似的类。

断言似乎也不完全是可行的,因为您不想在代码中发现错误。我想说,在这里终止一个例外是绝对正确的。


抛出异常是一种糟糕的实践,因为这要求调用构造函数的任何人捕获异常,这是一种糟糕的实践。

最好让一个构造函数(或任何方法)抛出一个异常,一般来说,这是未选中的IllegalArgumentException,因此编译器不会强制您捕获它。

如果希望调用者捕获检查的异常(从异常扩展而不是运行时异常),则应该抛出它。


我不赞成在构造函数中抛出异常,因为我认为这是不干净的。我的观点有几个原因。

  • 正如Richard提到的,您不能以简单的方式初始化实例。尤其是在测试中,只通过在初始化期间将一个测试范围的对象包围在一个try-catch中来构建它是非常烦人的。

  • 构造函数应该是无逻辑的。完全没有理由将逻辑封装在构造函数中,因为您总是以关注点分离和单一责任原则为目标。由于构造函数的关注点是"构造一个对象",因此如果遵循这种方法,它不应该封装任何异常处理。

  • 闻起来像是坏设计。imho如果我被迫在构造函数中进行异常处理,我首先会问自己在我的类中是否有设计欺诈。有时是必要的,但是我把它外包给一个建设者或工厂,以尽可能地保持建设者的简单。

  • 因此,如果有必要在构造函数中进行一些异常处理,为什么不将此逻辑外包给工厂的构建者呢?这可能需要更多的代码行,但可以让您自由地实现更健壮、更适合的异常处理,因为您可以将逻辑更多地外包给异常处理,并且不会粘到构造函数上,因为构造函数会封装太多的逻辑。如果您正确地委托异常处理,那么客户机就不需要知道任何关于构建逻辑的信息。