What is null?

794 阅读8分钟

Y说

按照惯例还是在文章开头随便聊聊。之前这个环节是借鉴的why哥,叫“荒腔走板”。现在决定还是换一个有自己特色的名字,冥思苦想,最终拍板“Y说”。

有一段时间没在公众号更新文章了,其实也不是忙,就是有点懒(主要原因),再加上没有太多灵感,所以,很抱歉~

过完年后开始复工了,该打工要打工,该更新还是要继续更新的。新年要有新气象,要努力挣钱,持续成长,也要坚持做自己想做的事。我会尽自己所能产出一些对大家有用的文章~

what is null?

为什么会想到这个话题呢?因为前段时间(大概是年前了),看到技术群里的小伙伴聊到这个话题。

想了一下,作为一个Java程序员,我在代码中经常与null打交道,时不时就会蹦出一个“空指针”异常,可以说是很熟悉了。所以在写代码的时候,也要尽量考虑到各种产生null的场景,也积累了一些小技巧。

但自己确实对null这个东西没有深究,包括群里的问题,我当时一时间也没想清楚,所以我下来偷偷地google了一把:

搞错了,应该是这个才对:

十年前的老问题了,上来就是一个夺命四连问:

  • 什么是null?
  • null是任何类型的实例吗?
  • null属于什么集合?
  • null在内存的什么地方?

我也认真读了下面的回复,总结如下:

在Java中,null不是任何类型的实例。你对任意的类型R,下列语句都会返回false

null instanceof R

因为null是代表“不存在”,它是一个特殊的值,所以它不包含任何类型的信息,在内存中的一个特殊的位置。所以下面两个都会输出true

System.out.println(null == null);
Animal animal = null;
List list = null;
System.out.println(animal == list);

那null到底在什么位置呢?上面那个stack overflow问题有个答主说到:

That is implementation specific, and you won't be able to see the representation of null in a pure Java program. (But null is represented as a zero machine address / pointer in most if not all Java implementations.)

意思是这个跟Java语言无关,是由Java实现自己去决定的,但大多数实现都是把它放在了起始位置。

顺便介绍一个小tips,每个对象都有一个identityHashCode,它是用这个对象在内存中的地址返回的hash值。

我们使用System类的一个native静态方法可以打印出null的identityHashCode,发现输出是0:

System.out.println(System.identityHashCode(null));

null有害吗?

null是有害的吗?这是一个有争议的问题,我只搬运一下stack overflow上的回答。

对于编程来说,null是弊大于利的。包括null的发明者C.A.R Hoare自己都承认,当时用null只是因为它比较好实现。却没想到null带来了无尽的错误、bug、系统崩溃。原话在这:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

但抛开程序语言不谈,我们回到面向对象的世界里面,null又是客观存在的。比如你要找一个符合xx条件的人,可这样的人就是不存在,总不能凭空捏造一个吧?

于是映射到程序语言上,我们又要解决这个问题。一般对待null有三种方式:

  1. 直接返回null
  2. 抛自定义异常
  3. 包装类

第一种是最简单粗暴的,也是我们代码中用得最多的,如果没有找到就直接返回null。这样做的弊端是在客户端需要手动检查它是否为null,不然就很容易造成不可预料的空指针异常。

这里有一个小技巧,我们可以在变量、方法参数、方法(返回值)上加注解标识它是否可能为空。这样IDE或者findBug检查就可以帮忙检查出来程序是否有潜在的空指针bug。jetbrains提供了两个注解:@Nullable@NotNull,可以配合IDEA做检查。而Spring也提供了两个注解@Nullable@NonNull。这里更推荐用Spring的,它的支持范围更大点,不受限于IDEA。Lombok好像也有相应的注解,这里不赘述。

第二种抛异常,主要用于业务上觉得不可能为空的场景。比如按某种条件去查询数据,从业务上来讲,不可能为空,那如果没有查到数据,就可以直接抛出一个异常,交给上层去catch或者往上抛。比如UserNotFoundException

