关于Java:将null传递给方法

关于Java:将null传递给方法

Passing null to a method

我正在阅读优秀的《清洁规范》

一个讨论是关于将空值传递给方法。

1
2
3
4
5
6
7
public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {
        return (p2.x - p1.x) * 1.5;
    }
}
...
calculator.xProjection(null, new Point(12,13));

它代表了不同的处理方式:

1
2
3
4
5
6
7
8
9
10
11
12
public double xProjection(Point p1, Point p2) {
    if (p1 == null || p2 == null) {
        throw new IllegalArgumentException("Invalid argument for xProjection");
    }
    return (p2.x - p1.x) * 1.5;
}

public double xProjection(Point p1, Point p2) {
    assert p1 != null :"p1 should not be null";
    assert p2 != null :"p2 should not be null";
    return (p2.x - p1.x) * 1.5;
}

我更喜欢断言方法,但是我不喜欢断言默认情况下处于关闭状态的事实。

该书最后指出:

In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default.

它实际上并没有涉及如何实施此限制?

无论哪种方式,您中的任何人都有强烈的意见。


一般规则是,如果您的方法不希望使用null参数,则应抛出System.ArgumentNullException。抛出正确的Exception不仅可以保护您免受资源破坏和其他不良影响,而且还可以为代码用户提供指南,从而节省了调试代码的时间。

另请阅读有关防御性编程的文章


也不是立即使用,而是与Spec#的提及有关。有一个建议在Java的未来版本中添加"空安全类型":"增强的空处理-空安全类型"。

根据提案,您的方法将变为

1
2
3
4
5
public class MetricsCalculator {
    public double xProjection(#Point p1, #Point p2) {
        return (p2.x - p1.x) * 1.5;
    }
}

其中#Point是对Point类型的对象的非null引用的类型。


断言的使用和异常的抛出都是有效的方法。任何一种机制都可以用来指示编程错误,而不是运行时错误,如此处的情况。

  • 断言具有性能优势,因为它们通常在生产系统上被禁用。
  • 异常具有安全性的优点,因为始终执行检查。

选择实际上取决于项目的开发实践。整个项目需要确定一个断言策略:如果选择是在所有开发过程中启用断言,那么我会说要使用断言来检查这种无效参数-在生产系统中,由于以下原因而抛出NullPointerException:无论如何,编程错误都不太可能以有意义的方式捕获和处理,因此就像断言一样。

但是实际上,我知道许多开发人员不相信断言会在适当的时候启用,因此选择了抛出NullPointerException的安全性。

当然,如果您不能为代码实施策略(例如,如果您正在创建库,并且这取决于其他开发人员如何运行您的代码),则应该选择对这些代码抛出NullPointerException的安全方法库API的一部分的方法。


It doesn't really go into how you would enforce this restriction?

如果它们传入null,则通过抛出ArgumentExcexception来强制实施。

1
2
3
if (p1 == null || p2 == null) {
    throw new IllegalArgumentException("Invalid argument for xProjection");
}

我更喜欢使用断言。

我有一条规则,我只能在公共方法和受保护方法中使用断言。这是因为我相信调用方法应确保将有效参数传递给私有方法。


Spec#看起来非常有趣!

当类似的东西不可用时,我通常会通过运行时空检查和内部方法的断言来测试非私有方法。我没有在每个方法中显式地编写null检查代码,而是将其委托给具有check null方法的实用程序类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Checks to see if an object is null, and if so
 * generates an IllegalArgumentException with a fitting message.
 *
 * @param o The object to check against null.
 * @param name The name of the object, used to format the exception message
 *
 * @throws IllegalArgumentException if o is null.
 */

public static void checkNull(Object o, String name)
    throws IllegalArgumentException {
   if (null == o)
      throw new IllegalArgumentException(name +" must not be null");
}

public static void checkNull(Object o) throws IllegalArgumentException {
   checkNull(o,"object");
}

// untested:
public static void checkNull(Object... os) throws IllegalArgumentException {
   for(Object o in os) checkNull(o);  
}

然后检查变成:

1
2
3
4
5
6
7
8
9
10
public void someFun(String val1, String val2) throws IllegalArgumentException {
   ExceptionUtilities.checkNull(val1,"val1");
   ExceptionUtilities.checkNull(val2,"val2");

   /** alternatively:
   ExceptionUtilities.checkNull(val1, val2);
   **/


   /** ... **/
}

