查看原文
其他

再见!该死的NullPointException

终码一生 2022-09-22

The following article is from 架构悟道 Author veezean


目录
  • 背景

  • null 的困扰

  • Optional 应对 null

  • 使用抛异常替代 return null

  • JDK 与开源框架的实践

  • 总结


背景


NullPointException 应该算是每一个码农都很熟悉的家伙了吧?谁的代码不曾抛过几个空指针异常呢…


比如你写了段下面的代码:
public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    Company company = employee.getTeam().getDepartment().getCompany();
    System.out.println(company);
}

private Employee getEmployee() {
    Employee employee = new Employee();
    employee.setEmployeeName("JiaGouWuDao");
    employee.setTeam(new Team("DevTeam4"));
    return employee;
}


运行程序,你可能就等不到你需要的结果,而是要喜提 NullPointException了…

作为 Java 开发中最典型的异常类型,甚至可能是很多程序员入行之后收到的第一份异常大礼包类型。


NullPointException 也似乎成为了一种魔咒,迫使程序员在敲出的每一行代码的时候都需要去思考下是否需要去做一下判空操作,久而久之,代码中便充斥着大量的 null 检查逻辑。


于是呢,上面的代码会变成下面这样:
public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    if (employee == null) {
        // do something here...
        return;
    }
    Team team = employee.getTeam();
    if (team == null) {
        // do something here...
        return;
    }
    Department department = team.getDepartment();
    if (department == null) {
        // do something here...
        return;
    }
    Company company = department.getCompany();
    System.out.println(company);
}


是不是大家的项目中都有见过这种写法的?每行代码中都流露着对 NullPointException 的恐惧有木有?是不是像极了一颗被深深伤害过的心在小心翼翼的保护着自己?

null 的困扰


通过上面代码示例,我们可以发现使用 null 可能会带来的一系列困扰:

  • 空指针异常,导致代码运行时变得不可靠,稍不留神可能就崩了

  • 使代码膨胀,导致代码中充斥大量的 null 检查与保护,使代码可读性降低
此外,null 还有一个明显的弊端:含义不明确,比如一个方法返回了 null,调用方不清楚到底是因为逻辑有问题导致为 null,还是说 null 其实也是一种可以接受的正常返回值类型?

所以说,一个比较好的编码习惯,是尽量避免在程序中使用 null,可以按照具体的场景分开区别对待:

  • 确定是因为代码或者逻辑层面处理错误导致的无值,通过 throw 异常的方式,强制调用方感知并进行处理对待

  • 如果 null 代表业务上的一种正常可选值,可以考虑返回 Optional 来替代。


当然咯,有时候即使我们自己的代码不返回 null,也难免会遇到调用别人的接口返回 null 的情况,这种时候我们真的就只能不停的去判空来保护自己吗?


有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下 null 的一些优雅应对策略。


Optional 应对 null


| Optional 一定比 return null 安全吗

前面我们提到了说使用 Optional 来替代 null,减少调用端的判空操作压力,防止调用端出现空指针异常。


那么,使用返回 Optional 对象就一定会比 return null 更靠谱吗?答案是:也不一定,关键要看怎么用!


