《Java8实战》第11章 用 Optional 取代 null

news2025/1/12 12:15:31

11.1 如何为缺失的值建模

public String getCarInsuranceName(Person person) { 
 return person.getCar().getInsurance().getName(); 
} 

上面的这种代码就很容易出现NullPointerException的异常。

11.1.1 采用防御式检查减少 NullPointerException

为了避免NullPointerException异常,一般就会加很多判断。

public String getCarInsuranceName(Person person) { 
	if (person != null) { 
     Car car = person.getCar(); 
     if (car != null) { 
     		Insurance insurance = car.getInsurance(); 
         if (insurance != null) { 
         		return insurance.getName(); 
         } 
     } 
 } 
 return "Unknown"; 
} 


或者
public String getCarInsuranceName(Person person) { 
 if (person == null) { 
   return "Unknown"; 
 } 
 Car car = person.getCar(); 
 if (car == null) { 
   return "Unknown"; 
 } 
 Insurance insurance = car.getInsurance(); 
 if (insurance == null) { 
   return "Unknown"; 
 } 
 return insurance.getName(); 
} 

这种每次引用一次变量都做一次null的检查。

11.1.2 null 带来的种种问题

  • 它是错误之源。

NullPointerException 是目前 Java 程序开发中最典型的异常。

  • 它会使你的代码膨胀。

它让你的代码充斥着深度嵌套的 null 检查,代码的可读性糟糕透顶。

  • 它自身是毫无意义的。

null 自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。

  • 它破坏了 Java 的哲学。

Java 一直试图避免让程序员意识到指针的存在,唯一的例外是:null 指针。

  • 它在 Java 的类型系统上开了个口子。

null 并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题

11.1.3 其他语言中 null 的替代品

比如 Groovy,通过引入安全导航操作符(safe navigation operator,标记为?)可以安全访问可能为 null 的变量。为了理解它是如何工作的,让我们看看下面这段 Groovy代码,它的功能是获取某个用户替他的汽车保险的保险公司的名称:
def carInsuranceName = person?.car?.insurance?.name
这段代码的表述相当清晰。person 对象可能没有 car 对象,你试图通过赋一个 null 给Person 对象的 car 引用,对这种可能性建模。类似地,car 也可能没有 insurance。Groovy的安全导航操作符能够避免在访问这些可能为 null 引用的变量时抛出 NullPointerException,在调用链中的变量遭遇 null 时将 null 引用沿着调用链传递下去,返回一个 null。
关于 Java 7 的讨论中曾经建议过一个类似的功能,不过后来又被舍弃了。

11.2 Optional 类入门

Java 8 中引入了一个新的类 java.util.Optional。这是一个封装 Optional 值的类。
使用 Optional 重新定义 Person/Car/Insurance 的数据模型

public class Person { 
 private Optional<Car> car; // 人可能有汽车,也可能没有汽车,因此将这个字段声明为 Optional
 public Optional<Car> getCar() { return car; } 
} 
public class Car { 
 private Optional<Insurance> insurance; // 汽车可能进行了保险,也可能没有保险,所以将这个字段声明为 Optional 
 public Optional<Insurance> getInsurance() { return insurance; } 
} 
public class Insurance { 
 private String name; // 保险公司必须有名字
 public String getName() { return name; } 
} 

在你的代码中始终如一地使用 Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是算法上的缺陷,抑或是数据中的问题。另外,我们还想特别强调,引入 Optional 类的意图并非要消除每一个 null 引用。与此相反,它的目标是帮助你更好地设计出普适的 API,让程序员看到方法签名,就能了解它是否接受一个 Optional 的值。这种强制会让你更积极地将变量从 Optional 中解包出来,直面缺失的变量值。

11.3 应用 Optional 的几种模式

11.3.1 创建 Optional 对象

  1. 声明一个空的 Optional

通过静态工厂方法 Optional.empty 创建一个空的 Optional 对象:
Optional<Car> optCar = Optional.empty();

  1. 依据一个非空值创建 Optional

