大家好,我是Evan。
本文主要内容如下:
一、前置基础
Optional类源码大量使用到:
1.四大函数式接口
2.lambda表达式
二、什么是Optional
1.Java 8新增了一个类 - Optional
2.Optional是一个容器,用于放置可能为空的值,它可以合理而优雅的处理 null。
3.Optional的本质,就是内部储存了一个真实的值,在构造的时候,就直接判断其值是否为空
4.java.util.Optional<T>类本质上就是一个容器,该容器的数值可以是空代表一个值不存在,也可以是非空代表一个值存在。
5.Optional类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。
2.1理论拓展
Monad 是一种用于处理副作用的编程模式,简单来说就是将一些可能产生副作用的操作封装起来,并在特定的作用域内执行,控制其对程序产生的影响。在函数式编程中经常使用 Monad 模式来处理一些副作用,如 IO 操作、异常处理、状态管理等。
Optional 是 Java 中一个非常典型的 Monad 实现,它的主要作用是避免空指针异常并对可能为空的对象进行封装,并提供一系列函数式的操作,如 map()
、filter()
、flatMap()
等方法,使代码更加健壮、优雅和安全。就像我们平时经常对空值进行判空处理一样,Optional 提供了一种更加优美和方便的方式,避免了深层次的嵌套判空,同时增加了代码的可读性和可维护性。
对于函数式编程和 Monad 模式来说,这种方式是非常重要的,因为随着程序的规模增大,副作用也会越来越多,这时候避免副作用对程序的影响就变得尤为重要。通过使用 Monad 模式和类似 Optional 这样的容器类型,我们可以更好地控制副作用,使程序更加稳定和可靠。
三、为什么要用Optional
1.要是用来解决程序中常见的 NullPointerException异常问题。但是在实际开发过程中很多人都是在一知半解的使用 Optional,类似 if (userOpt.isPresent()){...}这样的代码随处可见。如果是这样我更愿意看到老老实实的 null 判断,这样强行使用 Optional反而增加了代码的复杂度。
2.这是一个明确的警示,用于提示开发人员此处要注意null值。
3.不显式的判空,当出现俄罗斯式套娃判空时,代码处理上更加优雅。
4.使用 Optional 有时候可以很方便的过滤一些属性,而且它的方法可以通过链式调用,方法间相互组合使用,使我们用少量的代码就能完成复杂的逻辑。
5.防止空指针(NPE)、简化if...else...判断、减少代码圈复杂度
6.Optional 之所以可以解决 NPE 的问题,是因为它明确的告诉我们,不需要对它进行判空。它就好像十字路口的路标,明确地告诉你该往哪走
7.很久很久以前,为了避免 NPE,我们会写很多类似if (obj != null) {}的代码,有时候忘记写,就可能出现 NPE,造成线上故障。在 Java 技术栈中,如果谁的代码出现了 NPE,有极大的可能会被笑话,这个异常被很多人认为是低级错误。Optional的出现,可以让大家更加轻松的避免因为低级错误被嘲讽的概率。
8.第一是改变我们传统判空的方式(其实就是帮我们包装了一层,判空的代码帮我们写了),用函数式编程和申明式编程来进行对基本数据的校验和处理。第二就是声明式的编程方式对阅读代码的人更友好。
3.1俄罗斯式套娃判空详解
手动进行 if(obj!=null)的判空自然是最全能的,也是最可靠的,但是怕就怕俄罗斯套娃式的 if判空。
举例一种情况:
为了获取:省(Province)→市(Ctiy)→区(District)→街道(Street)→道路名(Name)
作为一个“严谨且良心”的后端开发工程师,如果手动地进行空指针保护,我们难免会这样写:
public String getStreetName( Province province ) {
if( province != null ) {
City city = province.getCity();
if( city != null ) {
District district = city.getDistrict();
if( district != null ) {
Street street = district.getStreet();
if( street != null ) {
return street.getName();
}
}
}
}
return "未找到该道路名";
}
为了获取到链条最终端的目的值,直接链式取值必定有问题,因为中间只要某一个环节的对象为 null,则代码一定会炸,并且抛出 NullPointerException异常,然而俄罗斯套娃式的 if判空实在有点心累。
Optional接口本质是个容器,你可以将你可能为 null的变量交由它进行托管,这样我们就不用显式对原变量进行 null值检测,防止出现各种空指针异常。
Optional语法专治上面的俄罗斯套娃式 if 判空,因此上面的代码可以重构如下:
public String getStreetName( Province province ) {
return Optional.ofNullable( province )
.map( i -> i.getCity() )
.map( i -> i.getDistrict() )
.map( i -> i.getStreet() )
.map( i -> i.getName() )
.orElse( "未找到该道路名" );
}
漂亮!嵌套的 if/else判空灰飞烟灭!
解释一下执行过程:
ofNullable(province ) :它以一种智能包装的方式来构造一个 Optional实例, province是否为 null均可以。如果为 null,返回一个单例空 Optional对象;如果非 null,则返回一个 Optional包装对象
map(xxx ):该函数主要做值的转换,如果上一步的值非 null,则调用括号里的具体方法进行值的转化;反之则直接返回上一步中的单例 Optional包装对象
orElse(xxx ):很好理解,在上面某一个步骤的值转换终止时进行调用,给出一个最终的默认值
四、Optional基本知识
Optional类常用方法:
Optional.of(T t) : 创建一个 Optional 实例。
Optional.empty() : 创建一个空的 Optional 实例。
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例。
isPresent() : 判断是否包含值。
orElse(T t) : 如果调用对象包含值,返回该值,否则返回t。
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值。
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()。
flatMap(Function mapper):与 map 类似,要求返回值必须是Optional。
4.1API的思考
1.of(T value)
一个东西存在那么自然有存在的价值。当我们在运行过程中,不想隐藏NullPointerException。
而是要立即报告,这种情况下就用Of函数。但是不得不承认,这样的场景真的很少。我也仅在写junit测试用例中用到过此函数。
2.get()
直观从语义上来看,get() 方法才是最正宗的获取 Optional 对象值的方法,
但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖。
五、工作中如何正确使用Optional
5.1 orElseThrow
orElseThrow()方法当遇到一个不存在的值的时候,并不返回一个默认值,而是抛出异常。
public void validateRequest(String requestId) {
Optional.ofNullable(requestId)
.orElseThrow(() -> new IllegalArgumentException("请求编号不能为空"));
// 执行后续操作
}
Optional<User> optionalUser = Optional.ofNullable(null);
User user = optionalUser.orElseThrow(() -> new RuntimeException("用户不存在"));
// 传入 null 参数,获取一个 Optional 对象,并使用 orElseThrow 方法
try {
Optional optional2 = Optional.ofNullable(null);
Object object2 = optional2.orElseThrow(() -> {
System.out.println("执行逻辑,然后抛出异常");
return new RuntimeException("抛出异常");
}
);
System.out.println("输出的值为:" + object2);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
5.2 filter
接收一个函数式接口,当符合接口时,则返回一个Optional对象,否则返回一个空的Optional对象。
例如,我们需要过滤出年龄在25岁到35岁之前的人群,那在Java8之前我们需要创建一个如下的方法来检测每个人的年龄范围是否在25岁到35岁之前。
public boolean filterPerson(Peron person){
boolean isInRange = false;
if(person != null && person.getAge() >= 25 && person.getAge() <= 35){
isInRange = true;
}
return isInRange;
}
public boolean filterPersonByOptional(Peron person){
return Optional.ofNullable(person)
.map(Peron::getAge)
.filter(p -> p >= 25)
.filter(p -> p <= 35)
.isPresent();
}
使用Optional看上去就清爽多了,这里,map()仅仅是将一个值转换为另一个值,并且这个操作并不会改变原来的值。
public class OptionalMapFilterDemo {
public static void main(String[] args) {
String password = "password";
Optional<String> opt = Optional.ofNullable(password);
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent();
System.out.println(result);
}
}
5.3 orElse和orElseGet
结论:当optional.isPresent() == false时,orElse()和orElseGet()没有区别;
而当optional.isPresent() == true时,无论你是否需要,orElse始终会调用后续函数。
若方法不是纯计算型的,使用Optional的orElse(T);
若有与数据库交互或者远程调用的,都应该使用orElseGet(Supplier)。
推荐使用orElseGet ,当存在一些复合操作,远程调用,磁盘io等大开销的动作禁止使用orElse。
原因:当value不为空时,orElse仍然会执行。
public class GetValueDemo {
public static String getDefaultName() {
System.out.println("Getting Default Name");
return "binghe";
}
public static void main(String[] args) {
/* String text = null;
System.out.println("Using orElseGet:");
String defaultText = Optional.ofNullable(text).orElseGet(GetValueDemo::getDefaultName);
assertEquals("binghe", defaultText);
System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(GetValueDemo.getDefaultName());
assertEquals("binghe", defaultText);*/
// TODO: 2023/5/13 重点示例
String name = "binghe001";
System.out.println("Using orElseGet:");
String defaultName = Optional.ofNullable(name).orElseGet(GetValueDemo::getDefaultName);
assertEquals("binghe001", defaultName);
System.out.println("Using orElse:");
defaultName = Optional.ofNullable(name).orElse(getDefaultName());
assertEquals("binghe001", defaultName);
}
}
运行结果如下所示。
Using orElseGet:
Using orElse:
Getting default name...
可以看到,当使用orElseGet()方法时,getDefaultName()方法并不执行,因为Optional中含有值,而使用orElse时则照常执行。所以可以看到,当值存在时,orElse相比于orElseGet,多创建了一个对象。如果创建对象时,存在网络交互,那系统资源的开销就比较大了,这是需要我们注意的一个地方。
5.4 map和flatMap
String len = null;
Integer integer = Optional.ofNullable(len)
.map(s -> s.length())
.orElse(0);
System.out.println("integer = " + integer);
Person person = new Person("evan", 18);
Optional.ofNullable(person)
.map(p -> p.getName())
.orElse("");
Optional.ofNullable(person)
.flatMap(p -> Optional.ofNullable(p.getName()))
.orElse("");
注意:方法getName返回的是一个Optional对象,如果使用map,我们还需要再调用一次get()方法,而使用flatMap()就不需要了。
5.5 项目实战
实战一
public class OptionalExample {
/**
* 测试的 main 方法
*/
public static void main(String[] args) {
// 创建一个测试的用户集合
List<User> userList = new ArrayList<>();
// 创建几个测试用户
User user1 = new User("abc");
User user2 = new User("efg");
User user3 = null;
// 将用户加入集合
userList.add(user1);
userList.add(user2);
userList.add(user3);
// 创建用于存储姓名的集合
List<String> nameList = new ArrayList();
List<User> nameList03 = new ArrayList();
List<String> nameList04 = new ArrayList();
// 循环用户列表获取用户信息,值获取不为空且用户以 a 开头的姓名,
// 如果不符合条件就设置默认值,最后将符合条件的用户姓名加入姓名集合
/*
for (User user : userList) {
nameList.add(Optional.ofNullable(user).map(User::getName).filter(value -> value.startsWith("a")).orElse("未填写"));
}
*/
// 输出名字集合中的值
/* System.out.println("通过 Optional 过滤的集合输出:");
System.out.println("nameList.size() = " + nameList.size());
nameList.stream().forEach(System.out::println);*/
/* Optional.ofNullable(userList)
.ifPresent(u -> {
for (User user : u) {
nameList04.add(Optional.ofNullable(user).map(User::getName).filter(f -> f.startsWith("e")).orElse("无名"));
}
});*/
Optional.ofNullable(userList)
.ifPresent(u -> {
u.forEach(m->{
Optional<String> stringOptional = Optional.ofNullable(m).map(User::getName).filter(f -> f.startsWith("a"));
stringOptional.ifPresent(nameList04::add);
});
});
System.out.println("nameList04.size() = " + nameList04.size());
nameList04.forEach(System.err::println);
Optional.ofNullable(userList).ifPresent(nameList03::addAll);
System.out.println("nameList03.size() = " + nameList03.size());
nameList03.stream().forEach(System.err::println);
}
}
实战二
以前写法
public String getCity(User user) throws Exception{
if(user!=null){
if(user.getAddress()!=null){
Address address = user.getAddress();
if(address.getCity()!=null){
return address.getCity();
}
}
}
throw new Excpetion("取值错误");
}
public String getCity(User user) throws Exception{
return Optional.ofNullable(user)
.map(u-> u.getAddress())
.map(a->a.getCity())
.orElseThrow(()->new Exception("取指错误"));
}
实战三 简化if.else
以前写法
public User getUser(User user) throws Exception{
if(user!=null){
String name = user.getName();
if("zhangsan".equals(name)){
return user;
}
}else{
user = new User();
user.setName("zhangsan");
return user;
}
}
java8写法
public User getUser(User user) {
return Optional.ofNullable(user)
.filter(u->"zhangsan".equals(u.getName()))
.orElseGet(()-> {
User user1 = new User();
user1.setName("zhangsan");
return user1;
});
}
实战四 解决checkStyle问题
BaseMasterSlaveServersConfig smssc = new BaseMasterSlaveServersConfig();
if (clientName != null) {
smssc.setClientName(clientName);
}
if (idleConnectionTimeout != null) {
smssc.setIdleConnectionTimeout(idleConnectionTimeout);
}
if (connectTimeout != null) {
smssc.setConnectTimeout(connectTimeout);
}
if (timeout != null) {
smssc.setTimeout(timeout);
}
if (retryAttempts != null) {
smssc.setRetryAttempts(retryAttempts);
}
if (retryInterval != null) {
smssc.setRetryInterval(retryInterval);
}
if (reconnectionTimeout != null) {
smssc.setReconnectionTimeout(reconnectionTimeout);
}
if (password != null) {
smssc.setPassword(password);
}
if (failedAttempts != null) {
smssc.setFailedAttempts(failedAttempts);
}
// ...后面还有很多这种判断,一个if就是一个分支,会增长圈复杂度
改造后:
Optional.ofNullable(clientName).ifPresent(smssc::setClientName);
Optional.ofNullable(idleConnectionTimeout).ifPresent(smssc::setIdleConnectionTimeout);
Optional.ofNullable(connectTimeout).ifPresent(smssc::setConnectTimeout);
Optional.ofNullable(timeout).ifPresent(smssc::setTimeout);
Optional.ofNullable(retryAttempts).ifPresent(smssc::setRetryAttempts);
Optional.ofNullable(retryInterval).ifPresent(smssc::setRetryInterval);
Optional.ofNullable(reconnectionTimeout).ifPresent(smssc::setReconnectionTimeout);
// ...缩减为一行,不但减少了圈复杂度,而且减少了行数
实战五 Optional提升代码的可读性
传统操作:
public class ReadExample {
// 举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
private static String newPSWD = "12345679";
public static void main(String[] args) throws Exception {
// 简单的清理
newPSWD = ObjectUtil.isEmpty(newPSWD) ? "" : newPSWD.trim();
// 是否符合密码策略
if (newPSWD.length() <= 8) throw new Exception("Password rules are not met: \n" + newPSWD);
// 加密
//将 MD5 值转换为 16 进制字符串
try {
final MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(newPSWD.getBytes(StandardCharsets.UTF_8));
newPSWD = new BigInteger(1, md5.digest()).toString(16);
} catch (
NoSuchAlgorithmException e) {
System.out.println("Encryption failed");
}
System.out.println("We saved a new password for the user: \n" + newPSWD);
}
}
优化版本:
优化一:
public class BetterReadExample {
// 举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
private static String newPSWD = "888888888";
public static void main(String[] args) throws Exception {
Function<String, String> md = (o) -> {
try {
final MessageDigest md5;
md5 = MessageDigest.getInstance("MD5");
md5.update(o.getBytes(StandardCharsets.UTF_8));
return new BigInteger(1, md5.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Encryption failed");
}
};
String digestpwd;
digestpwd = Optional.ofNullable(newPSWD)
.map(String::trim)
.filter(f -> f.length() > 8)
.map(md)
.orElseThrow(() -> new RuntimeException("Incorrect saving new password"));
System.err.println("digestpwd = " + digestpwd);
}
}
优化二:
/**
*增加可读性
*/
public class BetterReadExample02 {
// 举个栗子:你拿到了用户提交的新密码,你要判断用户的新密码是否符合设置密码的规则,比如长度要超过八位数,然后你要对用户的密码进行加密。
private static String newPSWD = "888888888";
//清除
private static String clean(String s){
return s.trim();
}
private static boolean filterPw(String s){
return s.length()>8;
}
private static RuntimeException myREx() {
return new RuntimeException("Incorrect saving new password");
}
public static void main(String[] args) throws Exception {
//项目实战中,把main方法里面的代码再抽出一个独立方法
Function<String, String> md = (o) -> {
try {
final MessageDigest md5;
md5 = MessageDigest.getInstance("MD5");
md5.update(o.getBytes(StandardCharsets.UTF_8));
return new BigInteger(1, md5.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Encryption failed");
}
};
String digestpwd;
digestpwd = Optional.ofNullable(newPSWD)
.map(BetterReadExample02::clean)
.filter(BetterReadExample02::filterPw)
.map(md)
.orElseThrow(BetterReadExample02::myREx);
System.err.println("digestpwd = " + digestpwd);
}
}
实战六 大胆重构代码
//1. map 示例
if ( hero != null){
return "hero : " + hero.getName() + " is fire...";
} else {
return "angela";
}
//重构成
String heroName = hero
.map(this::printHeroName)
.orElseGet(this::getDefaultName);
public void printHeroName(Hero dog){
return "hero : " + hero.getName() + " is fire...";
}
public void getDefaultName(){
return "angela";
}
//2. filter示例
Hero hero = fetchHero();
if(hero != null && hero.hasBlueBuff()){
hero.fire();
}
//重构成
Optional<Hero> optionalHero = fetchHero();
optionalHero
.filter(Hero::hasBlueBuff)
.ifPresent(this::fire);
实战七 舍弃三目运算
//第一种判空
if (Objects.notNull(taskNode.getFinishTime())) {
taskInfoVo.set(taskNode.getFinishTime().getTime());
}
//第二种判空 保留builder模式
TaskInfoVo
.builder()
.finishTime(taskNode.getFinishTime() == null ? null : taskNode.getFinishTime().getTime())
.build()));
//第三种判空
public Result<TaskInfoVo> getTaskInfo(String taskId){
TaskNode taskNode = taskExecutor.getByTaskId(String taskId);
//返回任务视图
TaskInfoVo taskInfoVo = TaskInfoVo
.builder()
.taskName(taskNode.getName())
.finishTime(Optional.ofNullable(taskNode.getFinishTime()).map(date ->date.getTime()).orElse(null))
.user(taskNode.getUser())
.memo(taskNode.getMemo())
.build()));;
return Result.ok(taskInfoVo);
}
六、Optional操作总结
NPE 之所以讨厌,就是只要出现 NPE,我们就能够解决。但是一旦出现,都已经是事后,可能已经出现线上故障。偏偏在 Java 语言中,NPE 又很容易出现。Optional提供了模板方法,有效且高效的避免 NPE。
接下来,我们针对上面的使用,总结一下:
Optional是一个包装类,且不可变,不可序列化
没有公共构造函数,创建需要使用of、ofNullable方法
空Optional是单例,都是引用Optional.EMPTY
想要获取Optional的值,可以使用get、orElse、orElseGet、orElseThrow
另外,还有一些实践上的建议:
使用get方法前,必须使用isPresent检查。但是使用isPresent前,先思考下是否可以使用orElse、orElseGet等方法代替实现。
orElse和orElseGet,优先选择orElseGet,这个是惰性计算
Optional不要作为参数或者类属性,可以作为返回值
尽量将map、filter的函数参数抽出去作为单独方法,这样能够保持链式调用
不要将null赋给Optional 虽然Optional支持null值,但是不要显示的把null 传递给Optional
尽量避免使用Optional.get()
当结果不确定是否为null时,且需要对结果做下一步处理,使用Optional;
在类、集合中尽量不要使用Optional 作为基本元素;
尽量不要在方法参数中传递Optional;
不要使用 Optional 作为Java Bean Setter方法的参数
因为Optional 是不可序列化的,而且降低了可读性。
不要使用Optional作为Java Bean实例域的类型
原因同上。
七、Optional错误使用
1.使用在 POJO 中
public class User {
private int age;
private String name;
private Optional<String> address;
}
这样的写法将会给序列化带来麻烦,Optional本身并没有实现序列化,现有的 JSON 序列化框架也没有对此提供支持的。
2.使用在注入的属性中
这种写法估计用的人会更少,但不排除有脑洞的。
public class CommonService {
private Optional<UserService> userService;
public User getUser(String name) {
return userService.ifPresent(u -> u.findByName(name));
}
}
首先依赖注入大多在 spring 的框架之下,直接使用 @Autowired很方便。但如果使用以上的写法,如果 userService set 失败了,程序就应该终止并报异常,并不是无声无息,让其看起来什么问题都没有。
- 直接使用 isPresent() 进行 if 检查
这个直接参考上面的例子,用 if判断和 1.8 之前的写法并没有什么区别,反而返回值包了一层 Optional,增加了代码的复杂性,没有带来任何实质的收益。其实 isPresent()一般用于流处理的结尾,用于判断是否符合条件。
list.stream()
.filer(x -> Objects.equals(x,param))
.findFirst()
.isPresent()
- 在方法参数中使用 Optional
我们用一个东西之前得想明白,这东西是为解决什么问题而诞生的。Optional直白一点说就是为了表达可空性,如果方法参数可以为空,为何不重载呢?包括使用构造函数也一样。重载的业务表达更加清晰直观。
//don't write method like this
public void getUser(long uid,Optional<Type> userType);
//use Overload
public void getUser(long uid) {
getUser(uid,null);
}
public void getUser(long uid,UserType userType) {
//doing something
}
5.直接使用 Optional.get
Optional不会帮你做任何的空判断或者异常处理,如果直接在代码中使用 Optional.get()和不做任何空判断一样,十分危险。这种可能会出现在那种所谓的着急上线,着急交付,对 Optional也不是很熟悉,直接就用了。这里多说一句,可能有人会反问了:甲方/业务着急,需求又多,哪有时间给他去做优化啊?因为我在现实工作中遇到过,但这两者并不矛盾,因为代码行数上差别并不大,只要自己平时保持学习,都是信手拈来的东西。
如对您有帮助,欢迎点赞,嘿嘿 !!!