前面在介绍清单用法的时候,讲到了既能使用for循环遍历清单,也能通过stream流式加工清单。譬如从一个苹果清单中挑选出红苹果清单,采取for循环和流式处理都可以实现。下面是通过for循环挑出红苹果清单的代码例子:

	// 通过简单的for循环挑出红苹果清单
private static void getRedAppleWithFor(List<Apple> list) {
List<Apple> redAppleList = new ArrayList<Apple>();
for (Apple apple : list) { // 遍历现有的苹果清单
if (apple.isRedApple()) { // 判断是否为红苹果
redAppleList.add(apple);
}
}
System.out.println("for循环 红苹果清单=" + redAppleList.toString());
}

至于通过流式处理挑出红苹果清单的代码示例如下:

	// 通过流式处理挑出红苹果清单
private static void getRedAppleWithStream(List<Apple> list) {
// 挑出红苹果清单
List<Apple> redAppleList = list.stream() // 串行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.collect(Collectors.toList()); // 返回一串清单
System.out.println("流式处理 红苹果清单=" + redAppleList.toString());
}

然而上述的两段代码只能在数据完整的情况下运行,一旦原始的苹果清单存在数据缺失,则两段代码均无法正常运行。例如,苹果清单为空,清单中的某条苹果记录为空,某个苹果记录的颜色字段为空,这三种情况都会导致程序遇到空指针异常而退出。看来编码不是一件轻松的活,不但要让程序能跑通正确的数据,而且要让程序对各种非法数据应对自如。换句话说,程序要足够健壮,要拥有适当的容错性,即使是吃错药了,也要能够自动吐出来,而不是硬吞下去结果一病不起。对应到挑选红苹果的场合中,则需层层递进判断原始苹果清单的数据完整性,倘若发现任何一处的数据存在缺漏情况(如出现空指针),就跳过该处的数据处理。于是在for循环前后添加了空指针校验的红苹果挑选代码变成了下面这样:

	// 在for循环的内外添加必要的空指针校验
private static void getRedAppleWithNull(List<Apple> list) {
List<Apple> redAppleList = new ArrayList<Apple>();
if (list != null) { // 判断清单非空
for (Apple item : list) { // 遍历现有的苹果清单
if (item != null) { // 判断该记录非空
if (item.getColor() != null) { // 判断颜色字段非空
if (item.isRedApple()) { // 判断是否为红苹果
redAppleList.add(item);
}
}
}
}
}
System.out.println("加空指针判断 红苹果清单=" + redAppleList.toString());
}

由此可见修改后的for循环代码一共增加了三个空指针判断,但是上面代码明显太复杂了,不必说层层嵌套的条件分支,也不必说多次缩进的代码格式,单单说后半部分的数个右花括号,简直叫人看得眼花缭乱,难以分清哪个右花括号究竟对应上面的哪个流程控制语句。这种情况实在考验程序员的眼力,要是一不留神看走眼放错其它代码的位置,岂不是捡了芝麻丢了西瓜?
空指针的校验代码固然繁琐,却是万万少不了的,究其根源,乃是Java设计之初偷懒所致。正常情况下,声明某个对象时理应为其分配默认值,从而确保该对象在任何时候都是有值的,但早期的Java图省事,如果程序员没在声明对象的同时加以赋值,那么系统也不给它初始化,结果该对象只好指向一个虚无缥缈的空间,而在太虚幻境中无论做什么事情都只能是黄粱一梦。
空指针的设计缺陷根深蒂固,以至于后来的Java版本难以根除该毛病,迟至Java8才推出了针对空指针的解决方案——可选器Optional。Optional本质上是一种特殊的容器,其内部有且仅有一个元素,同时该元素还可能为空。围绕着这个可空元素,Optional衍生出若干泛型方法,目的是将复杂的流程控制语句归纳为接续进行的方法调用。为了兼容已有的Java代码,通常并不直接构造Optional实例,而是调用它的ofNullable方法塞入某个实体对象,再调用Optional实例的其它方法进行处理。Optional常用的实例方法罗列如下:
get:获取可选器中保存的元素。如果元素为空,则扔出无此元素异常NoSuchElementException。
isPresent:判断可选器中元素是否为空。非空返回true,为空返回false。
ifPresent:如果元素非空,则对该元素执行指定的Consumer消费事件。
filter:如果元素非空,则根据Predicate断言条件检查该元素是否符合要求,只有符合才原样返回,若不符合则返回空值。
map:如果元素非空,则执行Function函数实例规定的操作,并返回指定格式的数据。
orElse:如果元素非空就返回该元素,否则返回指定的对象值。
orElseThrow:如果元素非空就返回该元素,否则扔出指定的异常。
接下来看一个Optional的简单应用例子,之前在苹果类中写了isRedApple方法,用来判断自身是否为红苹果,该方法的代码如下所示:

	// 判断是否红苹果