还可以使用静态工厂方法 Optional.of 依据一个非空值创建一个 Optional 对象:
Optional<Car> optCar = Optional.of(car);
如果 car 是一个 null,这段代码就会立即抛出一个 NullPointerException,而不是等到你试图访问 car 的属性值时才返回一个错误。

  1. 可接受 null 的 Optional

Optional<Car> optCar = Optional.ofNullable(car);
如果 car 是 null,那么得到的 Optional 对象就是个空对象。

11.3.2 使用 map 从 Optional 对象中提取和转换值

你可能想要从 insurance 公司对象中提取公司的名称。提取名称之前,你需要检查 insurance 对象是否为 null
Optional 也提供了一个 map 方法。它的工作方式如下
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
image.png

11.3.3 使用 flatMap 链接 Optional 对象

利用 map 重写之前的代码

Optional<Person> optPerson = Optional.of(person); 
Optional<String> name = optPerson.map(Person::getCar) 
 .map(Car::getInsurance) 
 .map(Insurance::getName); 

但这段代码无法通过编译
optPerson 是 Optional类型的变量,调用 map 方法应该没有问题。但 getCar 返回的是一个 Optional类型的对象(如代码清单 11-4 所示),这意味着 map 操作的结果是一个 Optional<Optional>类型的对象。
image.png

  1. 使用 Optional 获取 car 的保险公司名称
public String getCarInsuranceName(Optional<Person> person) { 
 return person.flatMap(Person::getCar) 
 .flatMap(Car::getInsurance) 
 .map(Insurance::getName) 
 .orElse("Unknown"); // 如果Optional的结果值为空,设置默认值
} 
  1. 使用 Optional 解引用串接的 Person/Car/Insurance 对象

image.png

11.3.4 操纵由 Optional 对象构成的 Stream

Java 9 引入了 Optional 的 stream()方法,使用该方法可以把一个含值的 Optional 对象转换成由该值构成的 Stream 对象,或者把一个空的 Optional 对象转换成等价的空 Stream。
找出 person 列表所使用的保险公司名称(不含重复项)
image.png

11.3.5 默认行为及解引用 Optional 对象

orElse方法默认值,当遭遇空的 Optional 变量时,默认值会作为该方法的调用返回值
Optional提供的变量

  • get() ,这个方法不太安全还是会抛出空指针异常的,
  • orElse(T other) 它允许你在Optional 对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends="" t=""?> other)是 orElse 方法的延迟调用版,因为 Supplier 方法只有在 Optional 对象不含值时才执行调用
  • or(Supplier<? extends=""?><? extends="" t=""?>> supplier)与前面介绍的orElseGet 方法很像,不过它不会解包 Optional 对象中的值,即便该值是存在的。
  • orElseThrow(Supplier<? extends="" x=""?> exceptionSupplier)和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow你可以定制希望抛出的异常类型
  • ifPresent(Consumer<? super="" t=""?>consumer)变量值存在时,执行一个以参数形式传入的方法,否则就不进行任何操作。

11.3.6 两个 Optional 对象的组合

方法,它接受一个 Person 和一个 Car 对象,设计一个普通版本和安全的版本

// 普通版本
public Insurance findCheapestInsurance(Person person, Car car) { 
 // 不同的保险公司提供的查询服务
 // 对比所有数据
 return cheapestCompany; 
} 

// 安全版本
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) { 
 if (person.isPresent() && car.isPresent()) { 
   return Optional.of(findCheapestInsurance(person.get(), car.get())); 
 } else { 
   return Optional.empty(); 
 } 
} 

但是这个安全版本的方法和之前的判null条件太像了

11.3.7 使用 filter 剔除特定的值

你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查保险公司的名称是否为“Cambridge-Insurance”。

常规方式
Insurance insurance = ...; 
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ 
 System.out.println("ok"); 
} 

使用Optional 对象的 filter 方法
Optional<Insurance> optInsurance = ...; 
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) 
 .ifPresent(x -> System.out.println("ok")); 

filter 方法接受一个谓词作为参数。如果 Optional 对象的值存在,并且它符合谓词的条件,filter 方法就返回其值;否则它就返回一个空的 Optional 对象。

