提到NullPointerException(简称NPE)异常,相信每个Java开发人员都不陌生,从接触编程的第1天起,它就和我们如影随形,最近处理的线上bug中,有不少都是对象没判空导致的NullPointerException异常。

1. 简单回顾

引起NullPointerException异常的地方有很多,比如调用String的trim()方法,比如对BigDecimal进行计算时,比如将包装类型转化为基本类型时,这里简单回顾下。

假设有个导入模版定义如下:

package com.zwwhnly.springbootaction.model;

import lombok.AllArgsConstructor;
import lombok.Data; /**
* 导入模版
*/
@Data
@AllArgsConstructor
public class ImportTemplate {
/**
* 模版id
*/
private int templateId; /**
* 模版名称
*/
private String templateName; /**
* 模版下载url
*/
private String url; /**
* 备注
*/
private String remark;
}

然后看下如下代码:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
System.out.println(importTemplate.getUrl());
} public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

正常情况下,这段代码肯定是没有问题的,但当getImportTemplateById方法返回null时,这段代码就会抛出NullPointerException异常,如下所示:

public static ImportTemplate getImportTemplateById(int id) {
return null;
}

为了程序能正常运行,就要判断importTemplate是否为null,所以代码就修改为了:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}
}

项目中类似的判空代码应该有很多,大家可以自行看下自己项目的代码。

2. 使用Optional

为了避免NullPointerException异常,JDK1.8新增了Optional类来处理空指针异常,该类位于java.util包下,提供了一系列方法,

并且可以配合Lambda表达式一起使用,使代码看起来更加清晰,接下来我们看下它的使用方法。

2.1 创建实例

创建Optional实例有以下3种方式,分别为:

  1. 调用empty方法

    Optional<ImportTemplate> optionalImportTemplate = Optional.empty();
  2. 调用of方法

    ImportTemplate importTemplate = new ImportTemplate(1, "销售订单-普通商品导入模版",
    "o_w-140e3c1f41c94f238196539558e25bf7", null);
    Optional<ImportTemplate> optionalImportTemplate = Optional.of(importTemplate);
  3. 调用ofNullable方法(推荐)

    ImportTemplate importTemplate = new ImportTemplate(1, "销售订单-普通商品导入模版",
    "o_w-140e3c1f41c94f238196539558e25bf7", null);
    Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);

值得注意的是,当参数为null时,调用of方法会抛NullPointerException异常,但调用ofNullable方法不会(更符合使用场景),因此推荐使用ofNullable方法

ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.of(importTemplate);

2.2 判断是否有值

可以调用isPresent方法来判断对象是否有值(不为null),使用方法如下所示:

ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate); System.out.println(optionalImportTemplate.isPresent());

以上代码的输出结果为:

ImportTemplate importTemplate = new ImportTemplate(1, "销售订单-普通商品导入模版",
"o_w-140e3c1f41c94f238196539558e25bf7", null);
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate); System.out.println(optionalImportTemplate.isPresent());

以上代码的输出结果为:

看下isPresent的源码,逻辑非常简单,就是判断了我们传入的对象是否有值,即不为null:

/**
* Return {@code true} if there is a value present, otherwise {@code false}.
*
* @return {@code true} if there is a value present, otherwise {@code false}
*/
public boolean isPresent() {
return value != null;
}

2.3 获取值

可以调用get方法来获取对象的有值,使用方法如下所示:

ImportTemplate importTemplate = new ImportTemplate(1, "销售订单-普通商品导入模版",
"o_w-140e3c1f41c94f238196539558e25bf7", null);
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.get());

以上代码的输出结果为:

值得注意的是,当我们传入的对象为null时,调用get方法会抛出java.util.NoSuchElementException异常,而不是返回null。

ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.get());

以上代码的输出结果为:

看下get方法的源码,就可以知道原因:

public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

2.4 先用isPresent,再用get(不推荐)

然后我们回顾下文初的代码:

ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}

可能很多同学会把代码优化为下面这样的写法:

Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
if (optionalImportTemplate.isPresent()) {
System.out.println(optionalImportTemplate.get().getUrl());
}

不推荐这么使用,因为判断的地方没减少,而且还不如原来看起来清晰。

2.5 ifPresent(推荐)

那该怎么优化呢?答案就是使用ifPresent方法,该方法接收一个Consumer类型的参数,当值不为null时,就执行,当值为null时,就不执行,源码如下所示:

public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

优化之后的代码如下所示:

Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
optionalImportTemplate.ifPresent(importTemplate -> System.out.println(importTemplate.getUrl()));

当然,也可以写更多的逻辑:

Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
optionalImportTemplate.ifPresent(importTemplate -> {
System.out.println(importTemplate.getTemplateId());
System.out.println(importTemplate.getTemplateName());
System.out.println(importTemplate.getUrl());
System.out.println(importTemplate.getRemark());
});

