NullPointerException 与 方法参数检查


下面是翻看资料+个人理解,不对请吐槽。


>> 困扰我的null检查

我记得当时Java用了一段时间以后,一个困扰我的问题就是: 到处都需要null检查 if( xxx != null ) ...
我的担心是: 我忘了/不确定/不能控制这里的 xxx 是什么,那如果它是 null 怎么办?如果我这里不做检查,那可能会引发 NullPointerException 的。
我讨厌Exception,我要避免发生那种情况,所以我尽量“防御性的”检查所有的情况。
尽管很多时候我不知道检查失败(else)的情况应该怎么处理。
通常我不知道怎么处理的时候,我就不写else,所以事实上是: 如果检查失败,什么都不做。
有时一个方法必须返回一个东西,我那时是这样处理的: 如果需要返回的是一个对象,那么返回null,如果是一个数字,返回-1……
我这样写代码的时候在心里告诉自己:
“你传给我一个null,所以我的返回值也是null,返回null表示‘这次调用有问题’,你的输入有问题,这不是我的责任。”
“如果我返回-1,也表示‘这次调用有问题,方法没有被正确执行’”
我很快发现,因为这些方法理论上“有可能”会返回null或-1,我在调用它们的时候也不得不加上对null或-1的检查,事情很快向这个方向发展:
“对这个方法的返回值进行检查,如果是-1,那说明什么,让我看一下当时的代码……恩,那说明我传进去的三个参数中有一个是null,让我看一下,
其中一个刚刚赋值的我确定不是null,另外两个分别是另外两个方法调用的返回值,让我看一下代码,那两个方法什么时候可能会返回null…………”
我那时没有意识到,我实际上在人工“跟踪”一个本来应该是“exception”的东西,并且把各种处理“exception”的不同逻辑散落得到处都是。

因为那时老师告诉我们: 健壮的程序应该能够处理各种“边缘情况”,不轻易“崩溃”。
事实证明那时候我对这句话的理解是错误的。
程序应该妥善处理的“边缘情况”应该是用户输入的“边缘情况”,而不应该去贸然“处理”程序本身逻辑有问题而造成传入某方法的变量“不是预期的范围”这种情况。


>> 方法的协议

把一个方法看做一个黑盒子,一个黑盒子接受某些输入,处理,然后返回某些输出。输入、输出有时为空(void)。
每一个黑盒子都有它的“协议”。
协议是一个黑盒子的说明书,它应该包括下面这些内容:

- 它的功能、返回值
(这个很显然,大家都不会漏掉,但是很多人不会记得在注释里清清楚楚写明所有的返回情况:当xxx时返回ooo,当……)
- 它对输入的要求
(这里是你需要保护你的方法的地方,“闲人免进”的牌子应该立在这里,而不是在内部某个地方很不厌其烦的检查“如果进来的这个是闲人,那先站在这边”)
- 调用什么时候会失败,失败时发生什么

当一个方法是对象方法的时候,这个说明书还要更复杂一点:当对象处在什么状态的时候,才能调用这个方法;当方法的执行成功返回或失败以后,对象又处在什么状态。

简单总结为,所有可能情况的:

- pre condition
- logic
- post condition



>> 方法体开头的检查

一个方法,当它在说明书里已经清清楚楚写明了它对调用时机和输入参数的要求时,在它的方法体内开头的部分就可以做检查了。
方法体常常被我分成三个很明确的部分:


  /**
   * Calculate duration time ...
   *
   * @param from    the start time, must be greater than 0, otherwise the
   *                method may throw a runtime exception.
   * @param to      the end time, must be greater than from, otherwise the
   *                method may throw a runtime exception.
   * @return the duration time amount between from and to
   */
  public static long calculateDuration(long from, long to) {

    // check input
    if( from < 0 )
      throw new IllegalArgumentException("Invalid start time: " + from);
      
    if( to < from )
      throw new IllegalArgumentException(
              "Invalid time interval: " + from + " ~ " + to);

    // logic
    long result = to - from;

    // return statement
    return result;
  }



说明书中的“输入要求”部分就是你规定的调用者必须遵守的“协议”。
当输入的参数不符合协议规定的情况的时候,不要客气,抛出Exception。
如果你看到一个null参数,请throw exception,————看到鬼请尖叫。
不合法的参数传进来,表示程序其他某些地方可能有问题,看到问题而不报告,属于窝藏。窝藏的多了,最后问题就很难找是在什么地方发生的。
抛出的Exception要尽可能“友好”(执法要严格,态度要友好),详细标明哪个参数有问题,有什么问题。调用者在调试的时候会看到异常信息,然后立刻知道什么地方出了问题。
注意在做参数检查的时候,尽量单个检查,而不要用一个if语句检查所有的情况: if( start < 0 || current < start ) throw ...
为什么?
当你追踪Exception信息的时候你就知道。

这里的例子是静态方法,如果是对象方法的话,你也可能在第一部分检查当前对象的状态;如果你的方法对调用线程也有严格要求,你还可能还会检查当前的线程,对错误线程的调用抛出Exception等。
总之你要检查的就是所有的“pre condition”是不是符合要求。