public boolean isRedApple() {
// 不严谨的写法。一旦color字段为空,就会发生空指针异常
return this.color.toLowerCase().equals("red");
}

显而易见这个isRedApple方法很不严谨,一旦颜色color字段为空,就会发生空指针异常。常规的补救自然是增加空指针判断,遇到空指针的情况便自动返回false,此时方法代码优化如下:

	// 判断是否红苹果
public boolean isRedApple() {
// 常规的写法,判断color字段是否为空,再做分支处理
boolean isRed = (this.color==null) ? false : this.color.toLowerCase().equals("red");
return isRed;
}

现在借助可空器Optional,支持一路过来的方法调用,先调用ofNullable方法设置对象实例,再调用map方法转换数据类型,再调用orElse方法设置空指针之时的取值,最后调用equals方法进行颜色对比。采取Optional形式的方法代码示例如下:

	// 判断是否红苹果
public boolean isRedApple() {
// 利用Optional进行可空对象的处理,可空对象指的是该对象可能不存在(空指针)
boolean isRed = Optional.ofNullable(this.color) // 构造一个可空对象
.map(color -> color.toLowerCase()) // map指定了非空时候的取值
.orElse("null") // orElse设置了空指针时候的取值
.equals("red"); // 再判断是否红苹果
return isRed;
}

然而上面Optional方式的代码行数明显超过了条件分支语句,它的先进性又何从体现呢?其实可选器并非要完全取代原先的空指针判断,而是提供了另一种解决问题的新思路,通过合理搭配各项技术,方能取得最优的解决办法。仍以挑选红苹果为例,原本判断元素非空的分支语句“if (item != null)”,采用Optional改进之后的循环代码如下所示:

	// 把for循环的内部代码改写为Optional校验方式
private static void getRedAppleWithOptionalOne(List<Apple> list) {
List<Apple> redAppleList = new ArrayList<Apple>();
if (list != null) { // 判断清单非空
for (Apple item : list) { // 遍历现有的苹果清单
if (Optional.ofNullable(item) // 构造一个可空对象
.map(apple -> apple.isRedApple()) // map指定了item非空时候的取值
.orElse(false)) { // orElse指定了item为空时候的取值
redAppleList.add(item);
}
}
}
System.out.println("Optional1判断 红苹果清单=" + redAppleList.toString());
}

注意到以上代码仍然存在形如“if (list != null)”的清单非空判断,而且该分支后面还有要命的for循环,这下既要利用Optional的ifPresent方法输入消费行为,又要使用流式处理的forEach方法遍历每个元素。于是进一步改写后的Optional代码变成了下面这般:

	// 把清单的非空判断代码改写为Optional校验方式
private static void getRedAppleWithOptionalTwo(List<Apple> list) {
List<Apple> redAppleList = new ArrayList<Apple>();
Optional.ofNullable(list) // 构造一个可空对象
.ifPresent( // ifPresent指定了list非空时候的处理
apples -> {
apples.stream().forEach( // 对苹果清单进行流式处理
item -> {
if (Optional.ofNullable(item) // 构造一个可空对象
.map(apple -> apple.isRedApple()) // map指定了item非空时候的取值
.orElse(false)) { // orElse指定了item为空时候的取值
redAppleList.add(item);
}
});
});
System.out.println("Optional2判断 红苹果清单=" + redAppleList.toString());
}

虽然二度改进后的代码已经消除了空指针判断分支,但是依然留下是否为红苹果的校验分支,仅存的if语句着实碍眼,干脆一不做二不休引入流式处理的filter方法替换if语句。几经修改得到了以下的最终优化代码:

	// 联合运用Optional校验和流式处理
private static void getRedAppleWithOptionalThree(List<Apple> list) {
List<Apple> redAppleList = new ArrayList<Apple>();
Optional.ofNullable(list) // 构造一个可空对象
.ifPresent(apples -> { // ifPresent指定了list非空时候的处理
// 从原始清单中筛选出红苹果清单
redAppleList.addAll(apples.stream()
.filter(a -> a != null) // 只挑选非空元素
.filter(Apple::isRedApple) // 只挑选红苹果
.collect(Collectors.toList())); // 返回结果清单
});
System.out.println("Optional3判断 红苹果清单=" + redAppleList.toString());
}