2.6 自定义默认值

Optional类提供了以下2个方法来自定义默认值,用于当对象为null时,返回自定义的对象:

  1. orElse
  2. orElseGet

先来看下orElse方法的使用:

public static void main(String[] args) {
ImportTemplate importTemplate = null;
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElse(getDefaultTemplate());
System.out.println(firstImportTemplate); importTemplate = new ImportTemplate(2, "销售订单-不定规格商品导入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null); ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElse(getDefaultTemplate());
System.out.println(secondImportTemplate);
} public static ImportTemplate getDefaultTemplate() {
System.out.println("getDefaultTemplate");
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

输出结果:

再来看下orElseGet方法的使用:

public static void main(String[] args) {
ImportTemplate importTemplate = null;
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElseGet(() -> getDefaultTemplate());
System.out.println(firstImportTemplate); importTemplate = new ImportTemplate(2, "销售订单-不定规格商品导入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null); ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElseGet(() -> getDefaultTemplate());
System.out.println(secondImportTemplate);
} public static ImportTemplate getDefaultTemplate() {
System.out.println("getDefaultTemplate");
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

输出结果:

从输出结果看,2个方法好像差不多,第1次调用都返回了默认模版,第2次调用都返回了传入的模版,但其实仔细观察,你会发现当使用

orElse方法时,getDefaultTemplate方法执行了2次,但调用orElseGet方法时,getDefaultTemplate方法只执行了2次(只在第1次传入模版为null时执行了)。

为什么会这样呢?带着这个疑问,我们看下这2个方法的源码,其中orElse方法的源码如下所示:

public T orElse(T other) {
return value != null ? value : other;
}

可以看到,参数other是个对象,这个参数肯定是要传的,但只有value为空时,才会用到(返回)这个对象。

orElseGet方法的源码如下所示:

public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

可以看到,参数other并不是直接传入对象,如果value为null,才会执行传入的参数获取对象,如果不为null,直接返回value。

2.7 自定义异常

Optional类提供了orElseThrow方法,用于当传入的对象为null时,抛出自定义的异常,使用方法如下所示:

public static void main(String[] args) {
ImportTemplate importTemplate = new ImportTemplate(2, "销售订单-不定规格商品导入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null);
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElseThrow(() -> new IndexOutOfBoundsException());
System.out.println(firstImportTemplate); importTemplate = null; ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElseThrow(() -> new IndexOutOfBoundsException());
System.out.println(secondImportTemplate);
}

输出结果:

2.8 过滤数据

Optional类提供了filter方法来过滤数据,该方法接收一个Predicate参数,返回匹配条件的数据,如果不匹配条件,返回一个空的Optional,使用方法如下所示:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<ImportTemplate> filterById = Optional.ofNullable(importTemplate)
.filter(f -> f.getTemplateId() == 1);
System.out.println(filterById.isPresent()); Optional<ImportTemplate> filterByName = Optional.ofNullable(importTemplate)
.filter(f -> f.getTemplateName().contains("发货单"));
System.out.println(filterByName.isPresent());
} public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

输出结果:

2.9 转换值

Optional类提供了以下2个方法来转换值:

  1. map
  2. flatMap

map方法的使用方法如下所示:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<String> optionalUrl = Optional.ofNullable(importTemplate)
.map(f -> "url:" + f.getUrl());
System.out.println(optionalUrl.isPresent());
System.out.println(optionalUrl.get());
} public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

输出结果:

flatMap方法和map方法类似,不过它支持传入Optional,使用方法如下所示:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<String> optionalUrl = Optional.ofNullable(importTemplate)
.flatMap(f -> Optional.ofNullable(f.getUrl()));
System.out.println(optionalUrl.isPresent());
System.out.println(optionalUrl.get());
} public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "销售订单-普通商品导入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}

输出结果:

3. 总结

对于程序员来说,一不注意就会出现NullPointerException异常,避免它的方式也很简单,比如使用前判断不能为空:

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}
}

比如为空时,直接返回(或者返回默认值):

public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate == null) {
return;
}
System.out.println(importTemplate.getUrl());
}

比如,使用本文中的Optional。

使用哪种方式不重要,尽可能地避免NullPointerException异常才重要。

4. 参考

理解、学习与使用 Java 中的 Optional

