java如何避免空指针_Java入门程序员,如何避免无处不在的空指针问题?

目录

烦人的空指针

封装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

你可能感兴趣的:(java如何避免空指针)