关于空指针的一点思考

关于一个空指针问题的思考。纯属个人观点,如有雷同,绝对不是抄袭。

观点

Java 空指针很好,它省内存。Java 空指针也很让人恼火,它是 Android 应用 crash 的最主要原因。另一个 crash 的主要原因可能是数组越界。

看着 crash 率上涨,我们恨不得将空指针和数组越界从地球上抹去。Java 不是号称比 C/C++ 更安全吗。但从 crash 的角度来看,Java 似乎并没有更安全啊!

对此我的理解是,Java 的所谓的更安全,其含义在于这空指针和数组越界是程序错误,是程序员必须去修复的程序错误!既然错了,那停止运行就是最正确的策略。而不是像 C/C++ 一样,空指针和数组越界,程序可能是停止运行,也可能是继续莫名奇妙地继续运行。

你分析为什么 C/C++ 程序为什么莫名其妙地运行,耗时耗力,到最后却发现是空指针(野指针)或者数组越界。你会真心觉得 Java 遇到空指针和数组越界后让程序直接停止运行是更为明智的做法!

案例

loadImage() 是我们 Android 项目中加载图片的工具方法。分别是修改前和修改后的代码。

修改前:

1
2
3
4
5
public static void loadImage(Context context, String url, int placeholder, ImageView imageView) {
if (context != null) {
Glide.with(context)....
}
}

修改后:

1
2
3
public static void loadImage(@NonNull Context context, String url, int placeholder, ImageView imageView) {
Glide.with(context)....
}

坏消息的是修改后的代码引起 crash 率明显上涨,好消息是这些 crash 集中在同一处代码。这是否说明修改后的代码不如修改前的呢?单从 crash 率来看,修改后的确不如修改后安全。

修改前的代码有对 context 判空,不会 crash,看似非常安全。我们思考一下它有什么问题呢。

第一,为什么会出现传空 context 呢?

我们知道,遵循良好习惯编写 Android 代码时并不容易出现空 context 的情形。以下是一些最佳实践:

  • 尽量减少 Context 的使用范围
  • 选择合理生命周期的 Context
  • 尽量将 Context 字段定义成 final
  • 尽量不要将 Context 作为 static 字段

当然不能否认 Android SDK 也确实有少量坑,比如在没有跟 Activity 关联的 Fragent 中调用 getContext() 时会返回 null。但对于 Fragment 这一情形,我们应该选择调用 loadImage(fragment) 而不是 loadImage(fragment.getContext())

第二,万一传了空 context 呢?

很显然这时并不会加载图片。不加载图片且没有任何日志,这是最糟糕的情形!这其实跟 C/C++ 中空指针/野指针导致程序莫名其妙运行没有本质的区别。

用户不清楚发生了什么,只会反馈 app 中有个地方看不到图片。开发就惨了,这个问题不好复现,也不容易跟踪

一些思考:

从修改后的代码比修改前的代码 crash 率高这一点来看,我们的代码中有一些地方传入的空的 context。空 context 本身并不可怕。可怕的是,我们并不确定是什么时候以及在哪里传入的。引起的问题轻则导致不加载图片,重则导致各种诡异事件。比起诡异,我更喜欢看似严重但更为直观明了的 crash,因为 crash 揭露了问题的本质!

当然以上说得过于理想,现实是 crash 率也是一个重要质量指标。折衷的做法可能是 loadImage() 对 context 判空并添加一些上报日志的机制,既不 crash 也能找到问题根源。

判空

我们经常有这样的代码:

1
2
3
4
5
6
7
public class Util {
public static void foo(String str) {
if (str != null) {
...
}
}
}
1
2
3
4
5
6
7
8
9
public class Customer {

public void bar() {
String myName = ...
if (myName != null) {
....
}
}
}

这个代码中 foo() 很安全,因为它一定不会 crash。

但它也很丑陋,bar()foo() 两处都有判空。问题的表面现象是 foo() 明显不信任 bar(),担心它传入一个 null。而问题的实质是

  • 你不信任你自己,或者是不信任你的队友
  • 或者是,你跟自己或你的队友没有达到明确、一致的约定

你担心代码中有不确定性。你在 boo()far() 能否处理 null 参数以及谁该处理 null 这些问题上的答案是模糊的。

为些我们不得不背上很重的负担。

  • 每次调用 far() 前是不是要判断是下参数呢?
  • 如果有的地方判断了,有的地方没判断,谁对谁错?

(这就好比你有好几块表,有的是11点,有的是12点,你会糊涂么?)

当然你也可以所有地方加判空,不去进行任何思考,让负担从大脑转移到双手。

Guava 库中有一篇 wiki UsingAndAvoidingNullExplained · google/guava Wiki是专门讨论 Java 中 null 问题的,很值得一看。Guava 中也专门提供 Optional 工具类用于处理 null 问题。

可以做一个非常简单的约定:

  • 公开方法(public),不允许 null 参数
  • 私有方法(private),允许 null 参数