让构造函数抛出异常是好的做法吗?[复制]

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

提问人:ako 提问时间:5/22/2011 最后编辑:Mitjaako 更新时间:5/30/2023 访问量:199776

问:

让构造函数抛出异常是一种好的做法吗? 例如,我有一个类,我将其作为其唯一属性。现在 我提供的类是Personage

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 Exception 构造函数

评论

29赞 Codemwnci 5/22/2011
对我来说看起来不错,但你的代码重复是不好的做法。只需从构造函数中调用 setAge 即可减少大量重复代码
24赞 Mat 5/22/2011
在这种情况下抛出 IllegalArgumentException 可能是个好主意,使其非常明确。
7赞 user541686 5/22/2011
@Codemwnci:如果是虚拟的,这不是一个好主意(就像这里的情况一样)。setAge
0赞 Asif Mushtaq 3/22/2016
你能解释@Mehrdad吗?为什么虚拟方法不应该在构造函数中调用?
2赞 user541686 3/22/2016
@UnKnown:这里解释一下。

答:

4赞 TofuBeer 5/22/2011 #1

抛出 Exception 是一种不好的做法,因为这需要调用构造函数的任何人捕获 Exception,这是一种不好的做法。

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

如果希望调用方捕获异常,则应引发选中的异常(从 Exception 扩展的内容,但不是 RuntimeException)。

评论

1赞 Robin Green 5/22/2011
编译器“强迫你捕捉东西”的想法具有误导性。通常,你可以声明你的方法来抛出相同的异常 - 直接回到方法 - 这意味着你不会被迫捕获东西。编译器实际上迫使您捕获内容的唯一情况是,当您在 throws 子句中重写未声明该异常或其超类的方法时。main
2赞 TofuBeer 5/23/2011
对不起,应该写“被迫处理”,这意味着抓住或添加一个抛出子句。
217赞 Stephen C 5/22/2011 #2

在构造函数中抛出异常并不是坏的做法。事实上,这是构造函数指示存在问题的唯一合理方法;例如,参数无效。

我还认为抛出选中的异常可以 1,假设选中的异常是1) 声明的,2) 特定于您报告的问题,以及 3) 期望调用者处理此2 的选中异常是合理的。

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

应选择与已发生的异常情况匹配的异常类。如果抛出,调用方很难将此异常与任意数量的其他可能声明和未声明的异常分开。这使得错误恢复变得困难,如果调用方选择传播异常,问题就会蔓延。Exception


1 - 有些人可能不同意,但 IMO 这种情况与在方法中抛出异常的情况之间没有实质性区别。标准检查与未检查的建议同样适用于这两种情况。
2 - 例如,如果尝试打开不存在的文件,则现有的 FileInputStream 构造函数将引发 FileNotFoundException。假设 FileNotFoundException 是经过检查的异常3 是合理的,则构造函数是引发该异常的最合适位置。如果我们在第一次(比如)进行读取写入调用时抛出 FileNotFoundException,则可能会使应用程序逻辑更加复杂。
3 - 鉴于这是已检查异常的激励示例之一,如果您不接受这一点,您基本上是在说所有异常都应该取消选中。这是不切实际的......如果您要使用 Java。


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

第二个问题是,如果断言失败,则会被抛出。公认的智慧是,试图捕捉及其任何亚型都是一个坏主意。但无论如何,你仍然抛出一个例外,尽管是间接的。assertAssertionErrorError

7赞 ashays 5/22/2011 #3

我从不认为在构造函数中抛出异常是一种不好的做法。在设计类时,你对该类的结构应该是什么有一定的想法。如果其他人有不同的想法并试图执行该想法,那么您应该相应地出错,并向用户提供有关错误是什么的反馈。就您而言,您可以考虑类似的事情

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

其中是您自己构造的异常类,可能会扩展另一个异常,例如或类似的东西。NegativeAgeExceptionIndexOutOfBoundsException

断言似乎也不是要走的路,因为你不是在试图发现代码中的错误。我想说的是,在这里以例外终止绝对是正确的做法。

8赞 Bill K 5/22/2011 #4

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

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

