欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java开发笔记(七十七)使用Optional规避空指针异常

程序员文章站 2022-07-11 09:21:59
前面在介绍清单用法的时候,讲到了既能使用for循环遍历清单,也能通过stream流式加工清单。譬如从一个苹果清单中挑选出红苹果清单,采取for循环和流式处理都可以实现。下面是通过for循环挑出红苹果清单的代码例子: 至于通过流式处理挑出红苹果清单的代码示例如下: 然而上述的两段代码只能在数据完整的情 ......

前面在介绍清单用法的时候,讲到了既能使用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开发笔记(序)章节目录