详解Java8的Optional类到底是怎样去避免空指针异常的

一、引言

java8中引入的一个很重要的类就是Optional类,大家都说Optional类可以有效地避免空指针异常,我们就先从这个讲起:

//业务代码
User user = userService.getUserbyId(id);
user.setUserName("张三")//使用Optionnal
User user = userService.getUserbyId(id);
Optional<User> op = Optional.of(user);
if(op.isPresent()){
    op.get().setUserName("张三")}

你觉得上述改良后的代码会报NullPointerException

中国人不骗中国人,这一句是会报空指针的:

Optional<User> op = Optional.of(user); //此处会报 NullPointerException

说好的Optional可以避免空指针呢?

我们来看看 Optional.of() 的源码:

  /**
     * Returns an {@code Optional} with the specified present non-null value.
     *
     * @param <T> the class of the value
     * @param value the value to be present, which must be non-null
     * @return an {@code Optional} with the value present
     * @throws NullPointerException if value is null
     */
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
 
 
  /**
     * Constructs an instance with the value present.
     *
     * @param value the non-null value to be present
     * @throws NullPointerException if value is null
     */
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }
 
  /**
     * Checks that the specified object reference is not {@code null}. This
     * method is designed primarily for doing parameter validation in methods
     * and constructors, as demonstrated below:
     * <blockquote><pre>
     * public Foo(Bar bar) {
     *     this.bar = Objects.requireNonNull(bar);
     * }
     * </pre></blockquote>
     *
     * @param obj the object reference to check for nullity
     * @param <T> the type of the reference
     * @return {@code obj} if not {@code null}
     * @throws NullPointerException if {@code obj} is {@code null}
     */
    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

原来 Optional.of() 方法会校验对象是否为空,所以这里我们应该采用另一个Optional.ofNullable() 方法:

User user = userService.getUserbyId(id);
Optional<User> op = Optional.ofNullable(user);
if(op.isPresent()){
    op.get().setUserName("张三")}

但是问题又来了,这种写法和下面这种真的有区别吗?

User user = userService.getUserbyId(0);
if(null != user){
    user.setUserName("小明")}

我如果忘记使用if(op.isPresent())对Optional对象判空,程序还是会报空指针异常。网上有一种解释,使用Optional强制你每次都要做采用固定的步骤:isPresent()和get(),强制要求你做Optional非空判断,避免报错

这不是脱那啥放那啥吗?说好的中国人不骗中国人呢

后来看了一些博客以及《Java 8函数式编程.pdf》以后(点击链接可获取),终于悟了。和大家分享一下Optional的正确打开方式——

1.放弃掉isPresent()和get()的用法,forget it!!!

2.充分结合java8的Lambda与Stream新特性,来一次链式调用吧。

网上告诉你的isPresent()和get()固定套路,根本就是瞎扯,真正体现Optional“有效避免空指针异常”是其ifPresent()、orElse()、orElseGet()以及orElseThrow()这几个方法。

那下面我们就来具体介绍一下

  • Optional有哪些方法
  • Lamda表达式
  • 函数式接口

二、 Optional类

2.1 Optional类常用方法

在这里插入图片描述

三、函数式编程

3.1 函数式接口

3.1.1 Consumer

Consumer是一个函数式编程接口:Consumer的意思就是消费,即接收某个参数并进行使用,且没有返回值

Consumer接口有两个方法:

  • accept(T t)
  • andThen(Consumer<? super T> after)

accept

接收一个泛型的参数T,对这个参数做一系列的操作,没有返回值。这样说可能不太清楚,下面举一个栗子:

/*
 定义一个方法包含两个参数
 参数1传递一个字符串
 参数2传递Consumer接口,泛型指定为String
 可以使用参数2传递的Consumer接口“消费”参数1传递的字符串
*/
public static void consume(String reStr, Consumer<String> consumer){
  consumer.accept(reStr);
}

public static void main(String[] args){
  //调用consume方法
  //因为Consumer接口是一个函数式接口。所以可以定义一个函数
  consume("ABCDEFG", new Consumer<String>() {
    @Override
    public void accept(String str) {
      //自定义接口消费方式
      //对字符串进行反转输出
      String reStr = new StringBuffer(str).reverse().toString();
      System.out.println(reStr);
    }
  });
}

上面这段代码的意思就是将一个函数传入一个拥有Consumer类型形参的方法,在这个方法中就可以通过accept方法调用传入的参数,比如我们在执行sql语句时,需要先切换数据库,那么我们就可以这样做:

// 先定义好切换数据库的方法
public void useDb(String dbName, Statement statement) {
  try {
    statement.execute(String.format("use %s", dbName));
  } catch (SQLException e) {
    log.warn("[useDb], error = {}", e.getMessage());
  }
}

//调用执行sql的方法时传入切换数据库方法
public void executeQuerySql(String sql, String dbName, BiConsumer<String, Statement> useDb) throws Exception {
  Connection connection = null;
  Statement statement = null;
  ResultSet resultSet = null;
  try {
    connection = getConnection();
    statement = connection.createStatement();
    // 执行sql时先切换数据库,再执行sql
    if (Objects.nonNull(useDb)) {
      useDb.accept(dbName, statement);
    }
    resultSet = statement.executeQuery(sql);
  } finally {
    if (Objects.nonNull(resultSet)) {
      resultSet.close();
    }
    if (Objects.nonNull(statement)) {
      statement.close();
    }
    if (Objects.nonNull(connection)) {
      connection.close();
    }
  }
}

andThen

首先我们看一下andThen的源码:

