目录
烦人的空指针
封装MiniNullWrapper
核心思想
工具类封装
烦人的空指针
Java程序员的整个职业生涯都在和空指针做斗争,不死不休。
来看一段日常开发可能会出现的代码:
public class NullPointerTest {
/*** 需求:根据用户名查找该用户所在的部门名称** @param args*/
public static void main(String[] args) {
String departmentNameOfUser = getDepartmentNameOfUser("test");
System.out.println(departmentNameOfUser);
}
/*** 假设这是A-Service的服务* 这一步很烦!!!** @param username* @return*/
public static String getDepartmentNameOfUser(String username) {
ResultTO resultTO = getUserByName(username);
if (resultTO != null) {
User user = resultTO.getData();
if (user != null) {
Department department = user.getDepartment();
if (department != null) {
return department.getName();
}
}
}
return "未知部门";
}
/*** 假设这是B-Service的服务(不用关注具体逻辑,就是随机模拟返回值,可能为null)** @param username* @return*/
public static ResultTO getUserByName(String username) {
if (username == null || "".equals(username)) {
return null;
}
Department department;
User user;
if (ThreadLocalRandom.current().nextBoolean()) {
department = new Department("总裁办", 10086);
} else {
department = null;
}
if (ThreadLocalRandom.current().nextBoolean()) {
user = new User("周董", 18, department);
user.setDepartment(department);
} else {
user = null;
}
return ResultTO.buildSuccess(user);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class User {
private String name;
private Integer age;
private Department department;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Department {
private String name;
private Integer code;
}
@Getter
@Setter
static class ResultTO implements Serializable {
private Boolean success;
private String message;
private T data;
public static ResultTO buildSuccess(T data) {
ResultTO result = new ResultTO<>();
result.setSuccess(true);
result.setMessage("success");
result.setData(data);
return result;
}
public static ResultTO buildFailed(String message) {
ResultTO result = new ResultTO<>();
result.setSuccess(false);
result.setMessage(message);
result.setData(null);
return result;
}
}
}
你会发现,如果一个POJO的层级过深而且恰好作为返回值返回时,调用者将苦不堪言,为了避免空指针不得不写一大堆的if判断,也就是被迫做空指针探测。
一种较为通用的做法是采用“卫函数”:
/*** 假设这是A-Service的服务* 这一步很烦!!!** @param username* @return*/
public static String getDepartmentNameOfUser(String username) {
ResultTO resultTO = getUserByName(username);
if (resultTO == null) {
return "ResultTO为空";
}
User user = resultTO.getData();
if (user == null) {
return "User为空";
}
Department department = user.getDepartment();
if (department == null) {
return "Department为空";
}
return department.getName();
}
虽然避免了过深的if嵌套,逻辑稍微清晰一点,但还是很啰嗦。
封装MiniNullWrapper
我们来尝试封装一个工具类,希望能简化NullPointerException的探测工作。
核心思想
设计一个Wrapper,内部有一个T value字段,用来接收返回值,这样就能把null包裹在内部,稍微安全了一些,因为Wrapper肯定不为null:new Wrapper(value).getXxx()。
但这还不够!因为如果这个value真的是null,而外界getValue()后再次调用的话,仍然会发生NullPointerException。
怎么处理?
其实答案已经出现过了:再次把返回值包装成Wrapper即可。
public static void main(String[] agrs) {
Wrapper resultWrapper = new Wrapper(getDepartmentnameByName(username));
Wrapper userWrapper = new Wrapper(resultWrapper.get()); // resultTO可能为null,所以再次包装 Wrapper departmentWrapper = new Wrapper(userWrapper.get());
Wrapper departmentNameWrapper = new Wrapper(departmentWrapper.get());
// ...}
但上面的代码其实“很傻”:从resultWrapper取出内部的值以后,又塞进新的wrapper...其实是没有意义的,最后那个departmentNameWrapper内部塞的其实还是username
我们应该对value进行判断,当value不为null时往下取一层,这样才能最终一层层剥开value得到最终的值:
public NullWrapper map(Function super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (value是否为null)
// 如果为null,直接用Wrapper包装null,避免直接暴露导致空指针 return new Wrapper(null);
else {
// 如果不为null,那么调用传入的映射规则,【剥掉一层嵌套】,把下一级取出来重新包装为Wrapper return new Wrapper(mapper.apply(value));
}
}
由于不论value是否为null,最终返回的都是Wrapper,而Wrapper有map(),所以可以形成链式调用:
也就是说,每一次map()其实都有可能剥掉一层嵌套,而且过程中不会发生空指针异常!
// 初始化包裹,得到WrapperWrapper wrapper = new Wrapper(firstLevelValue);
// 调用Wrapper#map()尝试向下剥开每一级的嵌套,由于map()返回的也是Wrapper对象,可以链式调用wrapper
.map(firstLevelValue -> firstLevelValue.getSecondLevelValue)
.map(secondLevelValue -> secondLevelValue.getThirdLevelValue)
.map(...)
为什么是有可能?
我们来考虑两种极端的情况:如果每一级value都为null,其实每次map()都是对null进行包装传递、取出、再包装,并没有剥开任何嵌套(null值不存在嵌套)
如果每一级value都不为null,每次map()都剥开一层,这是最理想的效果,离最终需要的value越来越近
第一种情况,其实也没什么,无非就是多new几个Wrapper对象(似乎有点浪费?后面再优化)。
第二种情况,符合我们的预期,但也不能每次剥开又给套上Wrapper吧,丑媳妇最终还是要见公婆。所以,除了map()方法,Wrapper还需要额外提供一个最终获取真实value的方法,比如Wrapper#orElse(T other),它允许调用者得到未包装的value,但是!有个条件:调用orElse()时必须传一个备用的值,如果value真的为null,则返回备用值代替null(但你如果作死,备用值传null,那也没办法)。
工具类封装
上面讲述的就是Wrapper工具类的核心思想,接着让我们一起来封装一下!
public final class MiniNullWrapper {
/*** 实际值*/
private final T value;
/*** 无参构造,默认包装null*/
private MiniNullWrapper() {
this.value = null;
}
/*** 有参构造器,用来包装外部传入的value** @param value*/
private MiniNullWrapper(T value) {
this.value = value;
}
/*** 静态方法,返回一个包装了null的Wrapper** @param * @return*/
public static MiniNullWrapper empty() {
// 调用无参构造,返回包装了null的Wrapper System.out.println("由于value为null,直接返回包装了null的Wrapper,让流程继续往下");
return new MiniNullWrapper<>();
}
/*** 静态方法,返回一个包装了value的Wrapper(value可能为null)** @param value* @param * @return*/
public static MiniNullWrapper ofNullable(T value) {
// 调用有参构造,返回包装了value的Wrapper return new MiniNullWrapper<>(value);
}
/*** 核心方法:* 1.如果value为null,直接返回空的Wrapper* 2.如果value不为null,则使用mapper对value进行处理,往下剥一层(这是关键,一有机会就要往下剥一层,否则就是原地踏步)** @param mapper* @param * @return*/
public MiniNullWrapper map(Function super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (value == null)
// 按上面说的,如果value为null,我都不处理了。但为了调用者拿到返回值后不会发生空指针,需要用Wrapper包装一下 return MiniNullWrapper.empty();
else {
/** value不为null,那么就要想尽办法将它剥去一层皮。* 由于此时value不为null,即使mapper的apply方法要做的操作是 value.getXxx()/value.setXxx(),都不会空指针* mapper.apply(value)处理后的结果继续用Wrapper包装,此时【新的wrapper里的value】是处理后的数据(下一层)* */
return MiniNullWrapper.ofNullable(mapper.apply(value));
}
}
/*** 终端操作,决定勇敢一次。当你做好面对外面的世界时,就要卸下伪装:直接把value丢出去。* 但为了不祸害别人,给个备选值:other。当你确实是null时,返回other。** @param other* @return*/
public T orElse(T other) {
return value != null ? value : other;
}
// -------- 测试方法 ---------
public static void main(String[] args) {
// 全部不为null Son sonNotNull = new Son("大头儿子");
Father fatherNotNull = new Father();
fatherNotNull.setSon(sonNotNull);
GrandPa grandPaNotNull = new GrandPa();
grandPaNotNull.setFather(fatherNotNull);
// 处理grandPa,观察map()中的处理方法有没有被调用 String sonName1 = MiniNullWrapper.ofNullable(grandPaNotNull)
.map(grandPa -> grandPa.getFather())
.map(father -> father.getSon())
.map(son -> son.getName())
.orElse("没得到儿子的名字");
// 全部为null// GrandPa grandPaNull = new GrandPa();// grandPaNull.setFather(null);// // 处理grandPa,观察map(),你会发现,从grandPa取出father后,由于发现是null,所以father->father.getSon()不会执行,避免了空指针// String sonName2 = MiniNullWrapper.ofNullable(grandPaNull)// .map(grandPa -> grandPa.getFather())// .map(father -> father.getSon())// .map(son -> son.getName())// .orElse("没得到儿子的名字"); }
// ---- 没啥实质内容,就是几个简单的类,我在getter方法中打印了一些信息 ---- static class GrandPa {
private Father father;
public Father getFather() {
System.out.println("GrandPa#getFather被调用了");
return father;
}
public void setFather(Father father) {
this.father = father;
}
}
static class Father {
private Son son;
public Son getSon() {
System.out.println("Father#getSon被调用了");
return son;
}
public void setSon(Son son) {
this.son = son;
}
}
@AllArgsConstructor
static class Son {
private String name;
public String getName() {
System.out.println("Son#getName被调用了");
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
大家先把上面的代码拷到本地消化后,再重新思考下面的问题:
MiniNullWrapper.ofNullable(result)
.map(step1)
.map(step2) // 是否曾担心,在这一步突然出现null,然后空指针异常呢? .map(step3)
.orElse(step4);
学习MiniNullWrapper后,是否已经有答案了呢?
所以,当你以后使用NullWrapper的map()时,大胆地往下“剥”,不要担心中间是否会出现null导致链路中断:如果value为null,压根不会执行你传入的mapper.apply(),而是创建空的Wrapper继续往下传递!
看到了吧,我们自己封装的MiniNullWrapper已经能够比较优雅地解决空指针了,Java会想不到?所以Java8 引入了Optional这个类,它非常简单,没有任何继承体系:
核心原理和上面的MiniNullWrapper是一样的,用法也相近:
public static String getDepartmentNameOfUser(String username) {
return Optional.ofNullable(getUserByName(username))
.map(ResultTO::getData)
.map(User::getDepartment)
.map(Department::getName)
.orElse("未知部门");
}
或者:
public boolean sendMessage(Long fromId, Long toId, String message) {
// 用户校验:如果用户不存在,直接抛异常 User user = Optional.ofNullable(userService.getUserById(fromId))
.orElseThrow(() -> new BizException(ErrorEnumCode.USER_NOT_EXIST));
// 组装数据并发送...}
也可以是:
public List listSubCities(String provinceCode) {
// 查到就返回,查不到就返回替代值(对于集合而言,尽量返回空集合) return Optional.ofNullable(getCitiesByPid(provinceCode))
.orElse(new ArrayList());
}
日常开发可以试试哟。
博主自己写的Java小册已经开始出售,欢迎加入一起学习。
小册介绍:bravo1988:中级Java程序员如何进阶(小册)zhuanlan.zhihu.com