目录
一、前置基础
二、什么是Optional
2.1理论拓展
三、为什么要用Optional
3.1俄罗斯式套娃判空详解
四、Optional基本知识
4.1API的思考
五、工作中如何正确使用Optional
5.1 orElseThrow
5.2 filter
5.3 orElse和orElseGet
5.4 map和flatMap
5.5 项目实战
实战一
实战二
实战三 简化if.else
实战四 解决checkStyle问题
实战五 Optional提升代码的可读性
实战六 大胆重构代码
实战七 舍弃三目运算
六、Optional操作总结
七、Optional错误使用
一、前置基础
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 失败了,程序就应该终止并报异常,并不是无声无息,让其看起来什么问题都没有。
3.直接使用 isPresent() 进行 if 检查
这个直接参考上面的例子,用 if判断和 1.8 之前的写法并没有什么区别,反而返回值包了一层 Optional,增加了代码的复杂性,没有带来任何实质的收益。其实 isPresent()一般用于流处理的结尾,用于判断是否符合条件。
list.stream()
.filer(x -> Objects.equals(x,param))
.findFirst()
.isPresent()
4.在方法参数中使用 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也不是很熟悉,直接就用了。这里多说一句,可能有人会反问了:甲方/业务着急,需求又多,哪有时间给他去做优化啊?因为我在现实工作中遇到过,但这两者并不矛盾,因为代码行数上差别并不大,只要自己平时保持学习,都是信手拈来的东西。
如对您有帮助,欢迎点赞,嘿嘿 !!