此外,断言对于调用方来说更难捕获,这很容易。

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

16赞 Spam Suppper 5/7/2012 #5

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

您可能还想摆脱二传手。您已经提供了一种通过构造函数启动的方法。实例化后是否需要更新?如果没有,请跳过二传手。一个好的规则,不要让事情过于公开。从 private 或 default 开始,并使用 保护您的数据。现在每个人都知道它已经正确地构建了,并且是不可变的。它可以放心使用。agefinalPerson

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

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
35赞 Hazok 1/8/2015 #6

正如此处的另一个答案中提到的,在 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
            ...
        }
    }

评论

3赞 Ajoy Bhatia 3/5/2015
我想知道是否有人意识到这个微妙点的重要性。在决定从构造函数引发异常时,需要注意这一点。
3赞 Stephen C 1/6/2017
正如我对我的回答所评论的那样,大多数 Java 代码不需要处理这种“攻击”。仅当有可能在安全敏感上下文中运行不受信任的代码时,它才有意义。
2赞 Mike 3/17/2017
这只是在非常有限的情况下才有问题,其中部分初始化的类可能是一个安全问题。我不认为有人建议将这一建议应用于所有班级。
3赞 Mike 3/17/2017
构造后验证允许存在尚未验证的对象,因此可以以无效状态存在。在大多数情况下,我认为这是一个更大的问题。我不确定为什么从构造函数抛出异常不能与 Spring 很好地集成。
1赞 Hazok 3/17/2017
@Mike请不要在我的评论中阅读更多内容。我从来没有说过从收缩器抛出异常不能很好地与 Spring 集成。Spring 提供了构造后验证的功能,遵循这样的模式可以减少从构造函数抛出异常的需要。
42赞 Richard 2/5/2015 #7

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

这样做的原因是你不能这样做:

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();
}

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

评论

6赞 Stephen 7/22/2015
你并不总是一个类的生产者和消费者。也就是说,其他人可能在未检查的情况下使用您的类。你可以很容易地争辩说他们未能满足你使用该类和GIGO的先决条件,但OP询问了这种做法的好坏。我认为,让你的类更易于使用、更可靠是很好的做法,而你接受的输入是自由的,有助于实现这一目标。
5赞 Richard 7/24/2015
我认为这归结为关于已检查和未检查异常的更大辩论。检查的例外经常被误用为一种响应,而不是特殊情况的指示。没有什么比构造函数中的错误更特殊的了。我认为如果绝对必要,抛出运行时异常比试图向用户指示发生了某些可预见的事件要好。如果可以预见,那也不例外,不是吗?通常,我发现您可以使用 IllegalStateException、NullPointerException 或 IllegalArgumentException。
0赞 Stephen 7/27/2015
我的印象是 OP 一般都在询问例外情况。我同意你的观点,即大多数情况都是由你提到的 3 个特定例外情况处理的。
0赞 Richard 7/27/2015
我认为他的例子显示了已检查的异常,当然我的答案是关于已检查的异常。
2赞 Artem A 12/23/2020
这是一个很好的例子,说明如何不这样做。构造函数是唯一一个应该实现所有检查的地方。没有某些属性,实体就无法存在。如果实体可能具有 null 值 - 它是不同的业务实体或 DTO 对象(另一个故事)。应始终从构造函数引发异常,以限制在运行时创建具有意外 NullReferenceExceptions 的无效参数。
5赞 Vegaaaa 11/15/2017 #8

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

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

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

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

因此,如果有必要在构造函数中进行一些异常处理,为什么不将此逻辑外包给 Builder of Factory?它可能多了几行代码,但让你可以自由地实现更可靠和更适合的异常处理,因为你可以更多地外包异常处理的逻辑,而不是坚持到构造函数,这将封装太多的逻辑。如果您正确地委托异常处理,则客户端不需要了解您的构造逻辑的任何信息。

评论

1赞 Tiago Stapenhorst 10/12/2022
嗯,这只有在你使用干净代码架构和另一种领域驱动设计方法时才是正确的。需要注意的是,并非所有软件都应该遵循 Clean Code/DDD 原则,作者也没有提到使用它。