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

ako*_*ako 155 java constructor exception

使构造函数抛出异常是一个好习惯吗?例如,我有一个类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;
  }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*n C 160

在构造函数中抛出异常并不是一种不好的做法.实际上,这是构造函数表明存在问题的唯一合理方式; 例如,参数无效.

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

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


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

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


Ric*_*ard 29

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

原因是你不能这样做:

private SomeObject foo = new SomeObject();
Run Code Online (Sandbox Code Playgroud)

相反,你必须这样做:

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

在我构建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");
}
Run Code Online (Sandbox Code Playgroud)

当然,有些情况下抛出一个已检查的异常可能是合理的

public SomeObject() {
    if (todayIsWednesday) throw new YouKnowYouCannotDoThisOnAWednesday();
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为这归结为关于已检查和未经检查的例外的更大辩论.被检查的例外通常被误用作一种反应而不是特殊情况的指示.很少有东西比构造函数中的错误更特殊.我认为如果绝对必要的话抛出一个运行时异常比试图向用户指出发生了一些forseen事件更好.如果它可以是forseen,它不是特例是吗?通常我发现你可以使用IllegalStateException,NullPointerException或IllegalArgumentException. (4认同)
  • 您并不总是一个班级的制作人和消费者.也就是说,其他人可能在没有检查的情况下使用您的课程.您可以轻易地争辩说他们未能满足您使用课程和GIGO的先决条件,但OP询问了练习的好坏.我认为让你的课程更容易使用和更可靠是良好的做法,并且你接受的自由作为输入有助于实现这一目标. (2认同)
  • 这是一个很好的例子,说明了如何不这样做。构造函数是唯一一个应该执行所有检查的地方。没有某些属性,实体就无法存在。如果实体可能具有 null 值 - 它是不同的业务实体或 DTO 对象(另一个故事)。应始终从构造函数中抛出异常,以限制在运行时创建具有意外 NullReferenceException 的无效参数。 (2认同)

Haz*_*zok 24

如此处的另一个答案所述,在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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

为了与旧版本兼容,潜在的解决方案涉及使用初始化标志.在成功返回之前,将标志设置为构造函数中的最后一个操作.提供敏感操作网关的所有方法必须首先查询该标志,然后再继续:

    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"
                );
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

此外,此类的任何安全敏感用法都应检查初始化标志的状态.在ClassLoader构造的情况下,它应检查其父类加载器是否已初始化.

可以通过终结器攻击来访问非最终类的部分初始化实例.攻击者会覆盖子类中受保护的finalize方法,并尝试创建该子类的新实例.此尝试失败(在上面的示例中,SecurityManager检查ClassLoader的构造函数会引发安全性异常),但攻击者只是忽略任何异常并等待虚拟机对部分初始化的对象执行最终化.当发生这种情况时,将调用恶意终结方法实现,使攻击者能够访问此对象,即对正在最终确定的对象的引用.尽管该对象仅部分初始化,但攻击者仍然可以在其上调用方法,从而绕过SecurityManager检查.

使用初始化标志虽然安全,但可能很麻烦.简单地确保公共非final类中的所有字段都包含安全值(例如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
            ...
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 我想知道是否还有其他人意识到这个微妙点的重要性.在决定从构造函数中抛出异常时,需要注意这一点. (3认同)
  • 正如我对我的回答所说,大多数Java代码都不需要处理这种"攻击".仅在安全敏感的上下文中运行不受信任的代码时才有意义. (3认同)
  • 在非常有限的情况下,这只是一个问题,其中部分初始化的类可能是安全问题.我不相信会建议将此建议应用于所有课程. (2认同)
  • 后构造验证允许存在尚未验证的对象,因此可以存在于无效状态.在大多数情况下,我认为这是一个更大的问题.我不确定为什么从构造函数中抛出异常不能很好地与Spring集成. (2认同)

小智 9

您不需要抛出已检查的异常.这是程序控制中的一个错误,因此您希望抛出未经检查的异常.使用Java语言已经提供的未经检查的异常之一,例如IllegalArgumentException,IllegalStateExceptionNullPointerException.

你可能也想摆脱二传手.您已经提供了一种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
Run Code Online (Sandbox Code Playgroud)

  • 我认为这是对OP问题的最精确答案。尽管其他答案是通用的,但可以在SO上找到其他问题。 (2认同)

ash*_*ays 7

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

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

NegativeAgeException您自己构建的异常类在哪里,可能会扩展另一个类似IndexOutOfBoundsException或类似的异常。

断言似乎也不完全是要走的路,因为您不是在尝试发现代码中的错误。我会说以异常终止绝对是在这里做的正确的事情。


Bil*_*l K 7

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

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

此外,断言对于调用者来说更难以陷阱,这很容易.

将其列为方法的javadoc中的"throws".


Veg*_*aaa 5

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

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

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

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

因此,如果需要在构造函数中进行一些异常处理,为什么不将这个逻辑外包给 Factory 的 Builder 呢?它可能会多几行代码,但使您可以自由地实现更强大且更适合的异常处理,因为您可以更多地外包异常处理的逻辑,并且不会拘泥于构造函数,这将封装太多内容逻辑。如果您正确委托异常处理,则客户端不需要了解有关您的构造逻辑的任何信息。


Tof*_*eer 4

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

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

如果您希望调用者捕获它,则应该抛出已检查的异常(从 Exception 扩展的内容,但不是 RuntimeException)。

  • 抱歉,应该写“强制处理”,意思是 catch 或添加 throws 子句。 (2认同)