可以与编辑器宏或代码处理脚本一起添加。
编辑:详细检查也可以通过这种方式添加,但是我认为自动添加一行非常容易。


In most programming languages there is no good way to deal with a null that is passed by a caller accidentally. Because this is the case, the rational approach is to forbid passing null by default.

我发现,到目前为止,JetBrains的@Nullable@NotNull注释方法可用于处理此问题。不幸的是,它是特定于IDE的,但确实干净,功能强大,IMO。

http://www.jetbrains.com/idea/documentation/howto.html

将此(或类似的东西)作为java标准会很好。


由于离题似乎已成为话题,因此Scala为此采取了一种有趣的方法。假定所有类型都不为空,除非您将其明确地包装在Option中以指示其可能为空。所以:

1
2
3
4
5
6
7
8
9
10
11
12
//  allocate null
var name : Option[String]
name = None

//  allocate a value
name = Any["Hello"]

//  print the value if we can
name match {
  Any[x] => print x
  _ => print"Nothing at all"
}

我通常不喜欢任何一种,因为这只是在减慢速度。无论如何,稍后都会引发NullPointerException,这将迅速导致用户发现他们正在将null传递给该方法。我曾经检查过,但是我40%的代码最终还是要检查代码,这时我认为这不值钱。


@克里斯·卡歇尔,我会说完全正确。我要说的唯一一件事是分别检查参数,并让exeption报告参数为空的参数,因为这使跟踪零点的来源变得更加容易。

@wvdschel哇!如果编写代码对您来说太费力,则应考虑使用PostSharp(或Java等效语言,如果可用的话)之类的东西,可以对程序集进行后处理并为您插入参数检查。


稍微偏离主题,但我认为findbug的一个非常有用的功能是能够注释方法的参数,以描述哪些参数不应传递空值。

通过对代码进行静态分析,findbug可以指出使用潜在的空值调用该方法的位置。

这有两个优点:

  • 注释描述了您应如何调用该方法的意图,并帮助了文档
  • FindBugs可以指向该方法的潜在问题调用者,从而使您可以跟踪潜在的错误。
  • 仅在可以访问调用方法的代码时才有用,但通常是这种情况。


    我同意还是不同意wvdschel的帖子,这取决于他的具体说法。

    在这种情况下,请确保此方法将在null上崩溃,因此此处可能不需要显式检查。

    但是,如果该方法仅存储传递的数据,并且稍后有其他方法可以处理该数据,则尽早发现错误的输入是更快修复错误的关键。在此之后的某个时刻,可能会有无数种方式将错误的数据恰巧提供给您的班级。这是在试图弄清事实之后老鼠如何进入您的房子,并试图在某个地方找到洞。


    尽管它不是严格相关的,但您可能要看一下Spec#。

    我认为它仍在开发中(由Microsoft开发),但是可以使用一些CTP,并且看起来很有希望。基本上,它允许您执行以下操作:

    1
    2
    3
    4
      public static int Divide(int x, int y)
        requires y != 0 otherwise ArgumentException;
      {
      }

    要么

    1
    2
    3
    4
    5
    6
      public static int Subtract(int x, int y)
        requires x > y;
        ensures result > y;
      {
        return x - y;
      }

    它还提供了诸如Notnull类型的其他功能。它建立在.NET Framework 2.0之上,并且完全兼容。您可能会看到,语法为C#。


    我认为,在方法开始时立即抛出C#ArgumentException或Java IllegalArgumentException是最清晰的解决方案。

    对于运行时异常,应该始终保持谨慎-方法签名上未声明的异常。由于编译器不会强制您捕获这些错误,因此很容易忘记它们。确保您具有某种"全部捕获"异常处理,以防止软件突然停止。那是用户体验中最重要的部分。


    处理此问题的最佳方法实际上是使用异常。最终,断言最终将给最终用户提供类似的体验,但是在向最终用户显示异常之前,开发人员无法调用您的代码来处理这种情况。 Ultimatley,您想确保尽早测试无效输入(尤其是在面向公众的代码中),并提供调用代码可以捕获的适当异常。


    以Java的方式,假设null来自编程错误(即永远不应该超出测试阶段),然后让系统将其抛出,或者如果有副作用到达该点,请在开始时检查null并抛出IllegalArgumentException或NullPointerException。

    如果null可能来自实际的特殊情况,但是您不想为此使用检查的异常,那么您肯定想在方法开始时使用IllegalArgumentException路由。


    推荐阅读