Java Optional使用指南的更多相关文章

  1. Java学习笔记(四)——google java编程风格指南(上)

    [前面的话] 年后开始正式上班,计划着想做很多事情,但是总会有这样那样的打扰,不知道是自己要求太高还是自我的奋斗意识不够?接下来好好加油.好好学学技术,好好学习英语,好好学习做点自己喜欢的事情,趁着自 ...

  2. Java学习笔记(五)——google java编程风格指南(中)

    [前面的话] 年后开始正式上班,计划着想做很多事情,但是总会有这样那样的打扰,不知道是自己要求太高还是自我的奋斗意识不够?接下来好好加油.好好学学技术,好好学习英语,好好学习做点自己喜欢的事情,趁着自 ...

  3. Java学习笔记(六)——google java编程风格指南(下)

    [前面的话] 年后开始正式上班,计划着想做很多事情,但是总会有这样那样的打扰,不知道是自己要求太高还是自我的奋斗意识不够?接下来好好加油.好好学学技术,好好学习英语,好好学习做点自己喜欢的事情,趁着自 ...

  4. Java多线程初学者指南系列教程

    转自:http://developer.51cto.com/art/200911/162925.htm 51cto 本系列来自NokiaGuy的“真的有外星人吗”博客,系列名称为<Java多线程 ...

  5. 【Todo】【读书笔记】Java多线程编程指南-设计模式篇

    下了这本书<Java多线程编程指南-设计模式篇>, 还有另一本<JAVA多线程设计模式>,据说内容有重复,结合着看.

  6. Java工程师学习指南 完结篇

    Java工程师学习指南 完结篇 先声明一点,文章里面不会详细到每一步怎么操作,只会提供大致的思路和方向,给大家以启发,如果真的要一步一步指导操作的话,那至少需要一本书的厚度啦. 因为笔者还只是一名在校 ...

  7. Java工程师学习指南 中级篇

    Java工程师学习指南 中级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我写的文章都是站 ...

  8. Java工程师学习指南 初级篇

    Java工程师学习指南 初级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

  9. Java工程师学习指南 入门篇

    Java工程师学习指南 入门篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

随机推荐

  1. .NET Core 3.0或3.1 类库项目中引用 Microsoft.AspNetCore.App

    本文为原创文章.首发:http://www.zyiz.net/ 在 ASP.NET Core 3.0+ web 项目中已经不需要在 .csproj 中添加对 Microsoft.AspNetCore. ...

  2. 任务调度框架Quartz快速入门!

    目录 Quartz是什么 Quartz中的重要API及概念 超重要API 重要概念 Quartz设计理念:为什么设计Job和Trigger? 最简单的Quartz使用案例 Job实例和JobDetai ...

  3. 看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap

    引言 面试官: 小伙子你有点眼熟啊,是不是去年来这面试过啊. 二胖: 啊,没有啊我这是第一次来这. 面试官: 行,那我们开始今天的面试吧,刚开始我们先来点简单的吧,java里面的容器你知道哪些啊,跟我 ...

  4. Liunx运维(七)-用户管理及用户信息查询命令

    文档目录: 一.useradd:创建用户 二.usermod:修改用户信息 三.userdel:删除用户 四.groupadd:创建新的用户组 五.groupdel:删除用户组 六.passwd:修改 ...

  5. 痞子衡嵌入式:MCUXpresso IDE下添加新路径下源文件进工程编译的方法

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是MCUXpresso IDE下添加新路径下源文件进工程编译的方法. 接着上篇文章 <MCUXpresso IDE下SDK工程导入与 ...

  6. Java GC --- Java堆内存

    Java堆是被所有线程共享的一块内存区域,所有对象实例和数组都在堆上进行内存分配.为了进行高效的垃圾回收,虚拟机把堆内存划分成: 1. 新生代(Young Generation): 由 Eden 与 ...

  7. 每日一个linux命令2

    cd命令 Linux cd命令可以说是Linux中最基本的命令语句,其他的命令语句要进行操作,都是建立在使用cd命令的基础之上. 1. 命令格式 cd [目录名] 2.命令功能 切换当前目录至dirN ...

  8. Redis缓存篇(二)淘汰机制:缓存满了怎么办?

    上一讲提到,缓存的容量总是小于后端数据库的.随着业务系统的使用,缓存数据会撑满内存空间,该怎么处理呢? 本节我们来学习内存淘汰机制.在Redis 4.0之前有6种内存淘汰策略,之后又增加2种,一共8种 ...

  9. Java中常用修饰符浅谈

    一.public.protected.default和private修饰符的作用域 public:在java程序中,如果将属性和方法定义为 public 类型,那么此属性和方法所在的类和及其子类,同一 ...

  10. [ABP教程]第七章 作者:数据库集成

    Web开发教程7 作者:数据库集成 关于此教程 在这个教程系列中,你将要构建一个基于ABP框架的应用程序 Acme.BookStore.这个应用程序被用于甘丽图书页面机器作者.它将用以下开发技术: E ...