文中所述Guava版本基于29.0-jre
,文中涉及到的代码完整示例请移步Github查看。
null的合理性
对于所有的Javaer来说,null类型是我们在编写代码中不可能不遇到的一个神奇的东西,当然每个人对null类型也有自己的看法和见解,在开始本篇文章之前,让我们看一下其他的一些人是如何看待null的吧。
Java JUC(java.util.concurrent)包的主要开发者Doug Lea的看法是"Null sucks."(令人恶心的null)。
null的发明者Sir C. A. R. Hoare的看法是"I call it my billion-dollar mistake." (null是我的十亿美元的错误),InfoQ的一篇文章也也介绍了null的错误。
粗心的使用null
可能会引起大量惊人的错误。查看Google的code base会发现大约95%的集合不应该包含null
值,当往集合里面放置null
的时候,发生快速失败(fail fast
)而不是安静的接收null
对开发者来说更有用。
此外,null
也是意义不明确的。很难明确的知道返回null
到底表示什么含义。比如Map.get(key)
返回null既可以表示这个key的值是null,也可以表示这个key不在Map中。null可以表示失败,可以表示成功,可以表示任何情况。使用其他的东西代替null会让含义更加明确。
这就是说,当null
适合在一些场景下使用的时候,对于内存空间和存取速度来说,null
使用的代价是低廉的,而且在object
数组中null也是不可避免的。相对于在一些库代码中(libraries code),在应用程序的代码中使用null会导致混乱,困难而且奇怪的bugs和令人不愉快的歧义等等。当Map.get(key)
返回null的时候,可以表示这个key不在map中,也可以表示这个key的值就是null。最关键的是,null不能表明null值的含义。
常见的在HashMap
中是允许key和value为null,但是在ConcurrentHashMap
中,key和value都不允许为null,这样设计的原因就在于Doug Lea对null的厌恶以及null的含义不明确。HashMap
不是线程安全的,所以我们经常在单线程内使用,这样value为null的entry可以认为是我们自己操作造成的,而ConcurrentHashMap
经常在多线程环境使用,当我们获取到一个value为null的entry时,很难知道是另外一个线程删除了这个key还是放入的entry的value就是null值。
由于这些原因,Guava的很多工具集被设计为对null产生fail fast
而不允许null值使用。此外Guava提供一些工具可以帮你在必须使用null
的时候更容易的使用null,也可以帮你避免使用null。
null在集合中的使用
不要在Set
中使用null
作为值或者在Map
中使用null
作为key。
当想在Map中使用null作为值时,最好把这些有着null值的key单独放在一个Set
中。Map
中存放null值的key和非null值的key是很简单的,但是最好不要这样做。把有着null值的key分离是更好的,并且需要考虑key的值为null对你的程序意味着什么。
如果在List
中使用null,若List
是很稀疏的,可能你需要使用Map<Integer, E>
(Map中的key作为下标,value作为List存储的值)。这样使用可能更有效,也更准确的满足你的应用程序的需要。
是否存在可以使用的空对象(null obejct),虽然不是总是存在的。如果在枚举中添加一个常量来表示你对null值期望的含义。例如,在java.math.RoundingMode
中有一个UNNECESSARY
值表示不需要舍入,如果舍入是必须的则抛出异常。
如果你确实需要null值,而且在使用不兼容null的集合中遇到了问题,请选择其他的实现方法。比如,使用Collections.unmodifiableList(Lists.newArrayList())
代替ImmutableList
。
null的替代者Optional
Optional比null更具有可读性,它强制的要求你去拆开Optional,避免直接使用可能造成返回null的方法。在实际的编写代码中,我们经常会忘记对方法的返回值是否为null进行判断,而更能记住对方法参数传递非null值。当把方法的返回值改成Optional的时候,调用者很难忘记null的情况,因为他们不得不拆开Optional。
Guava中提供了Optional
类com.google.common.base.Optional
,Java核心类库中在JDK1.8之后也加入了java.util.Optional
。
首先看下JDK和Guava提供的Optional
类有哪些方法
JDK | Guava | 描述 |
---|---|---|
Optional.of(T) |
Optional.of(T) |
用非null的值构造Optional 对象,使用null值会快速失败 |
Optional.empty() |
Optional.absent() |
返回某种类型不存在的Optional |
Optional.ofNullable(T) |
Optional.fromNullable(T) |
把可能为null的值构造为Optional ,非null值视为存在,null视为不存在 |
boolean isPresent() |
boolean isPresent() |
Optional 是否是非null的实例 |
T get() |
T get() |
返回存在的T类型的实例,不存在则抛出异常(JDK抛出异常NoSuchElementException ,Guava抛出异常IllegalStateException ) |
T orElse(T) |
T or(T) |
返回T类型的实例,如果不存在则返回某个值 |
T orElseThrow() |
- | 返回T类型的实例,如果不存在则抛出NoSuchElementException |
- | T orNull() |
返回T类型的实例,如果不存在返回null,是fromNullable的反操作 |
- | Set<T> asSet() |
把Optional 的值返回为一个不可变的Set ,Set中包含一个元素或者为空 |
- | Optional.fromJavaUtil(java.util.Optional<T>) |
把JDK的Optional转换为Guava的Optional |
- | java.util.Optional<T> Optional.toJavaUtil(Optional<T>) |
把Guava的Optional转换为JDK的Optional |
如何有效的使用Optional来提高我们代码的健壮性和可读性?
首先看一段我们最常编写的代码
/**
* 以自然顺序比较两个字符串并返回较大的字符串
*/
public String compare(String firstString, String secondString) {
if (firstString.compareTo(secondString) >= 0) {
return firstString;
}
return secondString;
}
这种情况下如果我们的传参是null,则会抛出空指针异常。
改进后的代码
/**
* 以自然顺序比较两个字符串并返回较大的字符串(若是字符串为null则返回null)
*/
public String compare(String firstString, String secondString) {
if (firstString == null || secondString == null) {
return null;
}
if (firstString.compareTo(secondString) >= 0) {
return firstString;
}
return secondString;
}
改进后的代码可以避免因传入null值导致出现异常,但是仍不可避免的就是对返回值的判断,如果我们按照如下方式调用
@Test
public void compare() {
UsingOptional usingOptional = new UsingOptional();
String first = "abcde";
String result = usingOptional.compare(first, null);
if (result.equalsIgnoreCase(first)) {
System.out.println("First Win!!!");
}
}
仍会在if语句处抛出异常,除非在比较前进行null值的判断,可有时我们不太关注null值判断的话,则会引发这样的bug。如何防止这样的情况发生呢?按照前文的建议,用Optional包装函数的返回值。
/**
* 以自然顺序比较两个字符串并返回较大的字符串
*/
public Optional<String> compareReturnOption(String firstString, String secondString) {
if (firstString == null || secondString == null) {
return Optional.fromNullable(null);
}
if (firstString.compareTo(secondString) >= 0) {
return Optional.of(firstString);
}
return Optional.of(firstString);
}
这样处理后,方法的返回值是Optional类,调用者需要拆开Optional以获取实际返回值
@Test
public void compareReturnOption() {
UsingOptional usingOptional = new UsingOptional();
String first = "abcde";
Optional<String> result = usingOptional.compareReturnOption(first, null);
if (result.isPresent()) {
if (result.get().equalsIgnoreCase(first)) {
System.out.println("First Win!!!");
}
}
}
有的人会有些困惑,这样的代码编写方式和传统的编写方式如下代码所示
@Test
public void compare() {
UsingOptional usingOptional = new UsingOptional();
String first = "abcde";
String result = usingOptional.compare(first, null);
if (result != null) {
if (result.equalsIgnoreCase(first)) {
System.out.println("First Win!!!");
}
}
}
并没有什么区别,都需要判空result != null
或result.isPresent()
然后取值。我认为使用Optional
包装发法返回值的重点在于能够提示调用者返回的值可能是absent
,能够引起调用者的关注,防止未检查的空指针异常抛出。
更进一步,如果我们把方法参数也用Optional包装,会成什么样子呢?
/**
* 以自然顺序比较两个字符串并返回较大的字符串
*/
public Optional<String> compareParamOption(Optional<String> firstString, Optional<String> secondString) {
if (!firstString.isPresent() || !secondString.isPresent()) {
return Optional.fromNullable(null);
}
if (firstString.get().compareTo(secondString.get()) >= 0) {
return firstString;
}
return secondString;
}
可以看到方法块中每次使用参数时都要调用参数的get方法,然后看下调用方代码
@Test
public void compareParamOption() {
UsingOptional usingOptional = new UsingOptional();
Optional<String> first = Optional.of("abcde");
// 这种写法会导致运行出错
// Optional<String> second = Optional.of(null);
Optional<String> second = Optional.fromNullable(null);
Optional<String> result = usingOptional.compareParamOption(first, second);
if (result.isPresent()) {
if (result.get().equalsIgnoreCase(first.get())) {
System.out.println("First Win!!!");
}
}
}
调用者需要自己创建参数的Optional包装对象,在方法代码和调用者代码中都看到了使用Optional包装参数所增加的繁琐语句,可见方法参数使用Optional包装并不是一个很好的编码实践。
更甚者,如果我们的参数时自定义类型,就需要使用Optional包装多层,判断时需要逐层拆解,工作量增长严重而且并不会比未包装的方式更健壮、更简洁。
Optional包装自定义类型
/**
* 以自然顺序比较两个人名字并返回较大的人员
*/
public Optional<Person> comparePersonName(Optional<Person> firstPerson, Optional<Person> secondPerson) {
if (!firstPerson.isPresent() || !secondPerson.isPresent()) {
return Optional.fromNullable(null);
}
if (!firstPerson.get().getName().isPresent() || !secondPerson.get().getName().isPresent()) {
return Optional.fromNullable(null);
}
if (firstPerson.get().getName().get().compareTo(secondPerson.get().getName().get()) >= 0) {
return firstPerson;
}
return secondPerson;
}
所以使用Optional的最佳编程实践应该是使用Optional包装方法的返回值,调用者在接收返回之后需要判断Optional是否absent,以防止空指针异常的产生。
JDK的Optional
JDK提供的Optional相比Guava的Optional有着更多的应用场景,其中最重要的就是和Lambda配合使用给开发者带来的收益。
@Nullable
和@NonNull
为了在编码过程中提醒编码人员对null
的注意,一些框架引入了两个注解——@Nullable
和@NonNull
,这两个注解在程序运行的过程中不会起任何作用,只会在IDE、编译器、FindBugs检查、生成文档的时候有做提示,同时对null
提供注解也是JSR 305
中的提议规范。这样在编写完代码进行代码质量检查时,可以在一定程度上防止我们把对null的错误使用。