default Consumer<T> andThen(Consumer<? super T> after) {
  Objects.requireNonNull(after);
  return (T t) -> { accept(t); after.accept(t); };
}

逻辑非常简单,就是先执行调用对象本身的accept方法,再执行after对象的accept方法。

下面我们来看一个具体的示例:

public static void consumerTest() {
  Consumer c1 = new Consumer() {
    @Override
    public void accept(Object o) {
      System.out.println(o);
    }
  };

  Consumer c2 = new Consumer() {
    @Override
    public void accept(Object o) {
      System.out.println(o + "-F2");
    }
  };

  //执行完c1后再执行c2的Accept方法
  c1.andThen(c2).accept("test");

  //连续执行c1的Accept方法
  c1.andThen(c1).andThen(c1).andThen(c1).accept("test1");
}

如果我们要每次打印日志后显示一下打印的时间(当然这是一个伪需求,原谅我一时想不出更好的栗子),那么可以用如下代码实现:

public static void formatMessage(String message, Consumer<String> c1, Consumer<String> c2) {
  c1.andThen(c2).accept(message);
}

public static void main(String[] args) {
  Consumer c1 = new Consumer() {
    @Override
    public void accept(Object o) {
      System.out.println(o);
    }
  };

  Consumer c2 = new Consumer() {
    @Override
    public void accept(Object o) {
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      System.out.println(sdf.format(new Date()));
    }
  };

  String log = "每次打印日志后显示打印的时间";
  formatMessage(log, c1, c2);
}
3.1.2 Function

Function也是一个函数式编程接口;它代表的含义是“函数”,而函数经常是有输入输出的,因此它含有一个apply方法,包含一个输入与一个输出。

这里我们定义了一个testApply()方法,它的第二个参数是Function类型的,并且指定了泛型,这里有两个泛型,第一个泛型是Function对象apply方法的形参类型,第二个泛型是apply方法的返回值类型。

我们调用testApply()方法时,传入了一个字符串和一个Function对象,并实现了Function对象的apply方法,这个方法实际就是将字符串转为整型,代码如下:

public static int testApply(String str, Function<String, Integer> f1) {
  return f1.apply(str);
}

public static void main(String[] args) {
  int num = testApply("123", new Function<String, Integer>() {
    @Override
    public Integer apply(String str) {
      return Integer.parseInt(str);
    }
  });
  
  System.out.println(num);
}

除apply方法外,Function还有三个方法:

  • compose

  • andThen

  • indentity

compose

首先我们来看一下compose()方法的源码:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
  Objects.requireNonNull(before);
  return (V v) -> apply(before.apply(v));
}

逻辑也非常简单,就是先执行before对象的apply方法,并将返回值作为参数传入调用者的apply中。

andThen

我们再来看一下Function的andThen()方法的源码:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
  Objects.requireNonNull(after);
  return (T t) -> after.apply(apply(t));
}

相信聪明的你一看就懂了,它跟compose方法是相反的,andThen是先执行调用者的apply方法,再将返回值作为参数传入after对象的apply方法。

identity

最后我们来看看identity()方法的源码:

static <T> Function<T, T> identity() {
  return t -> t;
}

identity方法会返回一个不进行任何处理的Function,即输出与输入值相等。这方法挺有意思哈,真是听君一席话如听一席话啊!

上述三个方法的使用示例如下;

/**
 * Function测试
 */
public static void functionTest() {
    Function<Integer, Integer> f = s -> s++;
    Function<Integer, Integer> g = s -> s * 2;

    /**
     * 下面表示在执行F时,先执行G,并且执行F时使用G的输出当作输入。
     * 相当于以下代码:
     * Integer a = g.apply(1);
     * System.out.println(f.apply(a));
     */
    System.out.println(f.compose(g).apply(1));

    /**
     * 表示执行F的Apply后使用其返回的值当作输入再执行G的Apply;
     * 相当于以下代码
     * Integer a = f.apply(1);
     * System.out.println(g.apply(a));
     */
    System.out.println(f.andThen(g).apply(1));

    /**
     * identity方法会返回一个不进行任何处理的Function,即输出与输入值相等; 
     * 相当于以下代码:
     * System.out.println("a");
     */
    System.out.println(Function.identity().apply("a"));
}
3.1.3 Predicate

Predicate为函数式接口,predicate的中文意思是“断定”,即判断的意思,判断某个东西是否满足某种条件; 因此它包含test方法,根据输入值来做逻辑判断,其结果为True或者False。

Predicate有如下几个方法:

  • test
  • negate
  • and
  • or
  • isEqual

这几个方法都很简单,直接看下面的示例吧:

/**
 * Predicate测试
 */
private static void predicateTest() {
    Predicate<String> p = o -> o.equals("test");
    Predicate<String> g = o -> o.startsWith("t");

    /**
     * negate: 用于对原来的Predicate做取反处理;
     * 如当调用p.test("test")为True时,调用p.negate().test("test")就会是False;
     */
    Assert.assertFalse(p.negate().test("test"));

    /**
     * and: 针对同一输入值,多个Predicate均返回True时返回True,否则返回False;
     */
    Assert.assertTrue(p.and(g).test("test"));

    /**
     * or: 针对同一输入值,多个Predicate只要有一个返回True则返回True,否则返回False
     */
    Assert.assertTrue(p.or(g).test("ta"));
}

3.2 Lamda表达式

这个网上已经讲烂了,不想多说了,放个链接算了!
Lambda表达式 详解

3.3 Stream流

Java 常用对象转换之 Stream 流

四、参考文章

  1. Java8新特性之Optional类进阶知识
  2. Java8新特性学习-函数式编程(Stream/Function/Optional/Consumer)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值