第三种解决方案是使用包装类,这也是Java 8提供的Optional类的作用了。使用Optional类可以有效防止空指针异常,但在程序里面也要写相应的判断逻辑或处理逻辑,可能会增加程序的复杂性。比如ifPresent等。

但综合来看,还是更推荐第二三种解决方案。因为第一种依赖IDEA或者findBugs,不是强一致的,但是有发生空指针异常的可能性。

一些避免空指针异常的小技巧

初始化集合

在Java语言里,如果不给一个字段初始化赋值,它会被赋值为一个默认值。而除了原始类型以外,所有类型的默认值都是null,集合(List、Map、Set等)也不例外。

而在使用集合的时候,可能会忘记对它进行空指针校验,所以就很容易抛出空指针异常。

特别是在POJO类里面,使用集合是非常常见的。这个时候我们可以在声明属性的时候,给它一个初始化的值:

List<String> userIds = new ArrayList();

使用工具类

合理使用一些工具类,可以防止空指针异常的同时,还能让我们的代码看起来更优雅。

比如比较两个对象是否相等,原始的写法是调用一个对象的equals方法,但如果这个对象本身是空,就容易发生空指针异常:

// 如果a是null,下列代码会发生空指针异常
a.equals(b)

而如果改成这样就可以有效避免这个问题了:

Objects.equals(a, b)

再比如我们可能会经常判断一个集合是不是空,可能会这么写:

// 如果list为null,下列代码会抛出空指针异常
list.size() == 0;
list.isEmpty();

一些框架也提供了相应的工具类来解决这个问题,我最常用的就是apache commons的工具类CollectionUtils

CollectionUtils.isEmpty(list)

提前assert

提前assert是一种短路方案。在代码的最前面就严查对象是否为空,可以防止执行到一半抛异常,脏数据或程序的复杂性。

Java自带了assert关键字,但功能比较简单,且默认是不开启的,需要JVM参数-enableassertions或者-ea打开断言。

如果你的项目是依赖Spring的话,这里更推荐Spring提供的Assert类,功能强大,用起来也比较方便。

Assert.notNull(oil, "oil mustn't be null");
Assert.isTrue(speed > 0, "speed must be positive");

要有一颗防null的心

不要信任任何第三方接口,哪怕这个接口是你旁边的哥们写的,甚至是你自己写的。

调用其它服务的API,一定要记得对返回值做相应的空检查,这样才能让自己的程序更健壮。

在写单元测试的时候,也尽量多考虑到各种空指针的情况,防范于未然。

kotlin是如何解决的?

kotlin作为一门比较新的语言,吸取了各种语言的精华和教训,它对null做了一些限制和改进。

kotlin从语法上限制,默认不可为null。但它也没有一棍子打死,提供了允许可以为null的语法。

// 声明一个非空变量
var a: String = "hello"
// 声明一个有可能为空的变量
var b: String? = "world"

var name: String? = null
// ?符号,如果变量为null,返回null,否则进行相应操作
var len = name?.length
print(len == null)  //输出:true

// ?: 符号,如果坐标表达式为null,进行右边的操作
var a: String? = "hello"
var b = a?.length ?: 100  //很明显左边不为null
println(b)  //输出: 5

个人觉得kotlin这种算的一种比较好的解决方案了。默认不可为null,可以有效的消除掉很多由空指针带来的隐患。但又提供语法可以支持null,通过短路返回、语法分析等手段来防止意外产生的空指针异常。

关于null就介绍到这里了,感谢大家阅读~

求个支持

我是Yasin,一个坚持技术原创的博主,我的微信公众号是:编了个程

都看到这儿了,如果觉得我的文章写的还行,不妨支持一下。

文章会首发到公众号,阅读体验最佳,欢迎大家关注。

你的每一个转发、关注、点赞、评论都是对我最大的支持!

还有学习资源、和一线互联网公司内推哦