public String getCarInsuranceName(Optional<Person> person, int minAge) { 
 return person.filter(p -> p.getAge() >= minAge) 
 .flatMap(Person::getCar) 
 .flatMap(Car::getInsurance) 
 .map(Insurance::getName) 
 .orElse("Unknown"); 
} 

image.png

11.4 使用 Optional 的实战示例

有效地使用 Optional 类意味着你需要对如何处理潜在缺失值进行全面的反思。

11.4.1 用 Optional 封装可能为 null 的值

大多数情况下,你可能希望这些方法能返回一个 Optional 对象。你无法修改这些方法的签名,但是你很容易用 Optional 对这些方法的返回值进行封装。
Object value = map.get("key");
如果没有该key,就是返回null。可以使用 Optional 封装 map 的返回值,
Optional<Object> value = **Optional.ofNullable**(map.get("key"));

11.4.2 异常与 Optional 的对比

函数无法返回某个值,这时除了返回 null,Java API 比较常见的替代做法是抛出一个异常。
典型的例子是使用静态方法 Integer.parseInt(String),将String 转换为 int。在这个例子中,如果 String 无法解析到对应的整型,该方法就抛出一个NumberFormatException。
与之前不同的是,这次你需要使用 try/catch 语句,而不是使用 if 条件判断来控制一个变量的值是否非空。
也可以用空的 Optional 对象,对遭遇无法转换的 String 时返回的非法值进行建模,这时你期望 parseInt 的返回值是一个 Optional。
我们修改不了之前的方法,但是我们可以封装一个新的工具方法

public static Optional<Integer> stringToInt(String s) { 
 try { 
	// 如果 String 能转换为对应的 Integer,将其封装在 Optional 对象中返回
   return Optional.of(Integer.parseInt(s)); 
 } catch (NumberFormatException e) { 
	//否则返回一个空的 Optional对象
   return Optional.empty(); 
 } 
}

我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用 OptionalUtility.stringToInt方法,将 String 转换为一个 Optional对象,而不再需要记得你在其中封装了笨拙的 try/catch 的逻辑了

11.4.3 基础类型的 Optional 对象,以及为什么应该避免使用它们

与 Stream 对象一样,Optional 也提供了类似的基础类型—— OptionalInt、OptionalLong 以及 OptionalDouble
有些地方可以不返回 Optional,而是直接返回一个 OptionalInt 类型的对象

11.4.4 把所有内容整合起来

Properties props = new Properties(); 
props.setProperty("a", "5"); 
props.setProperty("b", "true"); 
props.setProperty("c", "-3"); 

假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。由于一段时间必须是正数,你想要该方法符合下面的签名:
public int readDuration(Properties props, String name)
如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返回 0。采用 JUnit 的断言

assertEquals(5, readDuration(param, "a")); 
assertEquals(0, readDuration(param, "b")); 
assertEquals(0, readDuration(param, "c")); 
assertEquals(0, readDuration(param, "d")); 

readDuration的实现

传统的命令方式
public int readDuration(Properties props, String name) { 
 String value = props.getProperty(name); 
 if (value != null) { // 确保名称对应的属性存在
   try { 
// 将 String 属性转换为数字类型
     int i = Integer.parseInt(value); 
     if (i > 0) { 
       return i; 
     } 
   } catch (NumberFormatException nfe) { } 
 } 
 return 0; 
}

如果需要访问的属性值不存在,Properties.getProperty(String)方法的返回值就是一个 null,使用 ofNullable 工厂方法可以方便地将该值转换为 Optional 对象。接着,你可以向它的 flatMap 方法传递代码清单 11-7 中实现的 OptionalUtility. stringToInt 方法的引用,将 Optional转换为 Optional。最后,你非常轻易地就可以过滤掉负数。这种方式下,如果任何一个操作返回一个空的 Optional 对象,该方法都会返回 rElse 方法设置的默认值 0;否则就返回封装在 Optional 对象中的正整数。

public int readDuration(Properties props, String name) { 
 return Optional.ofNullable(props.getProperty(name)) 
 .flatMap(OptionalUtility::stringToInt) 
 .filter(i -> i > 0) 
 .orElse(0); 
}

