让构造函数抛出异常是一个好习惯吗?
例如,我有一个班级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;
}
}
答案 0 :(得分:149)
在构造函数中抛出异常并非坏习惯。实际上,构造函数指示存在问题是唯一的合理方式;例如参数无效。
然而,明确声明或抛出java.lang.Exception
几乎总是不好的做法。
您应该选择一个与发生的异常情况相匹配的异常类。如果抛出Exception
,调用者很难将此异常与任何其他可能的声明和未声明的异常分开。这使得错误恢复变得困难,并且如果调用者选择传播异常,则问题就会传播。
有人建议使用assert
来检查参数。这样做的问题是可以通过JVM命令行设置打开和关闭assert
断言的检查。使用断言来检查内部不变量是可以的,但是使用它们来实现javadoc中指定的参数检查并不是一个好主意...因为这意味着你的方法只会在启用断言检查时严格执行规范。
assert
的第二个问题是,如果一个断言失败,那么AssertionError
将被抛出,并且接受的智慧是尝试捕获它是一个坏主意 Error
及其任何子类型。
答案 1 :(得分:25)
我一直认为在构造函数中抛出已检查的异常是不好的做法,或者至少应该避免的事情。
原因是你不能这样做:
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();
}
但这种情况多久发生一次?
答案 2 :(得分:22)
正如another answer here中提到的,在Java Secure Coding Guidelines的准则7-3中,在非final类的构造函数中抛出异常会打开潜在的攻击向量:
准则7-3 / OBJECT-3:防止部分初始化 非final类的实例当非final类中的构造函数时 抛出异常,攻击者可以尝试部分获取访问权限 初始化该类的实例。确保非最终类 在构造函数成功完成之前,它仍然完全无法使用。
从JDK 6开始,可以防止构造可子类化的类 通过在Object构造函数完成之前抛出异常。至 执行此操作,在a中计算的表达式中执行检查 调用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; } }
为了与旧版本兼容,可能涉及一个潜在的解决方案 使用初始化标志。将标志设置为最后一次操作 成功返回之前的构造函数。提供的所有方法 敏感操作的网关必须先咨询该标志 程序:
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的情况下 构造时,应检查其父类加载器是否为 初始化。
可以访问非final类的部分初始化实例 通过终结者攻击。攻击者会覆盖受保护的终结 子类中的方法,并尝试创建它的新实例 子类。这种尝试失败了(在上面的例子中, SecurityManager签入ClassLoader的构造函数会引发安全性 例外),但攻击者只是忽略任何异常并等待 用于虚拟机在部分上执行最终化 初始化对象。当发生恶意终结方法时 调用实现,让攻击者访问它,a 引用正在最终确定的对象。虽然对象是唯一的 部分初始化后,攻击者仍然可以在其上调用方法, 从而绕过SecurityManager检查。虽然初始化了 flag不会阻止访问部分初始化的对象 确实阻止该对象上的方法对该对象执行任何有用的操作 攻击者。
使用初始化标志虽然安全,但可能很麻烦。只是 确保公共非最终类中的所有字段都包含安全 对象初始化完成之前的值(例如null) 成功地可以在类中表示合理的替代方案 对安全性不敏感。
更强大,但也更冗长的方法是使用“指针” 实现“(或”pimpl“)。该类的核心被移入a 非公共类与接口类转发方法调用。任何 将导致在完全初始化之前尝试使用该类 在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 ... } }
答案 3 :(得分:7)
这完全有效,我一直这样做。如果它是参数检查的结果,我通常会使用IllegalArguemntException。
在这种情况下,我不会建议断言因为它们在部署版本中被关闭而你总是希望阻止这种情况发生,但是如果你的团队做了所有它正在测试打开断言而你认为它们是有效的在运行时丢失参数问题的可能性比抛出可能更有可能导致运行时崩溃的异常更容易接受。
此外,断言对于调用者来说更难以陷阱,这很容易。
您可能希望将其列为方法的javadoc中的“throws”以及原因,以便呼叫者不会感到惊讶。
答案 4 :(得分:7)
您无需抛出已检查的异常。这是程序控制中的一个错误,因此您希望抛出未经检查的异常。使用Java语言已提供的未经检查的异常之一,例如IllegalArgumentException
,IllegalStateException
或NullPointerException
。
您可能还想摆脱二传手。您已经提供了通过构造函数启动age
的方法。实例化后是否需要更新?如果没有,请跳过设置器。一个好的规则,不要让事情比必要的更公开。从私有或默认开始,并使用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
答案 5 :(得分:4)
抛出异常是不好的做法,因为这需要任何调用构造函数的人捕获异常,这是一种不好的做法。
最好让一个构造函数(或任何方法)抛出一个异常,一般来说是IllegalArgumentException,它是未经检查的,因此编译器不会强迫你捕获它。
如果希望调用者捕获它,则应抛出已检查的异常(从Exception扩展的内容,但不是RuntimeException)。
答案 6 :(得分:4)
我从未认为在构造函数中抛出异常是一种不好的做法。在设计类时,您应该了解该类的结构应该是什么。如果其他人有不同的想法并尝试执行该想法,那么您应该相应地出错,向用户提供有关错误的反馈。在您的情况下,您可能会考虑类似
的内容if (age < 0) throw new NegativeAgeException("The person you attempted " +
"to construct must be given a positive age.");
其中NegativeAgeException
是您自己构建的异常类,可能会扩展另一个异常,例如IndexOutOfBoundsException
或类似的东西。
由于您并未尝试发现代码中的错误,因此断言似乎不是最佳选择。我会说终止一个例外绝对是正确的做法。
答案 7 :(得分:1)
我不是在构造函数中抛出异常,因为我认为这是非干净的。我的意见有几个原因。
正如Richard所说,您无法以简单的方式初始化实例。特别是在测试中,在初始化期间仅通过将其包围在try-catch中来构建测试范围的对象真的很烦人。
构造函数应该没有逻辑。完全没有理由将逻辑封装在构造函数中,因为您始终致力于分离关注点和单一责任原则。由于构造函数的关注点是“构造一个对象”,因此如果遵循这种方法,它不应该封装任何异常处理。
闻起来很糟糕。 Imho如果我被迫在构造函数中进行异常处理,我首先会问自己,我班上是否有任何设计欺诈行为。有时候这是必要的,但后来我把它外包给了一个构建器或工厂,以使构造函数尽可能简单。
因此,如果有必要在构造函数中进行一些异常处理,为什么不将这个逻辑外包给Builder of Factory?它可能是更多的代码行,但是你可以自由地实现更强大和更适合的异常处理,因为你可以将异常处理的逻辑外包得更多,而不是坚持构造函数,它将封装太多逻辑。如果您正确委派异常处理,客户端无需了解构造逻辑。