Java构造函数风格:检查参数不为空

82

如果您有一个接受某些参数但不允许它们为null的类,那么最佳实践是什么?

以下内容很明显,但异常信息有点不具体:

public class SomeClass
{
     public SomeClass(Object one, Object two)
     {
        if (one == null || two == null)
        {
            throw new IllegalArgumentException("Parameters can't be null");
        }
        //...
     }
}

这里的异常可以让你知道哪个参数是空的,但构造函数现在看起来相当丑陋:

public class SomeClass
{
     public SomeClass(Object one, Object two)
     {
        if (one == null)
        {
            throw new IllegalArgumentException("one can't be null");
        }           
        if (two == null)
        {
            throw new IllegalArgumentException("two can't be null");
        }
        //...
  }

这里构造函数更加简洁,但是现在构造函数的代码并不真正位于构造函数内部:

public class SomeClass
{
     public SomeClass(Object one, Object two)
     {
        setOne(one);
        setTwo(two);
     }


     public void setOne(Object one)
     {
        if (one == null)
        {
            throw new IllegalArgumentException("one can't be null");
        }           
        //...
     }

     public void setTwo(Object two)
     {
        if (two == null)
        {
            throw new IllegalArgumentException("two can't be null");
        }
        //...
     }
  }

这些样式中哪一个是最好的?

或者是否有更广泛接受的替代方案?


4
我建议选择第二个。它看起来丑陋并不代表它不正确。记住,代码是供人类阅读和理解,而不是机器。 - Woot4Moo
6
第二种和第三种方法的行为差异非常大,以至于无法合理地回答这个问题。第二种方法允许后续使用setters将值设置为“null”。如果你想要一致的行为,那么你应该选择第三种方法,这已经不是一个风格问题了。 - BalusC
@BalusC 假设2和3都有setter方法。如果2没有任何setter方法,那么它基本上与3相同;除了用户可以在对象创建后设置对象。 - CodyEngel
10个回答

101
第二个或第三个。因为它可以告诉API用户出了什么问题。为了更简洁,可以使用commons-lang的Validate.notNull(obj, message)。这样你的构造函数就会变成这样:
public SomeClass(Object one, Object two) {
    Validate.notNull(one, "one can't be null");
    Validate.notNull(two, "two can't be null");
    ...
}

将检查放在setter中也是可以的,需要加上同样冗长的注释。如果你的setter还有维护对象一致性的作用,你也可以选择第三种方式。


7
为什么将其放置在Setter中备受争议?我认为恰恰相反。如果构造函数检查并防止 null 值,那么如果Setter接受它,我会觉得这是一个错误。 - Joachim Sauer
@Joachim Sauer - 同意,我刚刚删除了这部分 - Bozho
9
在这个例子中,设置器(setters)不是final的,这可能会导致子类违反限制条件。对于构造函数中的所有调用,这些方法应该是final或private的。 - Yishai
5
讨厌Setter。全力支持不可变对象。 - RAY
2
现在你可以使用Guava的惊人库来完成这个。查找“checkArgument()”。请查看此链接以比较两者之间的差异:http://piotrjagielski.com/blog/google-guava-vs-apache-commons-for-argument-validation/(编辑:刚刚意识到讨论中提到了guava,但没有提供提供一个很好的比较链接)。 - DPM

71

Java 7加入了java.util.Objects.requireNonNull()函数到所有人都可以使用的API中。因此,检查所有参数是否为null可以简化为一个短列表,例如:

this.arg1 = Objects.requireNonNull(arg1, "arg1 must not be null");
this.arg2 = Objects.requireNonNull(arg2, "arg2 must not be null");

注意事项:

  • 确保不要颠倒两个参数 - 第二个参数是将用于NPE的消息,如果第一个参数为空,则会抛出NPE(如果您将它们颠倒,那么您的检查将永远不会失败)
  • 另一个最佳实践:如果可能,请将所有类成员都设为final(这样你就能确保:当某个对象成功创建时,其所有成员都不会为 null;并且它们不会随时间而改变)

9
现在应该接受这个答案。不需要第三方库,只需使用更新的JDK即可...... - Lonzak

41

你可以使用许多旨在简化前置条件检查的库之一。Google Guava中的许多代码使用com.google.common.base.Preconditions

Simple static methods to be called at the start of your own methods to verify correct arguments and state. This allows constructs such as

