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表达式 详解