比如:下面的代码,getContent() 方法返回了个 Optional 对象,然后 testCallOptional() 方法作为调用方,获取到返回值后的操作方式:
public void testCallOptional() {
    Optional<Content> optional = getContent();
    System.out.println("-------下面代码会报异常--------");
    try {
        // 【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果
        Content content = optional.get();
        System.out.println(content);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("-------上面代码会报异常--------");
}

private Optional<Content> getContent() {
    return Optional.ofNullable(null);
}


上述代码运行之后会发现报错了:
-------下面代码会报异常--------
java.util.NoSuchElementExceptionNo value present
    at java.util.Optional.get(Optional.java:135)
    at com.veezean.skills.optional.OptionalService.testCallOptional(OptionalService.java:47)
    at com.veezean.skills.optional.OptionalService.main(OptionalService.java:58)
-------上面代码会报异常--------


既然直接调用 Optional.get() 报错,那就是调用前加个判断就好咯?
public void testCallOptional2() {
    Optional<Content> optional = getContent();
    // 使用前先判断下元素是否存在
    if (optional.isPresent()) {
        Content content = optional.get();
        System.out.println(content);
    }
}


执行一下,果然不报错了。但是,这样真的就是解决方法吗?这样跟直接返回 null 然后使用前判空(下面的写法)其实也没啥区别,也并不会让调用方使用起来更加的优雅与靠谱:
public void testNullReturn2() {
    Content content = getContent2();
    if (content != null) {
        System.out.println(content.getValue());
    }
}


那怎么样才是正确的使用方式呢,下面一起来看下。


| 全面认识下 Optional

①创建 Optional 对象


Optional<T> 对象,可以用来表示一个 T 类型对象的封装,或者也可以表示不是任何对象。


Optional 类提供了几个静态方法供对象的构建:
在项目中,我们可以选择使用上面的方法,实现 Optional 对象的封装:
public void testCreateOptional() {
    // 使用Optional.of构造出具体对象的封装Optional对象
    System.out.println(Optional.of(new Content("111","JiaGouWuDao")));
    // 使用Optional.empty构造一个不代表任何对象的空Optional值
    System.out.println(Optional.empty());
    System.out.println(Optional.ofNullable(null));
    System.out.println(Optional.ofNullable(new Content("222","JiaGouWuDao22")));
}


输出结果:
Optional[Content{id='111'value='JiaGouWuDao'}]
Optional.empty
Optional.empty
Optional[Content{id='222'value='JiaGouWuDao22'}]


这里需要注意下 of 方法如果传入 null 会抛空指针异常,所以比较建议大家使用 ofNullable 方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:


| Optional 常用方法理解

在具体讨论应该如何正确使用 Optional 的方法前,先来了解下 Optional 提供的一些方法:

看到这里的 map 与 flatMap 方法,不知道大家会不会联想到 Stream 流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透 JAVA 的 Stream 流操作)?


的确,它们的作用也是类似的,都是用来将一个对象处理转换为另一个对象类型的:
对于 Optional 而言,map 与 flatMap 最终的实现效果其实都是一样的,仅仅只是入参的要求不一样,也即两种不同写法,两者区别点可以通过下图来理解:
实际使用的时候,可以根据需要选择使用 map 或者 flatMap:
public void testMapAndFlatMap() {
    Optional<User> userOptional = getUser();
    Optional<Employee> employeeOptional = userOptional.map(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map与flatMap的区别点:此处return的是具体对象类型
        return employee;
    });
    System.out.println(employeeOptional);

    Optional<Employee> employeeOptional2 = userOptional.flatMap(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map与flatMap的区别点:此处return的是具体对象的Optional封装类型
        return Optional.of(employee);
    });
    System.out.println(employeeOptional2);
}


从输出结果可以看出,两种不同的写法,实现是相同的效果:
Optional[Employee(employeeName=JiaGouWuDao)]
Optional[Employee(employeeName=JiaGouWuDao)]


| Optional 使用场景

①减少繁琐的判空操作


再回到本篇文章最开始的那段代码例子,如果我们代码里面不去逐个做判空保护的话,我们可以如何来实现呢?


看下面的实现思路:
public void getCompanyFromEmployeeTest() {
    Employee employeeDetail = getEmployee();
    String companyName = Optional.ofNullable(employeeDetail)
            .map(employee -> employee.getTeam())
            .map(team -> team.getDepartment())
            .map(department -> department.getCompany())
            .map(company -> company.getCompanyName())
            .orElse("No Company");
    System.out.println(companyName);
}


先通过 map 的方式一层一层的去进行类型转换,最后使用 orElse 去获取 Optional 中最终处理后的值,并给定了数据缺失场景的默认值。是不是看着比一堆 if 判空操作要舒服多了?


适用场景:需要通过某个比较长的调用链路一层一层去调用获取某个值的时候,使用上述方法,可以避免空指针以及减少冗长的判断逻辑。


②需要有值兜底的数据获取场景


编码的时候,经常会遇到一些数据获取的场景,需要先通过一些处理逻辑尝试获取一个数据,如果没有获取到需要的数据,还需要返回一个默认值,或者是执行另一处理逻辑继续尝试获取。


比如从请求头中获取客户端 IP 的逻辑,按照常规逻辑,代码会写成下面这样:
public String getClientIp(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    if (!StringUtils.isEmpty(clientIp)) {
        return clientIp;
    }
    clientIp = request.getHeader("X-Real-IP");
    return clientIp;
}


但是借助 Optional 来实现,可以这样写:
public String getClientIp2(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    return Optional.ofNullable(clientIp).orElseGet(() -> request.getHeader("X-Real-IP"));
}


适用场景:优先执行某个操作尝试获取数据,如果没获取到则去执行另一逻辑获取,或者返回默认值的场景。