 if (count <= 0) {
   throw new IllegalArgumentException("must be positive: " + count);
 }

to be replaced with the more compact

 checkArgument(count > 0, "must be positive: %s", count);

它有一个checkNotNull函数,这个函数在Guava中被广泛使用。你可以这样写:

 import static com.google.common.base.Preconditions.checkNotNull;
 //...

 public SomeClass(Object one, Object two) {
     this.one = checkNotNull(one);
     this.two = checkNotNull(two, "two can't be null!");
     //...
 }

大多数方法都重载为不带错误消息、带固定错误消息或带有可变参数的模板化错误消息。


关于IllegalArgumentExceptionNullPointerException

尽管您的原始代码在null参数上会抛出IllegalArgumentException,但Guava的Preconditions.checkNotNull会抛出NullPointerException

以下是来自Effective Java 2nd Edition: Item 60: 倾向于使用标准异常的一句话:

可以说,所有错误的方法调用都归结为非法的参数或非法状态,但其他异常通常用于某些特定类型的非法参数和状态。如果调用者在某个禁止使用空值的参数中传递了null,惯例要求抛出NullPointerException而不是IllegalArgumentException

NullPointerException并不仅仅是在访问null引用的成员时才会抛出;当参数为null且这是非法值时,抛出它们是相当标准的。

System.out.println("some string".split(null));
// throws NullPointerException

1
NPE vs IAE是一场圣战。尊重您的引用和整本书,但JavaDoc仍然说相反的话:http://java.sun.com/j2se/1.4.2/docs/api/java/lang/NullPointerException.html和http://java.sun.com/j2se/1.4.2/docs/api/java/lang/IllegalArgumentException.html。 - hudolejev
5
@hudolejev:我认为这并没有明确地说出完全相反的意思。IAE没有提到null,而NPE表示应用程序可以将其用于“null的其他非法使用”。 - polygenelubricants
好的,同意,也许“相反”这个词用得有点过了。 - hudolejev
6
有趣的是,JDK 7中引入的 Object.requireNonNull 也会抛出 NullPointerException,所以看来这场“圣战”已经彻底结束了。 - Patrick M

4

我会创建一个实用方法:

 public static <T> T checkNull(String message, T object) {
     if(object == null) {
       throw new NullPointerException(message);
     }
     return object;
  }

我建议将其返回为对象,这样您可以像这样进行赋值操作:
 public Constructor(Object param) {
     this.param = checkNull("Param not allowed to be null", param);
 }

编辑:关于使用第三方库的建议,特别是Google Preconditions,它比我的代码做得更好。但是,如果这是将库包含在项目中的唯一原因,我会有所犹豫。该方法太简单。


2
我相信 Objects.notNull 是 JDK7 提出的。 - Tom Hawtin - tackline
4
实际上,它实现了Objects.requireNonNull(T obj) 或 Objects.requireNonNull(T obj, String message)。第二个会抛出空指针异常。 - BlueLettuce16

4
除了以上给出的所有有效和合理的答案之外,我认为指出检查null可能不是“良好实践”是有益的。(假设读者除了OP以外可能会将问题视为教条主义)
来自Misko Hevery关于可测试性的博客: 断言还是不断言

3
关于可测试性的有趣观点,与检查构造函数有关。请在您的答案中添加链接内容摘要(以防链接失效)。 - Mike Rylander
对于阅读所提到的博客文章的任何人:也要阅读其中的评论。我认为在评论区中提出了支持DBC和防御性编程方面的有效论点。然后你可以自己做出决定... - packoman
链接似乎已经失效了。我在这里找到了内容:https://testing.googleblog.com/2009/02/to-assert-or-not-to-assert.html - undefined

3
Java中检查前提条件的比较 - Guava vs. Apache Commons vs. Spring Framework vs. Plain Java Asserts。
public static void fooSpringFrameworkAssert(String name, int start, int end) {
        // Preconditions
        Assert.notNull(name, "Name must not be null");
        Assert.isTrue(start < end, "Start (" + start + ") must be smaller than end (" + end + ")");

        // Do something here ...
    }