11.5 小结

  • null 引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8 中引入了一个新的类 java.util.Optional,对存在或缺失的变量值进行建模。
  • 你可以使用静态工厂方法 Optional.empty、Optional.of 以及 Optional.ofNullable创建 Optional 对象。
  • Optional 类支持多种方法,比如 map、flatMap、filter,它们在概念上与 Stream类中对应的方法十分相似。
  • 使用 Optional 会迫使你更积极地解引用 Optional 对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
  • 使用 Optional 能帮助你设计更好的 API,用户只需要阅读方法签名,就能了解该方法是否接受一个 Optional 类型的值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/436189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】基础IO——文件操作|文件描述符|重定向|缓冲区

文章目录 一、文件操作1. 文件预备知识2. 回顾C文件操作3. 文件操作的系统调用标志位的传递openwriteread 二、文件描述符1. 文件描述符的理解2. 文件描述符的分配规则 三、重定向1. 重定向的本质2. dup2系统调用 四、缓冲区1. 缓冲区的刷新策略2. 缓冲区的位置3. 简单模拟实现…

当程序员的好处和坏处,我用七年经历来和大家聊一聊

我想和大家分享一下我做程序员这七年来的一些感受和经验&#xff0c;同时也想和大家聊一聊做程序员的好处和坏处&#xff0c;让大家真正深入了解程序员的工作&#xff0c;是不是和大家想象中的一样。 首先&#xff0c;我毕业于四川某不知名的二本院校&#xff0c;于2016年进入…

【软考备战·希赛网每日一练】2023年4月19日

文章目录 一、今日成绩二、错题总结第一题第二题第三题 三、知识查缺 题目及解析来源&#xff1a;2023年04月19日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 第二题 解析&#xff1a; server-side n.服务器端 enterprise n.企业 client n.客户 d…

matplotlib的配色(随机颜色函数,各种渐变色,彩虹色)

也是画图的时候经常会遇到的问题&#xff0c;什么颜色好看&#xff1f; 先直接上一个配色表&#xff1a; plt官网&#xff1a;List of named colors — Matplotlib 3.8.0.dev898g4f5b5741ce documentation 需要什么颜色传入就行了。 例如我下面画一个柱状图&#xff0c;自己选…

ctfhub技能树 web sql注入

1.整型注入 页面正常时 判断注入字段数 ?id1 order by 2判断注入回显位 ?id-1 union select 1,2查数据库 ?id-1 union select 1,database()库名&#xff1a;sqli 查数据表 ?id-1 union select 1,group_concat(table_name) from information_schema.tables where tabl…

kotlin协程、线程切换,函数方法委托

kotlin协程、线程切换&#xff0c;函数方法委托 一般编程的技法&#xff0c;比如&#xff0c;在Android中&#xff0c;假设在主线程中实现了一个函数&#xff0c;但该函数是耗时操作&#xff0c;毫无疑问&#xff0c;需要将这个函数的实现切入非主线程中操作&#xff0c;那么可…

Springcloud核心组件

在这里总结一下所有组件&#xff1a; springcloud是分布式微服务的一站式解决方案&#xff0c;可以说微服务是一个概念&#xff0c;而springcloud就是这个的实现 springcloud有五大核心组件&#xff1a; 注册中心 引言 由于微服务处于不同的进程&#xff0c;也就是说&…

【软考备战·希赛网每日一练】2023年4月13日

文章目录 一、今日成绩二、错题总结第一题第二题第三题第四题第五题 三、知识查缺 题目及解析来源&#xff1a;2023年04月13日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 本题有争议&#xff0c;了解即可。 第二题 解析&#xff1a; 上图UML图为…

[计算机图形学]几何:网格处理(前瞻预习/复习回顾)

一、前言 网格的三种处理&#xff1a;网格细分&#xff0c;网格简化&#xff0c;网格正则化&#xff0c;细分会产生更多的三角面片来让模型更加光滑&#xff0c;简化则相反会减少网格的三角面片数量&#xff0c;正则化则会让三角形面更加规则。如上图中最右边两幅图&#xff0…

SpringBoot监听器源码解析

