1. Null 的问题
假设现在有一个需要三个参数的方法。其中第一个参数是必须的,后两个参数是可有可无的。
第一种情况,在我们调用这个方法的时候,我们只能传入两个参数,对第三个参数,我们在上下文里是没有的,那么我们调用方法的时候,就需要用一个特殊值去告知这个方法:
第三个参数我们拿不到,参数是不存在或者不明确的。
这个特殊的值应该用什么呢?在 Java 中,我们会选择用 null 去表示这种情况。
第二种情况,如果在调用方法的时候,我们有三个参数,只是第三个参数没有值,我们也需要传入一个特殊的值去表示:
参数存在,但是没有值。
这个特殊的值是什么呢?没错,在 Java 中,又是 null。
你看到了,现在 null 值的含义本身出现了两个意思:
参数不存在参数没有值二义性在计算机科学里是能避免就尽量避免的。所以,null 值的二义性是一个 Java 中的设计缺陷。不过,也不光是在 Java 语言中,null 的二义性在编程语言里是广泛存在的一个问题。这个问题被称为 Null 引用问题。
Null 引用是计算机科学中一个历史悠久又臭名昭著的问题。在 1964 年,由快排算法的创造者东尼·霍尔发明。他自称这是个十亿美元的错误。
在 Java 中,当我们去调用一个对象值为 null 的方法或者属性时,就会报 java.lang.NullPointerException,简称为 NPE。
传统上,这些 NPE 问题,必须完全依赖程序员本身细致周密的检查,对于 null 的检查充斥在了 Java 代码的字里行间,让代码变得臃肿丑陋,非常恶心。
同时,由于 NPE 的二义性问题,开发人员往往无法完全防护住 NPE,这使得 NPE 成为了开发人员的噩梦。明明逻辑上,一个对象是存在的,只是不知道其明确含义,但是只要引用了这个没有明确含义值的对象的方法,就会被告知NPE,简直让人防不胜防。
并且,更可恶的是,在 Java 中,NPE 是运行期异常,这就意味着 NPE 无法早期发现,只有上线运行了,才可能出现问题。
讨厌的 null,成本巨大的 NPE,让 Java 开发人员在不断地实践中,采用了各种方法去对付 null,让我们看看这些方法。
NPE 是运行期异常,只会在系统运行期间造成,所以导致代码检查无法提前发现它。如果我们能想办法把在运行期出现的 NPE,提前在编译代码时探测到,那么我们就会大大减轻 NPE 对系统造成的损害。
于是,@NonNull 这个注解横空出世了。
2. 横空出世的注解
@NonNull 这个注解就是一个标记,这个标记可以和 IDE 联动:当可能出现 NPE 时,IDE 会标出警告。
我们先看一段代码:
上面的代码没有加入 @NonNull,可以看到 IDE 并没有给出什么警告。
让我们加上 @NonNull 注解看看:
可以看到,Idea 和 @NonNull 注解形成了联动,并给出了可能出现 NPE 的警告。
有了这个警告,其实对一个复杂的项目来说还不够,因为这些警告很容易就会被忽略过去了,即使忽略了,项目依然可以编译运行起来。
那么,我们是不是可以再增加一步检查?当检查到了可疑的 NPE,根本不允许编译通过。是时候给大家介绍一下 findbugs 了!
3. findbugs 出场了
我们先在 maven 中配置好 findbugs:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.github</groupId> <artifactId>leetcodeMaster</artifactId> <version>1.0-SNAPSHOT</version> <build> <resources> <resource> <directory>src/main/resources</directory> <!--扫描resources包下的配置文件--> <filtering>true</filtering> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> </resource> <resource> <directory>src/main/java</directory><!--扫描java包下的配置文件--> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>findbugs-maven-plugin</artifactId> <version>3.0.5</version> <configuration> <!-- 设置分析工作的等级,可以为Min、Default和Max --> <effort>Low</effort> <!-- Low、Medium和High (Low最严格) High只扫描严重错误。建议用Medium--> <threshold>Medium</threshold> <failOnError>true</failOnError> <includeTests>true</includeTests> <!--findbugs需要忽略的错误的配置文件--><!-- <excludeFilterFile>conf/findbugs-exclude-filter.xml</excludeFilterFile>--> <!--findbugs需要忽略的错误的配置文件--> <includeFilterFile>conf/findbugs-include-filter.xml</includeFilterFile> </configuration> <executions> <execution> <id>run-findbugs</id> <!-- 在package(也可设为compile) 阶段触发执行findbugs检查,比如执行 mvn clean package --> <phase>compile</phase> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <depencies> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <depency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </depency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <depency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </depency> <!-- https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 --> <depency> <groupId>com.google.code.findbugs</groupId> <artifactId>jsr305</artifactId> <version>3.0.2</version> </depency> </depencies></project>紧接着运行maven,对项目进行编译。
mvn clean compile findbugs:findbugs
可以看到,findbugs 发现可能会在运行期间出现 NPE 后,中断了项目构建过程。
我们再打开 findbugs 的界面看看具体的报错位置:
你瞧,findbugs 准确的找到了可能出现 NPE 的根源。
通过以上这些手段,我们尽可能的将 NPE 提前到编译期发现。
但是啊但是,对一个规模庞大且复杂的项目来说,光使用静态代码检查还是不够的。因为类似 findbugs 这种的静态代码检查工具,不可能对每个 NPE 的检查点都检查到位。并且,探测的问题有时候因为业务原因,也会放松检查要求。
别慌,我们可以让静态代码检查再加上一些别的方法,来联手堵住 NPE 问题,这就是我们下面要说的 Optional。
4. 用 Optional 去除二义性
由于铺天盖地的 null 检查,使得 Java 程序员叫苦不堪。于是官方自 Java8 起,参考了 google 的 guava,引入了 Optional 类型用来避免每次繁琐丑陋的 null 检查。
Optional 本质上就是一个容器,这个容器持有了一个变量类型为 T 的值。所以,Optional 这个容器中的值只会有两种情况,要么为类型 T 的变量值,要么为null。
对于可能出现的为 null 的情况,Optional 本身从创建、检查,到抽取、使用,都提供了对应的方法供使用者调用。并采用了意义很明确的方法去排除了null的二义性。
我们看示例代码:
class Player{ private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }}public class Optional4NPE { public static void main(String[] args) { Optional<Player> optionalPlayer = Optional.ofNullable(null); optionalPlayer.ifPresent(u -> System.out.println(u.getName())); }}以上代码我们使用了一个 Optional 中的 ofNullable,去创建了一个包含了类型为 Player、值为 null 的 Optional 容器。
运行结果:
'Process finished with exit code 0'
运行后,代码没有任何输出,也没有出现 NPE 异常。没有输出的原因是我们传入了一个 null 值,这个 null 表示值不存在。此时,我们调用 Optional 的 ifPresent 方法做了判断,只有存在值时,才会执行打印输出。
接下来,我们把 null 替换成有意义的值看看。
import java.util.Optional;class Player{ private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }}public class Optional4NPE { public static void main(String[] args) { Player player = new Player(); player.setId(1); player.setName("demoUser"); Optional<Player> optionalPlayer = Optional.ofNullable(player); optionalPlayer.ifPresent(u -> System.out.println(u.getName())); }}输出结果:
demoUserProcess finished with exit code 可以看到,当传入一个我们创建的 player 时,执行了打印输出方法。
上面我们已经发现,通过 Optional 的 ifPresent 方法,我们明确了 null 的含义,明确认定只要值为 null,就表示不存在。那如果一个变量存在,但是没有值或者没有有意义的值呢?
我们把代码改改:
import java.util.Optional;class Player{ private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; }}public class Optional4NPE { public static void main(String[] args) { Player player = null; Player defaultPlayer = new Player(); defaultPlayer.setId(1); defaultPlayer.setName("————undefinedNAME-----"); Player player1 = Optional.ofNullable(player).orElse(defaultPlayer); System.out.println(player1.getName()); }}运行结果如下:
————undefinedNAME-----Process finished with exit code 0这里可以看到,我们使用 orElse 方法,当一个变量值为 null 时,返回一个默认值。通过返回默认值,我们明确了 null 的另外一个含义,对象存在,但是可能没有实际意义。
Optional 的出现,大大改善了我们的 Java 代码质量,减少了 NPE 的可能性,并使得代码的可读性大大增强。
通过使用 Optional,开发人员还能非常自然轻松的使用 Null Object Pattern 模式去处理 Null 问题。Optional 是非常值得在项目中大范围使用的。
5. 总结
最后总结一下。
我们在项目中综合利用 @NonNull 注解,findbugs 静态代码检查,还有引入 Optional 等方式,大大减少了 NPE 出现的场合。
不过,有一说一,这些方法也会加大项目开发复杂度,增大了编译测试时间。
同时,使用好 findbugs 也是有一些门槛的,其本身检测代码有时候严格程度也很难把握。Optional本身也提供了 of 方法,这个方法不小心也会引入新的 NPE 问题。
但是,我认为这些相对于 NPE 可能对线上系统造成的损失而言,都是值得的。我们现在可以说:
NPE,你可以走开点了。