    public static void fooApacheCommonsValidate(String name, int start, int end) {
        // Preconditions
        Validate.notNull(name, "Name must not be null");
        Validate.isTrue(start < end, "Start (%s) must be smaller than end (%s)", start, end);

        // Do something here ...
    }

    public static void fooGuavaPreconditions(String name, int start, int end) {
        // Preconditions
        Preconditions.checkNotNull(name, "Name must not be null");
        Preconditions.checkArgument(start < end, "Start (%s) must be smaller than end (%s)", start, end);

        // Do something here ...
    }

    public static void fooPlainJavaAsserts(String name, int start, int end) {
        // Preconditions
        assert null != name : "Name must not be null";
        assert start < end : "Start (" + start + ") must be smaller than end (" + end + ")";

        // Do something here ...
    }

这篇文章的概述是:比较在Java中检查先决条件的不同方法。具体内容请参考http://www.sw-engineering-candies.com/blog-1/comparison-of-ways-to-check-preconditions-in-java


请注意,纯Java断言默认情况下是不被检查的。它们必须被特别启用。请参阅http://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html#enable-disable。 - joe776

3

除了抛出未经检查的异常之外,另一种选择是使用assert。否则,我会抛出已检查的异常,以使调用者意识到构造函数不能使用非法值。

你的前两个解决方案之间的区别是什么 - 你是否需要详细的错误消息,你是否需要知道哪个参数失败了,或者只要知道由于非法参数而无法创建实例就足够了?

请注意,第二个和第三个示例无法正确报告两个参数都为null的情况。

顺便说一句 - 我投票支持(1)的变体:

if (one == null || two == null) {
    throw new IllegalArgumentException(
      String.format("Parameters can't be null: one=%s, two=%s", one, two));
}

在这种情况下,空值是程序员的错误,调用者在调用构造函数之前可以检查。因此,我认为它不适合作为已检查异常的候选项。 - Yishai

1

静态分析的注解也很有用,可以替代或者补充运行时检查。

例如,FindBugs 提供了 @NonNull 注解。

public SomeClass( @NonNull Object one, @NonNull Object two) {


很遗憾,它们的效果并不是很好。请参见:use-nullable-and-nonnull-java-annotations-more-effectivly - Mike Rylander
我看了你在问题中提到的观点。如果做得好,静态分析仍然有价值。运行时检查也可以有价值。 - Andy Thomas

0

我猜你在谈论Java中内置的assert。在我看来,使用它并不是一个好主意。因为它可以通过命令行参数打开/关闭。因此有人说只有在私有方法中使用才是可接受的。

我的导师们告诉我不要重复造轮子!他们建议我使用库。它们(可能)被设计和测试得很好。当然,你需要确保选择一个高质量的库。

其他人告诉我,企业人士在某些方面是错误的,你引入了比所需更多的依赖关系 - 对于简单的任务而言。我也能接受这一点...但这是我的最新经验:

首先,我编写了自己的私有方法来检查空参数。这很无聊且冗余。我知道我应该把它放到一个实用类中。但为什么我要一开始就写它呢?当别人已经做过时,我可以节省时间,不必编写单元测试和设计现有的东西。除非你想练习或学习,否则我不建议这样做。

我最近开始使用Google的Guava,发现它和Apache Commons一起使用后,你不仅会用于单个方法,而且会越来越多地发现和使用它们。最终,这将使您的代码更短,更易读,更一致,更易于维护。

顺便说一句:根据您的目标,我建议使用上述库中的2或3个...


0
你可以简单地编写一个方法,该方法接受所有需要验证的构造函数参数。该方法根据哪个参数无效抛出特定消息的异常。 你的构造函数调用此方法,如果通过验证,则初始化值。

1
当然,但这是否比上面列出的任何样式都更优越呢? - user130076
如果您的对象实例永远不应该具有空字段,则这并不是更好的选择。在这种情况下,Bozho的答案很棒,即使用您的第三个解决方案。 但是,如果这些字段可以为空,但不是在实例化时,则真正需要进行检查的是您的构造函数。在这种情况下,最好使用一个单独的方法,仅由构造函数调用,这样更容易维护,并使构造函数代码整洁。 - Marc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接