有的时候你不做检查,运行到某个地方调用参数对象的方法时,也会自动抛出 NullPointerException, ArrayIndexOutOfBoundsException 等等,这是不是说,让它自己抛出就好了,不用在开头做检查了?
取决于不同的情况,但最好在开头自己检查,因为到程序自己抛出 runtime exception 的时候,你的方法可能已经运行了几句,可能已经改变了自身对象的状态,也可能已经改变了某些参数对象的状态,
也可能改变了某些静态环境的状态,这个时候方法再fail,可能已经导致某些调用前还处在稳定(合法)状态的数据变成了不合法的状态。
所以最佳实践是,检查尽量放在方法体的开头,当你的方法还没做任何事的时候,养成良好的编码习惯。



>> 要明雷,不要暗雷

前面说前提不对时不要客气,抛出Exception。
但有的时候调用者在调用你的方法以前,不知道前提对不对,——这时你得提前告诉他/她。
不要让人家来调用你的方法,catch exception 然后才知道参数不对,这就会导致另外一个问题: 强迫调用者用 try-catch 来做流控制。

不合法的参数应该清清楚楚写在注释里,除此之外,如果需要,提供一个帮助调用者做前提检查的方法。

比如一个 Iterator ,如果你直接调用 next(),它也许会返回给你下一个元素,也许会抛出一个 IndexOutOfBoundsException 或者 NoSuchElementException。
那么调用者在调用以前怎么知道呢? ——所以接口的设计者提供了一个检查的方法: hasNext()

你为调用者提供的检查前提的方法,常常被放在 if, for, while 的 condition 部分。
这句话可以换成这样说: 只有当你为调用者提供了检查前提的方法,调用者才有东西可以放在 if, for, while 的 condition 部分。



>> assert 与 方法可见性

前面说方法开头要做前提检查,那当我很明确知道参数不会有问题的时候,也要傻傻的去检查吗?

—— 你怎么知道参数100%没问题?
—— 方法只有我自己调用。
—— 你怎么知道别人不会去调用?
—— 方法是 private 的。

抛开反射的问题不说,从设计上来说,private的方法,甚至包括默认的 package private 的方法,其调用者一般可以预知是只有自己/团队成员的时候,参数可以不做检查。
这里的想法是: 在项目完成、交付使用以前,要确保内部调用的所有方法参数正确,然后在交付使用的运行时,内部调用不做检查。
把一个项目、API看做一个大的黑盒子,该盒子只对外部调用做检查,而(在充分测试的基础上)内部省略检查。
其潜台词是这样的: 项目/API/模块经过内部测试,保证只要外部调用都通过检查,内部就不会有问题。

这时,Java提供了assert机制: 在项目开发过程中,用assert做内部调用的参数检查,写起来省事,并且在项目完成以后很容易把这些检查关闭。

怎样区分哪些方法总是被内部调用,而哪些方法可能被外部调用?——用可见性来区分呀。
这里我一般把 public 和 protected 方法视为可以被外部调用的方法(即一个API/模块的对外接口),而把默认的和private的方法视为只会被内部调用的方法。
事实上对可见性的划分,Java DOC 注释是很好的依据: 公共可见的部分必须提供 Java DOC 注释。
在不考虑序列化的情况下,Java DOC的规定是所有用 public 或者 protected 修饰的东西都要提供 Java DOC 注释。
(你什么时候在Java API文档里见到过 默认的 或者 private 的 类、方法 或 成员?)

所以总结为: 所有需要提供 Java DOC 注释的方法,用if语句做前提检查,其他的方法可以用 assert 来检查。



>> 对 if( "abc".equals(str) ) 这种写法的吐槽

根据 Object 类中对 equals 方法协议的规定,这种写法相当于 if( str != null && str.equals("abc") )

它的糟糕之处在于:

1 - 它依赖于 Object 对equals方法的协议规定,而没有明确写出 str != null,读到这个代码的人需要知道,或者去查 equals 方法的协议,才能完全清楚其逻辑
2 - 它把两个condition合在同一个if检查内,而否决了这种可能: if( str != null ){ if( str.equals("abc") ) {...} else {...} } else { ... }
3 - 新手养成这样写的习惯以后常常会省略else,即习惯性的对“参数不符合条件”的情况不做处理,而又没有明确意识到这一点。

这种写法问题的根源是没有对“合法参数”与“不合法参数”怎样处理有一个明确清晰的认识,对可能存在的错误的职责边界没有明确划分,擅自包容程序其他地方可能存在的问题。

对于null,前面提到的方法说明书中对返回值的规定还应该明确包括: null 是不是合法的返回值,方法有没有可能在任何时候返回 null 值。

通常,返回null都是很糟糕的决定。

如果结果是一个空的集he,最好就返回一个空的集he: Collections.emptyList() , Collections.emptyMap() 等等,不然你觉得为什么Java的API会提供这些方法给你呢?
如果结果是一个空的数组,就返回一个空的数组好了: return new String[0];

这样调用者拿到返回值可以直接loop,而不必担心null的问题。

还有一种情况是用null来表示某种“特殊返回值”,其实这时用一个命名过的常量来表示不是更好吗:

XXXType value = someMethod();
if( value == XXXType.SPECIAL_VALUE_NAME ) {...}

不是比

if( value == null ) {...}

的可读性好得多吗?

所以很多时候null检查其实是可以避免的,而另外一些时候null检查又是必须明确的。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值