(翻译)Handling null in Java
原作者:Roy van Rijn
原文地址:http://www.redcode.nl/blog/2010/02/handling-null-in-java/
空值处理是我在jee的项目中遇到的最常见的问题. 但为什么这会是个问题呢? 接下来我们又准备选用何种策略来解决这个问题呢? 在这篇文章中我将尽力找出这个答案.
让我们先来看一段商业逻辑代码,并且我们假设没有空值:
public BigDecimal getBalance(Person person) { Set<Account> accounts = person.getAccounts(); BigDecimal totalBalance = BigDecimal.ZERO; for(Account account: accounts) { totalBalance = totalBalance.add(account.getBalance()); } return totalBalance; }
这看起来非常的好理解! 但是这不是我们大多数项目中的情况. 通常我们看到的代码是这样:
public BigDecimal getBalance(Person person) { if(person != null) { Set<Account> accounts = person.getAccounts(); if(accounts != null) { BigDecimal totalBalance = BigDecimal.ZERO; for(Account account: accounts) { if(account != null) { totalBalance = totalBalance.add(account.getBalance()); } } } return totalBalance; } else { return null; } }
呵呵,这看起来一点也不漂亮! 我们如何去改进呢?
Inversion of logic
第一个策略可以根据我之前写的文章inversion of logic(http://www.redcode.nl/blog/2009/12/inversion_of_logic/). 这里的思想是先退出, 这将提高这个方法的可读性. 让我们看看更具这个原则改进后的代码:
public BigDecimal getBalance(Person person) { if(person == null || person.getAccounts() == null) { return null; } Set<Account> accounts = person.getAccounts(); BigDecimal totalBalance = BigDecimal.ZERO; for(Account account: accounts) { if(account != null) { totalBalance = totalBalance.add(account.getBalance()); } } return totalBalance; }
这看起来好一点了,简短些,但是还是有我们不想要的”!=”和”==null”的检查
Code by contract
避免空值检查的最好方式是在你的应用中避免空值. 只需要在你的所有的方法返回时非空! 这听起来非常容易并且直接,但是做起来比听起来要困难一些,因为每个方法返回非空并不是我们的习惯.
令人欣慰的是目前有些ide已经实现了@NotNull和@Nullable这两个annotations. 有了这两个annotations你可以通知其他的程序员,你的IDE及代码分析工具:你创建你的方法时的思想:
@Nullable public Person getPerson(Long id) { return something.retrievePerson(id); } public void printPersonName(Long id) { Person person = getPerson(id); System.out.println(person.getName()); //Causes warning: getPerson is Nullable, thus this is a possible NPE! }
它也可以帮助你清楚的声明,你的方法调用中,参数person非空:
public void printPersonName(@NotNull Person person) { System.out.println(person.getName()); //Very good, we know we won't get a NPE here! } public void executeThis() { Person person = null; printPersonName(person); //Causes warning: person might be null, thus can cause a NPE! //Code analysis tools and/or IDE will warn you about this. }
它也可以帮助你修正可能的编码错误:
@NotNull
public Person getPersonFromDatabase(@NotNull Long id) {
//Use JPQL
Query query = em.createQuery("SELECT p FROM Person p WHERE p.id = :value");
q.setParameter("value", id);
return q.getSingleResult();
//The IDE will complain about this.
//The database might return null, we don't allow returning null in this method.
}使用这个方法你多了几分把握. 但是这不是万全之策. 我们必须停下来想一想,空值是从哪里来的?
事实上,当时停止返回空值的时候(这取决于你和你的团队),仍旧有一些没有检查的情况. 例如你使用的外部的apis,框架,ORM-mapper. 所以每次执行非你写的方法的时候,你仍旧不得不手工检查. 但是你务必要做到这一点. 而后的代码你可以不做检查,因为你使用了@NotNull这个annotation. 假如你对上面的Person对象及所有fields这么做了,那么你可以是第一个例子的代码变得漂亮,干净很多. 没有空值检查,它们由annotation约束. 唯一要做的就是添加参数的信息.
@NotNull public BigDecimal getBalance(@NotNull Person person) { Set<Account> accounts = person.getAccounts(); BigDecimal totalBalance = BigDecimal.ZERO; for(Account account: accounts) { totalBalance = totalBalance.add(account.getBalance()); } return totalBalance; }
在我的经验中,这样做很不错, 我已经这样做了有超过一打的次数,甚至在@NotNull和@Nullable出现之前. 在这两个annotations出现之前,我们就在javadoc里添加这样的信息, 但是随着IDE检查的出现,这样的做法就不变得容易很多.
Null object pattern
使用一个null-object是与coding-by-contract的方式完全不同的门路. 这种思想背后的模式是不返回空对象,代替的是返回一个真的对象. 我们的例子就会变成这样:
public interface Person { String getName(); void setName(String name); List<Account> getAccounts(); //..etc.. } public class NullPerson implements Person { public String getName() { return ""; } public void setName(String name) {} public List<Account> getAccounts() { return Collections.emptyList(); } }
这样做后,巨大的好处是你在调用person.getName()的时候是永远安全的,因为即使你得到一个NullPerson,但这个对象也早就存在了. 这个Null-Object很明显是个单例.
public BigDecimal getBalance(Person person) { Set<Account> accounts = person.getAccounts(); //NullPerson returns empty list, no more NPE! BigDecimal totalBalance = BigDecimal.ZERO; for(Account account: accounts) { totalBalance = totalBalance.add(account.getBalance()); } return totalBalance; }
关于这个模式的更详细阐述,但更一般的版本是由martin Fowler的special case pattern(http://martinfowler.com/eaaCatalog/specialCase.html). 这里不只有一个Null-special情况,你也可以扩展:
public class UnknownPerson implements Person { public String getName() { return ""; } public void setName(String name) {} public List<Account> getAccounts() { return Collections.emptyList(); } } public class InvalidatedPerson implements Person { public String getName() { return ""; } public void setName(String name) {} public List<Account> getAccounts() { return Collections.emptyList(); } }
现在你可以返回更多信息,而不只是一个无意义的”null”!
这种模式也有一个问题. 它也没有解决你的问题. 为什么我们会想要计算一个NullPerson的账户总结余呢?当你取得一个person并且是一个NullPerson实例的时候,捕获且适当的处理,而不是一味的继续下面的代码.
Safe null operator
有一段时间,有一种思考,就是通过Coin项目把Safe-null-operator引入java7中. 假如你有Groovy的编程经验,那么你可能见过以下的代码:
public String getFirstLetterOfName(Person person) { return person?.getName()?.substring(0, 1); }
这个思想就是用”?.”来替代”.”. 这个引号标志意味着”假如我们将要访问的对象是空,那么返回空,否则执行下一个方法”.
那么在这个例子中:
假如person为null,那么返回null
假如getName()为空,返回null
最后返回substring(0,1)的结果
假如写成目前的代码形式就是这样:
public String getFirstLetterOfName(Person person) { if(person == null) { return null; } if(person.getName() == null) { return null; } return person.getName().substring(0, 1); }
你也可以返回一个默认的值:
public void printName(Person person) { System.out.println(person?.getName() ?: "Anonymous"); }
假如perrson或者getName()是空,那么”?:”操作符就返回默认的字符串.
这看起来非常的不错吧,哈?唯一的问题是,这样的新操作符最终没有被引入到Java7的变更列表中.
有趣的是:你知道为什么”?:”操作符被称为”猫王”操作符么?从侧边看起来它像猫王的一个笑脸,包括他大圈的头发!