1.1 创建SpringApplication对象 public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args); }SpringApplication(。。){//获取到所有配置的ApplicationListener类型的监…

Android中的多线程编程与异步处理

Android中的多线程编程与异步处理 引言 在移动应用开发中&#xff0c;用户体验是至关重要的。一个流畅、高效的应用能够吸引用户并提升用户满意度。然而&#xff0c;移动应用面临着处理复杂业务逻辑、响应用户输入、处理网络请求等多个任务的挑战。为了确保应用的性能和用户体验…

《springboot实战》第六章 实现自定义全局异常处理

前言 springboot实现自定义全局异常处理&#xff0c;以及统一返回数据。 1、分析 首先&#xff0c;实现全局异常的流程 从图中可以看到&#xff0c;实现全局异常会需要这样几个类&#xff1a; 自定义异常接口类自定义异常枚举类自定义异常类自定义异常处理类自定义全局响应…

藏在GPT背后的治理分歧:那些赞同和反对的人们|AI百态(下篇)

AGI的火种正在燎原。 一面是无可否认的AI生产力&#xff0c;正在赋能千行百业&#xff1b;而另一面&#xff0c;这团火似乎烧向了我们不可控的隐秘角落。 在《AI百态&#xff08;上篇&#xff09;——ChatGPT的“N宗罪”》中&#xff0c;我们提到监管重锤在落下&#xff0c;意大…

安装 Docker和基本操作实验文档

一、安装 Docker 目前 Docker 只能支持 64 位系统。 systemctl stop firewalld.service setenforce 0 #安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 yum-utils&#xff1a;提供了 yum-config-manager 工具。device mapper&#xff1a; 是Li…

分布式系统概念和设计-操作系统中的支持和设计

分布式系统概念和设计 操作系统支持 中间件和底层操作系统的关系&#xff0c;操作系统如何满足中间件需求。 中间件需求:访问物理资源的效率和健壮性&#xff0c;多种资源管理策略的灵活性。 任何一个操作系统的目标都是提供一个在物理层&#xff08;处理器&#xff0c;内存&a…

【网络安全】Xss漏洞

xss漏洞 xss漏洞介绍危害防御方法xss测试语句xss攻击语句1. 反射性xss2.存储型xss3.DOM型xssdvwa靶场各等级渗透方法xss反射型&#xff08;存储型方法一致&#xff09;LowMediumHightimpossible Dom型LowMediumHight xss漏洞介绍 定义&#xff1a;XSS 攻击全称跨站脚本攻击&am…

Twitter|GraphJet:推特的实时内容推荐(论文+源码解读)

以下内容具有主观性&#xff0c;有些问题的理解和回答不一定准确&#xff0c;仅供参考。翻译不确定的后面都有原文。 1.论文 1.1论文的动机是什么&#xff1f; 作者在追溯基于图推荐的系统的进化过程&#xff0c;发现了两大趋势&#xff08;更快更广&#xff09;。 趋势一是…

MySQL ,MyBatis 1.参数占位符 2. ParameterType 3. SQL 语句中特殊字符处理

1.参数占位符&#xff1a; 1&#xff09;#{}&#xff1a;执行sql时&#xff0c;会将#仔占位符替换为&#xff1f;&#xff0c;将来自动设置参数值 2&#xff09;${}&#xff1a;拼SQL。会存在SQL注入问题 3.使用时机&#xff1a; * 参数传递&#xff0c;都使用#{} *如果要对表名…

Elasticsearch:保留字段名称

作为 Elasticsearch 用户&#xff0c;我们从许多不同的位置收集数据。 我们使用 Logstash、Beats 和其他工具来抓取数据并将它们发送到 Elasticsearch。 有时&#xff0c;我们无法控制数据本身&#xff0c;我们需要管理数据的结构&#xff0c;甚至需要在摄取数据时处理字段名称…

腾讯云轻量服务器测评:4核8G12M带宽流量CPU限制说明

腾讯云轻量4核8G12M服务器446元一年&#xff0c;之前是4核8G10M配置&#xff0c;现在公网带宽和月流量包整体升级&#xff0c;系统盘为180GB SSD盘&#xff0c;每月2000GB免费流量&#xff0c;如果是选择买赠专区的4核8G12M配置是518元15个月&#xff0c;腾讯云百科来详细说下4…