好不容易去掉了所有if和for语句,尽管代码的总行数未有明显减少,不过逻辑结构显然变得更加清晰了。

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(七十七)使用Optional规避空指针异常的更多相关文章

  1. Java开发笔记(十七)各得其所的多路分支

    前面提到条件语句的标准格式为“if (条件) { /* 条件成立时的操作代码 */ } else { /* 条件不成立时的操作代码 */ }”,乍看之下仿佛只有两个分支,一个是条件成立时的分支,另一个 ...

  2. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  3. Java开发笔记(七十六)如何预防异常的产生

    每个程序员都希望自己的程序稳定运行,不要隔三岔五出什么差错,可是程序运行时冒出来的各种异常着实烦人,令人不胜其扰.虽然可以在代码中补上try/catch语句捕捉异常,但毕竟属于事后的补救措施.与其后知 ...

  4. Java开发笔记(八十七)随机访问文件的读写

    前面介绍了字符流读写文件的两种方式,包括文件字符流和缓存字符流,但是它们的写操作都存在一个问题:不管是write方法还是append方法,都只能从文件开头写入,而不能追加到文件末尾或者在文件中间某个位 ...

  5. Java开发笔记(二十七)数值包装类型

    方法的出现缘起优化代码结构,但它的意义并不局限于此,正因为有了方法定义,编程语言才更像一门能解决实际问题的工具,而不仅仅是只能用于加减乘除的计算器.在数学的发展过程中,为了表示四则运算,人们创造了加减 ...

  6. Java开发笔记(三十七)利用正则串分割字符串

    前面介绍了处理字符串的常用方法,还有一种分割字符串的场景也很常见,也就是按照某个规则将字符串切割为若干子串.分割规则通常是指定某个分隔符,根据字符串内部的分隔符将字符串进行分割,例如逗号.空格等等都可 ...

  7. Java开发笔记(四十七)关键字this的用法

    前面介绍了类的基本定义,包括成员属性.成员方法.构造方法几个组成要素,可谓是具备了类的完整封装形态.不过在进行下一阶段的学习之前,有必要梳理一下前述的类定义代码,看看是否存在哪些需要优化的地方.首先观 ...

  8. Java开发笔记(五十七)因抽象方法而产生的抽象类

    前面介绍了类的常见用法,令人感叹面向对象的强大,几乎日常生活中的所有事物,都可以抽象成Java的基类及其子类.然而抽象操作也有副作用,就是某个抽象而来的行为可能是不确定的,比如半夜鸡叫,如果是公鸡则必 ...

  9. Java开发笔记(六十七)清单:ArrayList和LinkedList

    前面介绍了集合与映射两类容器,它们的共同特点是每个元素都是唯一的,并且采用二叉树方式的类型还自带有序性.然而这两个特点也存在弊端:其一,为啥内部元素必须是唯一的呢?像手机店卖出了两部Mate20,虽然 ...

随机推荐

  1. framework7 入门(数据绑定)

    数据绑定是用template7,官网,framework7内置template7,不用单独下载,下面介绍一些简单的绑定 当你的数据是个数组 return{ data:function(){ let i ...

  2. 串口RS232和485通信的波形分析

    一.串行数据的格式 异步串行数据的一般格式是:起始位+数据位+停止位,其中起始位1 位,数据位可以是5.6.7.8位,停止位可以是1.1.5.2位. 起始位是一个值为0的位,所以对于正逻辑的TTL电平 ...

  3. 使用Sublime Text 或 vs2017开发Node.js程序

    在学习一门开发语言时,为了从简单的方式入手,有时候直接用Notepad开始敲代码.曾经我也这样干过,这样做简洁而不简单啊! 随着时间的流逝,人也变得懒惰起来,做事前总是想借助一些工具来搞事情.< ...

  4. [Swift]LeetCode518. 零钱兑换 II | Coin Change 2

    You are given coins of different denominations and a total amount of money. Write a function to comp ...

  5. [Swift]LeetCode643. 子数组最大平均数 I | Maximum Average Subarray I

    Given an array consisting of n integers, find the contiguous subarray of given length k that has the ...

  6. [Swift]LeetCode861. 翻转矩阵后的得分 | Score After Flipping Matrix

    We have a two dimensional matrix A where each value is 0 or 1. A move consists of choosing any row o ...

  7. kubernetes---docker-image

    imagePullPolicy  <String> Always : 总是从仓库下载 , 如果是image的tag是latest ,如果需要一直保持最新,则应设为Always ,从仓库下载 ...

  8. 值得收藏的Mybatis通用Mapper使用大全。

    引言 由于小编的记性不太好,每次在写代码的时候总是把通用mapper的方法记错,所以今天把通用mapper的常用方法做一下总结,方便以后直接查看.好了,不废话啦. 引包 <!-- 通用Mappe ...

  9. FAutoTest-微信小程序 / 公众号H5 自动化利器

    X5内核H5自动化背景 近来有很多童靴咨询如何做微信小程序/公众号等H5页面来做自动化,之前写了一篇文章微信小程序自动化测试实践 https://www.cnblogs.com/yyoba/p/945 ...

  10. 浅谈Java内存模型

    Java内存模型虽说是一个老生常谈的问题 ,也是大厂面试中绕不过的,甚至初级面试也会问到.但是真正要理解起来,还是相当困难,主要这个东西看不见,摸不着.网上已经有大量的博客,但是人家的终究是人家的,自 ...