使构造函数抛出异常是一种好习惯吗? [重复]

问题

这个问题在这里已有答案:

  • 何时抛出异常? 33个答案

使构造函数抛出异常是一个好习惯吗?例如,我有一个classPerson,我有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;
  }
}

#1 热门回答(131 赞)

在构造函数中抛出异常并不是坏习惯。事实上,它是构造函数表明存在问题的唯一合理方式;例如参数无效。

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

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

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

assert的第二个问题是,如果一个断言失败,那么将会抛出AssertionError,并且接受的智慧是,它意味着尝试捕获Error及其任何子类型。


#2 热门回答(22 赞)

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

原因是你不能这样做:

private SomeObject foo = new SomeObject();

相反,你必须这样做:

private SomeObject foo;
public MyObject() {
    try {
        foo = new SomeObject()
    } Catch(PointlessCheckedException e) {
       throw new RuntimeException("ahhg",e);
    }
}

在我构建SomeObjectII时,我知道它的参数是什么,为什么我应该将它包装在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();
}

但这种情况多久发生一次?


#3 热门回答(17 赞)

another answer here中所述,在JavaSecure Coding Guidelines的指南7-3中,在非final类的构造函数中抛出异常会打开潜在的攻击向量:

准则7-3 / OBJECT-3:抵御非最终类的部分初始化实例当非最终类中的构造函数抛出异常时,攻击者可以尝试访问该类的部分初始化实例。确保非final类仍然完全不可用,直到其构造函数成功完成。从JDK 6开始,可以通过在Object构造函数完成之前抛出异常来防止构造可子类化的类。为此,请在对this()或super()的调用中计算的表达式中执行检查。 //非最终的java.lang.ClassLoader
    公共抽象类ClassLoader {
        protected ClassLoader(){
            此(securityManagerCheck());
        }
        private ClassLoader(Void被忽略){
            // ...继续初始化...
        }
        private static Void securityManagerCheck(){
            SecurityManager security = System.getSecurityManager();
            if(security!= null){
                security.checkCreateClassLoader();
            }
            return null;
        }
    }
 为了与旧版本兼容,潜在的解决方案涉及使用初始化标志。在成功返回之前,将标志设置为构造函数中的最后一个操作。提供敏感操作网关的所有方法必须首先查询标志,然后再继续:public abstract class ClassLoader {

        private volatile boolean initialized;

        protected ClassLoader(){
            //创建ClassLoader所需的权限
            securityManagerCheck();
            在里面();

            //构造函数的最后一个动作。
            this.initialized = true;
        }
        protected final类defineClass(...){
            checkInitialized();

            //遵循常规逻辑
            ...
        }

        private void checkInitialized(){
            if(!initialized){
                抛出新的SecurityException(
                    "非最终未初始化"
                );
            }
        }
    }
 此外,此类的任何安全敏感用法都应检查初始化标志的状态。在ClassLoader构造的情况下,它应检查其父类加载器是否已初始化。可以通过终结器攻击来访问非最终类的部分初始化实例。攻击者会覆盖子类中受保护的finalize方法,并尝试创建该子类的新实例。此尝试失败(在上面的示例中,SecurityManager检查ClassLoader的构造函数会引发安全性异常),但攻击者只是忽略任何异常并等待虚拟机对部分初始化的对象执行最终化。当发生这种情况时,会调用恶意的finalize方法实现,让攻击者可以访问它,这是对正在最终确定的对象的引用。尽管该对象仅部分初始化,但攻击者仍然可以在其上调用方法,从而绕过SecurityManager检查。虽然初始化标志不会阻止对部分初始化对象的访问,但它确实阻止该对象上的方法对攻击者做任何有用的操作。使用初始化标志虽然安全,但可能很麻烦。简单地确保公共非final类中的所有字段包含安全值(例如null),直到对象初始化成功完成,才能在对安全性不敏感的类中表示合理的替代方法。更强大但更冗长的方法是使用"指向实现的指针"(或"pimpl")。使用接口类转发方法调用将类的核心移动到非公共类中。在完全初始化之前尝试使用该类将导致NullPointerException。这种方法也适用于处理克隆和反序列化攻击。公共抽象类ClassLoader {

        private final ClassLoaderImpl impl;

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

    / * pp * / class ClassLoaderImpl {
        / * pp * / ClassLoaderImpl(){
            //创建ClassLoader所需的权限
            securityManagerCheck();
            在里面();
        }

        / * pp * / Class defineClass(...){
            //遵循常规逻辑
            ...
        }
    }