让构造函数抛出异常是好的实践吗?

让构造函数抛出异常是一个好习惯吗? 例如,我有一个类Person,我有age作为它的唯一属性。现在 我写的类如下

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;
}
}
189446 次浏览

抛出Exception是一种糟糕的做法,因为这要求任何调用构造函数的人都要捕获Exception,这是一种糟糕的做法。

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

如果希望调用者捕获受控异常,则应该抛出受控异常(从Exception扩展而非RuntimeException的异常)。

在构造函数中抛出异常并不是不好的实践。事实上,只有是构造函数指示存在问题的合理方式;例如,参数无效。

我还认为抛出检查异常可以是OK1,假设检查异常是1)声明的,2)特定于你报告的问题,3)期望调用者为this2处理检查异常是合理的。

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

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


1 -有些人可能不同意,但在我看来,这种情况与在方法中抛出异常的情况没有实质性区别。标准的checked通知和unchecked通知同样适用于这两种情况 2 -例如,如果你试图打开一个不存在的文件,现有的FileInputStream构造函数将抛出FileNotFoundException。假设FileNotFoundException是一个检查异常__abc6是合理的,那么构造函数是要抛出的异常的最合适的地方。如果在第一次(比如)调用readwrite时抛出FileNotFoundException,则容易使应用程序逻辑更加复杂 3 -鉴于这是受控异常的一个激励例子,如果你不接受这一点,你基本上是在说所有的异常都应该是不受控的。这是不现实的……


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

assert的第二个问题是,如果断言失败,则会抛出AssertionError,而公认的智慧是,试图捕获Error及其任何子类型是坏主意

我从不认为在构造函数中抛出异常是一种坏习惯。在设计类时,您对类的结构应该是什么有一定的想法。如果其他人有不同的想法,并试图执行这个想法,那么您应该相应地出错,并向用户反馈错误是什么。在你的情况下,你可以考虑

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

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

断言似乎也不是正确的方法,因为您并没有试图在代码中发现错误。我认为用异常终止是绝对正确的做法。

这是完全正确的,我一直都这么做。如果它是参数检查的结果,我通常使用IllegalArguemntException。

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

另外,断言对于调用者来说更难以捕获,这很容易。

您可能希望在方法的javadocs中将其列为“throws”,并附上原因,以便调用者不会感到惊讶。

您不需要抛出受控异常。这是程序控制范围内的错误,因此您希望抛出一个未检查的异常。使用Java语言已经提供的未检查异常之一,例如IllegalArgumentExceptionIllegalStateExceptionNullPointerException

您可能还希望摆脱setter。你已经提供了一种通过构造函数初始化age的方法。它是否需要在实例化后进行更新?如果不是,跳过setter。好的规则是,不要把事情公开化。从private或default开始,并用final保护您的数据。现在每个人都知道Person已经被正确构造,并且是不可变的。你可以自信地使用它。

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

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

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

指南7-3 / OBJECT-3:防御部分初始化 非final类的构造函数 抛出异常,攻击者可以尝试获得部分访问权 该类的初始化实例。确保非final类 在构造函数成功完成之前完全不可用 从JDK 6开始,可以防止构造可子类的类 在Object构造函数完成之前抛出异常。来 这样做,在表达式中执行检查,在 调用this()或super().

    // 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;
}
}
为了与旧版本兼容,一个潜在的解决方案包括 初始化标志的使用。中将标志设置为中的最后一个操作 返回成功之前的构造函数。所有提供 网关进行敏感操作前必须先咨询标志 程序:< / p >
    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"
);
}
}
}
此外,对此类类的任何安全敏感的使用都应该进行检查 初始化标志的状态。在ClassLoader的例子中 构造时,它应该检查父类装入器是否为 初始化。< / p >

可以访问非final类的部分初始化实例 通过终结器攻击。攻击者覆盖受保护的finalize 方法,并尝试创建该方法的新实例 子类。此尝试失败(在上面的示例中 ClassLoader的构造函数中的SecurityManager检出抛出一个安全性 异常),但是攻击者只是忽略任何异常并等待 用于虚拟机对部分执行终结 初始化对象。当这种情况发生时,恶意finalize方法 实现被调用,使攻击者能够访问这个 对正在结束的对象的引用。虽然对象只是 部分初始化后,攻击者仍然可以调用它的方法, 从而绕过SecurityManager检查。当初始化 标志不阻止访问部分初始化的对象,它 是否阻止该对象上的方法为对象做任何有用的事情 攻击者。< / p > 使用初始化标志虽然安全,但可能很麻烦。简单的 确保公共非final类中的所有字段都包含一个safe 值(例如null),直到对象初始化完成 Successfully可以在类中表示一个合理的选择 不安全敏感。

一个更健壮,但也更冗长的方法是使用“指向”的指针 实现”(或“皮条客”)。类的核心被移动到 非公共类用接口类转发方法调用。任何 在类完全初始化之前尝试使用它将导致 在NullPointerException中。这种方法也适用于处理

.克隆反序列化攻击
    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
...
}
}

我一直认为在构造函数中抛出受控异常是不好的做法,或者至少应该避免。

原因是你不能这样做:

private SomeObject foo = new SomeObject();

相反,你必须这样做:

private SomeObject foo;
public MyObject() {
try {
foo = new SomeObject()
} Catch(PointlessCheckedException e) {
throw new RuntimeException("ahhg",e);
}
}
在构造SomeObject时,我知道它的参数是什么 那为什么要指望我把它包在try catch里呢? 啊,你说,但是如果我从动态参数构造一个对象,我不知道它们是否有效。 你可以…在将参数传递给构造函数之前验证它们。那将是很好的练习。 如果你所关心的是参数是否有效,那么你可以使用IllegalArgumentException.

所以不要抛出受控异常,只要这样做

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

当然,在某些情况下,抛出受控异常可能是合理的

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

但这种可能性有多大?

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

  1. 正如Richard提到的,您不能以简单的方式初始化实例。特别是在测试中,只在初始化期间将测试范围的对象包围在try-catch中来构建它真的很烦人。

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

  3. 闻起来像是设计不好。恕我直言,如果我被迫在构造函数中进行异常处理,我首先会问自己,在我的类中是否有任何设计欺诈。有时这是必要的,但我将其外包给构建器或工厂,以使构造器尽可能简单。

因此,如果有必要在构造函数中进行一些异常处理,为什么不将此逻辑外包给工厂的构建器呢?这可能需要多写几行代码,但可以让您自由地实现更健壮、更适合的异常处理,因为您可以将异常处理的逻辑更多地外包出去,而不必拘泥于构造函数,因为构造函数将封装太多逻辑。如果您正确地委托异常处理,客户端不需要知道任何关于构造逻辑的信息。