③替代可能为 null 的方法返回值


下面是一段从项目代码中截取的片段:
public FileInfo queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return new FileInfo(entity.getName(), entity.getFilePath(), false);
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true);
    }
    return null;
}


可以看到最终的 return 分支中,有一种可能会返回 null,这个方法作为项目中被高频调用的一个方法,意味着所有的调用端都必须要做判空保护。


可以使用 Optional 进行优化处理:
public Optional<FileInfo> queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return Optional.ofNullable(new FileInfo(entity.getName(), entity.getFilePath(), false));
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return Optional.ofNullable(new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true));
    }
    return Optional.empty();
}


这样的话,就可以有效的防止调用端踩雷啦~


适用场景:实现某个方法的时候,如果方法的返回值可能会为 null,则考虑将方法的返回值改为 Optional 类型,原先返回 null 的场景,使用 Optional.empty() 替代。


④包装数据实体中非必须字段


首先明确一下,Optional 的意思是可选的,也即用于标识下某个属性可有可无的特性。啥叫可有可无?


看下面代码:
public class PostDetail {
    private String title;
    private User postUser;
    private String content;
    private Optional<Date> lastModifyTime = Optional.empty();
    private Optional<Attachment> attachment = Optional.empty();
}


上面是一个帖子详情数据类,对于一个论坛帖子数据而言,帖子的标题、内容、发帖人这些都是属于必须的字段,而帖子的修改时间、帖子的附件其实是属于可选字段(因为不是所有的帖子都会被修改、也不是所有帖子都会带附件),所以针对这种可有可无的字段,就可以声明定义的时候使用 Optional 进行封装。

使用 Optional 进行封装之后有两个明显的优势:

  • 强烈的业务属性说明,明确的让人知晓这个是一个可选字段,等同于数据库建表语句里面设置 nullable 标识一样的效果。

  • 调用端使用的时候也省去了判空操作。


适用场景:数据实体定义的时候,对于可选参数,采用 Optional 封装类型替代。


使用抛异常替代 return null


相比于返回一个 Optional 封装的对象,直接抛异常具有强烈的警醒意味,意在表达此处存在预期之外的不合理情况,要求编码的时候,调用端必须要予以专门处理。
public Team getTeamInfo() throws TestException {
    Employee employee = getEmployee();
    Team team = employee.getTeam();
    if (team == null) {
        throw new TestException("team is missing");
    }
    return team;
}


相比直接 return null,显然抛异常的含义更加明确。


JDK 与开源框架的实践


JDK 提供的很多方法里面,其实都是遵循着本文中描述的这种返回值处理思路的,很少会看到直接返回 null 的——不止 JDK,很多大型的开源框架源码中,也很少会看到直接 return null 的情况。


比如 com.sun.jmx.snmp.agent 中的一段代码:
public SnmpMibSubRequest nextElement() throws NoSuchElementException  {
    if (iter == 0) {
        if (handler.sublist != null) {
            iter++;
            return hlist.getSubRequest(handler);
        }
    }
    iter ++;
    if (iter > size) throw new NoSuchElementException();
    SnmpMibSubRequest result = hlist.getSubRequest(handler,entry);
    entry++;
    return result;
}


再比如 Spring 中 org.springframework.data.jpa.repository.support 包下面的方法例子:
public Optional<T> findById(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    Class<T> domainType = getDomainClass();
    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }
    LockModeType type = metadata.getLockModeType();
    Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}


总结


好啦,关于编码中对 null 的一些应对处理策略与思路呢,这里就给大家分享到这里,希望可以对大家有所启发,通过不断的细节优化与改进,最终摆脱被空指针摆布的局面~


那么,对上面提到的一些内容与场景,你是否也有遇到相关的情况呢?你是怎么处理的呢?欢迎多切磋交流下~


此外:关于本文中涉及的演示代码的完整示例,我已经整理并提交到 github 中,如果您有需要,可以自取:
https://github.com/veezean/JavaBasicSkills

PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。


往期推荐



替代ELK:ClickHouse+Kafka+FlieBeat

面试官:如何防止你的 jar 包被反编译?

DataGrip 2022.2 新版来袭,界面更大气了

Spring Batch批处理框架,真心强啊!!

如何设计一个高质量的 API 接口

VS Code Java 7 月更新:Lombok 支持重大提升, Spring 增强新功能!

使用uuid做MySQL主键,被老板,爆怼一顿!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存