提问人:ako 提问时间:5/22/2011 最后编辑:Mitjaako 更新时间:5/30/2023 访问量:199776
让构造函数抛出异常是好的做法吗?[复制]
Is it good practice to make the constructor throw an exception? [duplicate]
问:
让构造函数抛出异常是一种好的做法吗?
例如,我有一个类,我将其作为其唯一属性。现在
我提供的类是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;
}
}
答:
抛出 Exception 是一种不好的做法,因为这需要调用构造函数的任何人捕获 Exception,这是一种不好的做法。
最好让构造函数(或任何方法)抛出异常,通常说是 IllegalArgumentException,这是未选中的,因此编译器不会强制您捕获它。
如果希望调用方捕获异常,则应引发选中的异常(从 Exception 扩展的内容,但不是 RuntimeException)。
评论
main
在构造函数中抛出异常并不是坏的做法。事实上,这是构造函数指示存在问题的唯一合理方法;例如,参数无效。
我还认为抛出选中的异常可以 1,假设选中的异常是1) 声明的,2) 特定于您报告的问题,以及 3) 期望调用者处理此2 的选中异常是合理的。
然而,明确声明或抛出几乎总是不好的做法。java.lang.Exception
应选择与已发生的异常情况匹配的异常类。如果抛出,调用方很难将此异常与任意数量的其他可能声明和未声明的异常分开。这使得错误恢复变得困难,如果调用方选择传播异常,问题就会蔓延。Exception
1 - 有些人可能不同意,但 IMO 这种情况与在方法中抛出异常的情况之间没有实质性区别。标准检查与未检查的建议同样适用于这两种情况。
2 - 例如,如果尝试打开不存在的文件,则现有的 FileInputStream
构造函数将引发 FileNotFoundException
。假设 FileNotFoundException
是经过检查的异常3 是合理的,则构造函数是引发该异常的最合适位置。如果我们在第一次(比如)进行读取
或写入
调用时抛出 FileNotFoundException
,则可能会使应用程序逻辑更加复杂。
3 - 鉴于这是已检查异常的激励示例之一,如果您不接受这一点,您基本上是在说所有异常都应该取消选中。这是不切实际的......如果您要使用 Java。
有人建议用于检查参数。这样做的问题在于,可以通过 JVM 命令行设置打开和关闭断言检查。使用断言来检查内部不变量是可以的,但是使用它们来实现 javadoc 中指定的参数检查不是一个好主意......因为这意味着您的方法只会在启用断言检查时严格实现规范。assert
assert
第二个问题是,如果断言失败,则会被抛出。公认的智慧是,试图捕捉及其任何亚型都是一个坏主意。但无论如何,你仍然抛出一个例外,尽管是间接的。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 中将其列为“抛出”以及原因,以便调用者不会感到惊讶。
您不需要抛出已检查的异常。这是程序控制范围内的 bug,因此您希望引发未经检查的异常。使用 Java 语言已提供的未经检查的异常之一,例如 或 。IllegalArgumentException
IllegalStateException
NullPointerException
您可能还想摆脱二传手。您已经提供了一种通过构造函数启动的方法。实例化后是否需要更新?如果没有,请跳过二传手。一个好的规则,不要让事情过于公开。从 private 或 default 开始,并使用 保护您的数据。现在每个人都知道它已经正确地构建了,并且是不可变的。它可以放心使用。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
正如此处的另一个答案中提到的,在 Java 安全编码指南的准则 7-3 中,在非最终类的构造函数中抛出异常会打开潜在的攻击媒介:
准则 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; } }
为了与旧版本兼容,一个潜在的解决方案包括 使用初始化的标志。将标志设置为 构造函数,然后再成功返回。所有方法都提供 敏感操作的网关必须先查阅标志 进行:
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 的情况下 构造时,它应该检查其父类加载器是否为 初始 化。
可以访问非最终类的部分初始化实例 通过终结器攻击。攻击者会覆盖受保护的 finalize 方法,并尝试创建该子类的新实例 亚纲。此尝试失败(在上面的示例中, ClassLoader 构造函数中的 SecurityManager 检查抛出安全性 exception),但攻击者只是忽略任何异常并等待 使虚拟机在部分 初始化的对象。发生这种情况时,恶意 finalize 方法 调用实现,使攻击者能够访问此 对要完成的对象的引用。虽然对象只是 部分初始化后,攻击者仍然可以对其调用方法, 从而规避 SecurityManager 检查。当初始化时 标志不会阻止对部分初始化对象的访问,它 会阻止该对象上的方法对 攻击者。
使用初始化的标志虽然安全,但可能很麻烦。只是 确保公共非最终类中的所有字段都包含 Safe 值(如 null),直到对象初始化完成 成功地可以在以下类中表示合理的替代方案 不敏感。
一种更健壮但也更冗长的方法是使用“指向 实现“(或”pimpl“)。类的核心被移入 具有接口类转发方法调用的非公共类。任何 在完全初始化之前尝试使用该类将导致该类 在 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 时,我知道它的参数是什么 那么,为什么要期望我把它包裹在尝试捕获中呢? 啊,你说,但是如果我从动态参数构造一个对象,我不知道它们是否有效。 好吧,你可以......在将参数传递给构造函数之前对其进行验证。这将是很好的做法。 如果您只关心参数是否有效,则可以使用 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();
}
但这种情况多久发生一次呢?
评论
我不赞成在构造函数中抛出异常,因为我认为这是不干净的。我的观点有几个原因。
正如 Richard 所提到的,您无法以简单的方式初始化实例。特别是在测试中,仅通过在初始化期间将测试范围的对象包围在try-catch中来构建测试范围的对象真的很烦人。
构造函数应该是无逻辑的。完全没有理由将逻辑封装在构造函数中,因为您始终以关注点分离和单一责任原则为目标。由于构造函数的关注点是“构造一个对象”,因此如果遵循这种方法,则不应封装任何异常处理。
它闻起来像糟糕的设计。恕我直言,如果我被迫在构造函数中进行异常处理,我首先会问自己我的类中是否有任何设计欺诈。有时这是必要的,但后来我将其外包给建筑商或工厂,以使构造器尽可能简单。
因此,如果有必要在构造函数中进行一些异常处理,为什么不将此逻辑外包给 Builder of Factory?它可能多了几行代码,但让你可以自由地实现更可靠和更适合的异常处理,因为你可以更多地外包异常处理的逻辑,而不是坚持到构造函数,这将封装太多的逻辑。如果您正确地委托异常处理,则客户端不需要了解您的构造逻辑的任何信息。
评论
setAge