Hello,你们好呀,我是老丑。
今天的主题是它——空指针异常。Null Pointer Exception。
说明:空指针是 Null Pointer。空指针异常是 Null Pointer Exception。对于空指针的引用,去触发其行为亦或者获取其属性会产生空指针异常。
各位在代码届纵横数年,风光无限.不知道是否还会遇见这臭名昭著的空指针问题呢?想必不会了吧.
注:这篇文章讲的是空指针异常,也不全是。
本篇文章以Java语言输出,但是不局限。
空指针异常,你来了吗?
Null Pointer Exception : 你若'盛开',蝴蝶自来!
各位大佬:那我还是不'盛开'吧!退!退!退!
我们看一个空指针异常
的demo
public class NullPointer {
public static void main(String[] args) {
NullPointer nullPointer = new NullPointer();
System.out.println(nullPointer.getLength(null));
}
/**
* 获取字符串 s 的长度
* @param s 字符串
* @author laochou
*/
public int getLength(String s) {
return s.length();
}
}
如上述代码中 getLength()
是在开发过程中十分带有其面向对象
四大特性中的封装浓厚色彩的常规操作。
运行上述代码,恭喜你就可以得到一个Null Pointer Exception
啦。就像很多人没有boy friend
或者 girl friend
,动不动就喊着要New
一个出来,是啊,这件事是美好的呀,但是Null Pointer
也是生活呀。不要只抬头看天上的月亮,也要偶尔看看地上的六便士一样。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at cn.laochou.bug.NullPointer.getLength(NullPointer.java:15)
at cn.laochou.bug.NullPointer.main(NullPointer.java:7)
Null Pointer Exception 也是生活
空指针异常,一个我们既"爱"又恨的它。
我相信,各位大佬的代码不会出现空指针异常。但是线上环境中,错误日志中时隐时现的Null Pointer Exception
,从哪里来的呢?
空指针异常并不可怕,可怕的是你,发现了空指针异常,但你却不去解决它。就如失败并不可怕,可怕的是你不敢面对它
。
我要面对它
它的进攻虽然猛烈——一大堆的堆栈。但那又何妨。只要我防御做的好,它就攻不进来——防御式编程。
想想也是。历代排除法都是好使的,就比如你考试的时候,题目的答案死活算不出来,但是你知道其他是几个是不可能的,或者是错误的。但这就已经够了——结果是好的,过程我们可以偷偷摸摸的搞吧搞吧。
public static void main(String[] args) {
NullPointer nullPointer = new NullPointer();
System.out.println(nullPointer.getLength(null));
}
/**
* 获取字符串 s 的长度
* @param s 字符串
* @author laochou
*/
public int getLength(String s) {
if(s == null) return 0;
return s.length();
}
public int getLength(String s) {
return s == null ? 0 : s.length();
}
}
以上两种写法都可以避免产生Null Pointer Exception
,毕竟空指针的业务场景被我们给Pass掉了。
上述对于getLength()
方法,其本质就是对外界完全不信任
了。
getLength(): 我能不能完全信任外界呢?
Null Pointer Exception: 当然可以呀,你让外界方法调用你的时候,让它们传递安全可靠的字符串对象就好了呀。
getLength(): 是吗?那还挺好的,那我的底层实现就可以不校验了。
getLength(): 但是我真的可以相信外界吗?它们真的值得去相信吗
internal monologue: 我相信看到这里的各位大佬们,可能会有一些感触哦。我们也想相信这浮躁的社会,但谁也不敢相信,毕竟谁都不想被伤的遍体鳞伤。
Null Pointer Exception: 你还是靠自己吧,外界的哪些大猪蹄子!!!
对于工具方法,是需要防御式编程的。
其实从另外一个角度来看,我们做了防御式编程,底层实现更加内聚了。我们想想,如果由外界来做防御,保证其传递过来的对象是可靠的,那么是否是一种逻辑冗余。调用处全是 if
,那是不是可以了解为实现内聚了呢?
代码本身就是追求高内聚,低耦合
的。
防御式编程 到 以点带面?怎么回事
我们可以以点带面
的思考下,下面一个业务场景。
场景如下:
我们是一个业务系统,业务系统中存在业务项配置,其中业务项配置存在 当前的业务配置状态和上一次的业务配置状态。其中业务配置通过OOA,OOD,OOP最后成为了我们下面的 BusinessConfig
。我们提供了一个服务来修改我们的业务配置的状态。BusinessConfigService
主要是提供服务。
// 业务配置对象
class BusinessConfig {
// 业务编码
String businessCode;
// 当前的业务配置状态
String currentStatus;
// 上一次的业务配置状态
String lastStatus;
public String getCurrentStatus() {
return currentStatus;
}
public void setCurrentStatus(String currentStatus) {
this.currentStatus = currentStatus;
}
public String getLastStatus() {
return lastStatus;
}
public void setLastStatus(String lastStatus) {
this.lastStatus = lastStatus;
}
}
// 领域服务
class BusinessConfigService{
// 修改业务配置的状态
void updateBusinessConfigStatus(BusinessConfig businessConfig, String status) {
businessConfig.setLastStatus(businessConfig.getCurrentStatus());
businessConfig.setCurrentStatus(status);
businessConfigRepository.updateBusinessConfig(businessConfig);
}
}
// repo
class BusinessConfigRepository {
void updateBusinessConfig(BusinessConfig businessConfig) {
mapper.update(businessConfig);
}
}
各位大佬,请看我们BusinessConfigService.updateBusinessConfigStatus(businessConfig,status)
该方法的实现
- 我们先修改
businessConfig
的上一次状态,设置为businessConfig
的当前的状态 - 修改
businessConfig
的当前状态,设置为该方法入参中的状态。 - 调用
businessConfigRepository
来更新businessConfig
。
以上貌似看上去很完美,很OK。
思考
// 领域服务
class BusinessConfigService{
// 修改业务配置的状态
void updateBusinessConfigStatus(BusinessConfig businessConfig, String status) {
// 这两个状态的切换,真的应该在这里写吗?
businessConfig.setLastStatus(businessConfig.getCurrentStatus());
businessConfig.setCurrentStatus(status);
businessConfigRepository.updateBusinessConfig(businessConfig);
}
}
对于updateBusinessConfigStatus(businessConfig, status)
方法中的两个状态切换,写在这里真的合适吗?
Service 层方法,真的需要关注领域对象中的状态变更的逻辑吗?虽然从业务角度上来看,确实可以满足业务诉求
我们看下,下面的实现是不是会更好一些呢?
// 业务配置对象
class BusinessConfig {
// 代表上面代码块中的 BusinessConfig 都存在
...
// 提供一个充血方法,刷新业务配置的状态
// 由该方法讲上述 服务层的 逻辑内聚在 业务配置对象中
void refreshBusinessConfigStatus(String status) {
this.setLastStatus(this.getCurrentStatus());
this.setCurrentStatus(status);
}
}
服务层演化:
// 领域服务
class BusinessConfigService{
// 修改业务配置的状态
void updateBusinessConfigStatus(BusinessConfig businessConfig, String status) {
businessConfig.refreshBusinessConfigStatus(status);
businessConfigRepository.updateBusinessConfig(businessConfig);
}
}
从演化后的代码看,在服务层的代码中,其BusinessConfig
的状态更新逻辑更加内聚了,该refreshBusinessConfigStatus(status)
方法封装了其状态发生变更的所有逻辑,如果有一天,更新BusinessConfig
状态的逻辑中还需要更新其他的一些属性,就比如状态更新时间,那么我们的改动点是不是很小了,外面都是使用你的现有资产。
当然这其实只是一个demo,大家可能觉得有点多此一举了。但是从项目长期迭代更新过程中,你累积的领域资产也就越来越多。迭代起来会越来越快。
跑题了,得拉回来。
OK,我们讲的还是空指针异常。在Java语言中,存在一项黑科技——Optional
// 这样写 是不是更优美了 可读性也增强了。
public int getLength(String s) {
// 优雅从不过时
return Optional.ofNullable(s).map(String::length).orElse(0);
}
在日常开发过程中,其实 Optional
用的非常多,判空利器。
Optional + Stream YYDS
对于集合的过滤,筛选
// 去除集合中的Null和空元素
public List<String> removeEmpty(List<String> codes) {
// orElseGet 就是当 ofNull 成立的话,就直接使用 orElseGet所得到的对象进行后续的处理。
return Optional.ofNullable(codes).orElseGet(() -> new ArrayList<>(0))
.stream()
.filter(Objects::nonNull)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
}
传入的集合空的,直接抛出业务异常
public List<String> getCodes(List<String> codes) {
return Optional.ofNullable(codes).orElseThrow(() -> new IllegalArgumentException("codes can't be null"))
.stream()
.filter(Objects::nonNull)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
}
这样的代码不香吗?各位大佬,please 秀出你们的Optional使用手法。