Java8中Optional的一些常见错误用法总结
前言
Java8引入的Optional类型,基本是把它当作null值优雅的处理方式。其实也不完全如此,Optional在语义上更能体现有还是没有值。所以它不是设计来作为null的替代品,如果方法返回null值表达了二义性,没有结果或是执行中出现异常。
在Oracle 做 Java语言工作的 BrianGoetz在StackOverflow回复ShouldJava8gettersreturnoptionaltype?中讲述了引入 Optional的主要动机。
Ourintentionwastoprovidealimitedmechanismforlibrarymethodreturntypeswherethereneededtobeaclearwaytorepresent“noresult”,andusingnullforsuchwasoverwhelminglylikelytocauseerrors.
说的是 Optional提供了一个有限的机制让类库方法返回值清晰的表达有与没有值,避免很多时候null造成的错误。并非有了 Optional就要完全杜绝NullPointerException。
在Java8之前一个实践是方法返回集合或数组时,应返回空集合或数组表示没有元素;而对于返回对象,只能用null来表示不存在,现在可以用 Optional来表示这个意义。
自Java8于 2014-03-18发布后已5年有余,这里就列举几个我们在项目实践中使用Optional常见的几个用法。
Optional类型作为字段或方法参数
这儿把Optional 类型用为字段(类或实例变量)和方法参数放在一起来讲,是因为假如我们使用IntelliJIDEA来写Java8代码,IDEA对于 Optional作为字段和方法参数会给出同样的代码建议:
Reportsanyusesofjava.util.Optional
,java.util.OptionalDouble,java.util.OptionalInt,java.util.OptionalLongorcom.google.common.base.Optionalasthetypeforafieldorparameter.Optionalwasdesignedtoprovidealimitedmechanismforlibrarymethodreturntypeswherethereneededtobeaclearwaytorepresent"noresult".Usingafieldwithtypejava.util.OptionalisalsoproblematiciftheclassneedstobeSerializable,whichjava.util.Optionalisnot.
不建议用任何的Optional类型作为字段或参数,Optional设计为有限的机制让类库方法返回值清晰的表达"没有值"。Optional是不可被序列化的,如果类是可序列化的就会出问题。
上面其实重复了Java8引入 Optional的意图,我们还有必要继续深入理解一下为什么不该用 Optional作为字段或方法参数。
当我们选择Optional类型而非内部包装的类型后,应该是假定了该Optional类型不为null,否则我们在使用Optional字段或方法参数时就变得复杂了,需要进行两番检查。
publicclassUser{ privateStringfirstName; privateOptionalmiddleName=Optional.empty(); privateStringlastName; publicvoidsetMiddleName(Optional middleName){ this.middleName=middleName; } publicStringgetFullName(){ StringfullName=firstName; if(middleName!=null){ if(middleName.isPresent()){ fullName=fullName.concat("."+middleName.get()); } returnfullName.concat("."+lastName); } }
由于middleName的setter方法,我们可能造成middleName变为null值,所以在构建fullName时必须两重检查。
并且在调用setMiddleName(...)方法时也有些累赘了
user.setMiddleName(Optional.empty()); user.setMiddleName(Optional.of("abc"));
而如果字段类型非Optional类型,而传入的方法参数为Optional类型,要进行赋值的话
privateStringmiddleName; publicvoidupdateMiddleName(OptionalmiddleName){ if(middleName!=null){ this.middleName=middleName.orElse(null); }else{ this.middleName=null; } }
前面两段代码如果应用Optional.ofNullable(...)包裹Optional来替代if(middleName!=null)就更复杂了。
对于本例直接用String类型的middleName 作为字段或方法参数就行,null值可以表达没有middleName。如果不允许null值 middleName,显式的进行入口参数检查而拒绝该输入--抛出异常。
利用Optional过度检查方法参数
这一Optional的用法与之前的可能为null值的方法参数,不分清红皂白就用if...else检查,总有一种不安全感,步步惊心,结果可能事与愿违。
publicUsergetUserById(StringuserId){ if(userId!=null){ returnuserDao.findById(userId); }else{ returnnull; } }
只是到了Java8改成了用Optional
returnif(Optional.ofNullable(userId) .map(id->userDao.findById(id)) .orElse(null);
上面两段代码其实是同样的问题,如果输入的userId是null值不调用findById(...)方法而直接返回null值,这就有两个问题
userDao.findById(...) getUserById(userId)
这种情况下立即抛出NullPointerException是一个更好的主意,参考下面的代码
publicUsergetUserById(StringuserId){//抛出出NullPointerException如果nulluserId returnuserDao.findById(Objects.requireNoNull(userId,"InvalidnulluserId"); } //or publicUsergetUserById(StringuserId){//抛出IllegalArgumentException如果nulluserId Preconditions.checkArgument(userId!=null,"InvalidnulluserId"); returnuserDao.findById(userId); }
即使用了Optional的orElseThrow抛出异常也不能明确异常造成的原因,比如下面的代码
publicUsergetUserById(StringuserId){ returnOptional.ofNullable(userId) .map(id->userDao.findById(id)) orElseThrow(()-> newRuntimeException("userId是null或findById(id)返回了null值")); }
纠正办法是认真的审视方法的输入参数,对不符合要求的输入应立即拒绝,防止对下层的压力与污染,并报告出准确的错误信息,以有利于快速定位修复。
Optional.map(...)中再次null值判断
假如有这样的对象导航关系user.getOrder().getProduct().getId(),输入是一个 user对象
StringproductId=Optional.ofNullable(user) .map(User::getOrder) .flatMap(order->Optional.ofNullable(order.getProduct()))//1 .flatMap(product->Optional.ofNullable(product.getId()))//2 .orElse("");
#1和#2中应用flatMap再次用Optional.ofNullable()是因为担心order.getProduct()或product.getId()返回了null值,所以又用Optional.ofNullable(...)包裹了一次。代码的执行结果仍然是对的,代码真要这么写的话真是Oracle的责任。这忽略了Optional.map(...)的功能,只要看下它的源代码就知道
publicOptionalmap(Functionmapper){ Objects.requireNonNull(mapper); if(!isPresent()) returnempty(); else{ returnOptional.ofNullable(mapper.apply(value)); } }
map(...)函数中已有考虑拆解后的null值,因此呢flatMap中又Optional.ofNullable是多余的,只需简单一路用map(...)函数
StringproductId=Optional.ofNullable(user) .map(User::getOrder) .map(order->order.getProduct())//1 .map(product->product.getId())//2 .orElse("");
Optional.ofNullable应用于明确的非 null值
如果有时候只需要对一个明确不为null的值进行Optional包装的话,就没有必要用ofNullable(...)方法,例如
publicOptionalgetUserById(StringuserId){ if("ADMIN".equals(userId)){ UseradminUser=newUser("admin"); returnOptional.ofNullable(adminUser);//1 }else{ returnuserDao.findById(userId); } }
在代码#1处非常明确adminUser是不可能为null值的,所以应该直接用Optional.of(adminUser)。这也是为什么Optional要声明of(..)和ofNullable(..)两个方法。看看它们的源代码:
publicstaticOptional of(Tvalue){ returnnewOptional<>(value); } publicstatic Optional ofNullable(Tvalue){ returnvalue==null?empty():of(value); }
知道被包裹的值不可能为null时调用ofNullable(value)多了一次多余的null值检查。相应的对于非null值的字面常量
Optional.ofNullable(100);//这样不好 Optional.of(100);//应该这么用
小结:
- 要理解Optional的设计用意,所以语意上应用它来表达有/无结果,不适于作为类字段与方法参数
- 倾向于方法返回单个对象,用Optional类型表示无结果以避免null值的二义性
- Optional进行方法参数检查不能掩盖了错误,最好是明确非法的参数输入及时抛出输入异常
- 对于最后两种不正确的用法应熟悉Optional的源代码实现就能规避
链接:
Java8Optionalusecases
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。