前言:stream 流式编程是操作集合常用的手段,里面包含的方法离不开大量的函数式接口,本文主要介绍这四种常用的函数式接口,并简要的谈谈它们的应用。
目录
一、什么是函数式接口
函数式接口(Functional Interface)是只有一个抽象方法的接口,但是可以有多个非抽象方法。函数式接口可以被隐式的转换为Lambda表达式,基于这个特性就可以实现直接将函数作为数据进行传递。通过检查该接口是否有@FunctionalInterface注解,判断该接口是否是一个函数式接口,如果是,编译通过;如果不是,则编译失败。java.util.function 包下定义了Java 8 的丰富的函数式接口,包括Consumer、Function、Supplier 和 Predicate 这四种。
可以说函数式接口的作用就像是存操作的容器,它的抽象方法用来接收参数然后让封装的操作来处理,或者用来获取操作后的结果。
二、方法引用
函数式接口常常由 Lambda 表达式来实现,用方法引用来简化 Lambda 表达式的主体部分,且使用方法引用的接口必须是函数式接口。方法引用有四种常见的和一种特殊的,常见的有类名::静态方法、对象名::实例方法、类名::new、数组::new,特殊的是 类名::实例方法。这种特殊的使用场景是在嵌套函数中,外层提供的参数刚好是内层所需的参数,且实现的操作也是此类中现有的方法,如stream流中的map() 方法。
将List<User> 中的每个对象映射成它的 name 字段时,原方法是:
@Test
public void test() {
List<User> users = new ArrayList<>();
List<String> names = users.stream().map(user -> user.getName()).collect(Collectors.toList());
}
改用方法引用就可以缩写为:
@Test
public void test() {
List<User> users = new ArrayList<>();
List<String> names = users.stream().map(User::getName).collect(Collectors.toList());
}
三、Function(类型转换)
3.1 接口说明
Function<T,R> 从注解中我们可以了解到:Function 这个接口它接收一个参数并且产生一个结果,也就是第一个参数类型 T 代表入参,第二个参数类型 R 代表返回的结果。这两个参数交给了它的函数式方法 apply()来处理,apply() 实现的逻辑就是接收参数类型 T 然后返回参数类型 R,也就是将 T 类型的数据转换为了 R 类型的数据。
3.2 应用
这个接口的应用在 stream 流中的 map() 就是一个很好的例子,点进 map() 方法可以发现它实际上就是封装的 Function 接口。这里的Function 接口第一个参数就是可以传 T 的任何父类型,包括 T 本身;第二个参数就是可以传任何 R 的子类型,包括 R 本身。
举个粒子,我们有一个 User 类,里面有 id , name , address 这三种属性。现在我们有三个 User 对象,需要把他们的名字给打出来,怎么实现效率最快呢?
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String address;
}
这时就可以用 stream 流了
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class 测试 {
@Test
public void test() {
// 用 ArrayList 装三条数据
List<User> users = new ArrayList<>();
for (int i = 0; i < 3; ) {
users.add(new User(++i, "zs" + i, "翻斗花园"+i+"号"));
}
// 然后用 stream 的 map 对其进行映射就可以输出啦
users.stream().map(User::getName).forEach(System.out::println);
}
}
来看看结果:
3.3 优化
正常输出,没问题。现在我们来改进一下,我们不仅要输出 name 还要输出 id ,如果有其他字段的话我都要给他们一一输出,现在该咋整捏?我们这时就可以利用 Function<T,R> 这个接口的特点,把这个 stream 单独提出去,把映射、输出这些操作封装成一个方法,然后想把哪些字段进行这个处理,就可以传哪些字段进来,这样就可以大大的简化我们的代码结构。直接上代码:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class 测试 {
@Test
public void test() {
// 用 ArrayList 装三条数据
List<User> users = new ArrayList<>();
for (int i = 0; i < 3;) {
users.add(new User(++i, "zs" + i,"翻斗花园"+i+"号"));
}
// 调用方法,输出字段
printField(users,User::getName);
printField(users,User::getAddress);
}
void printField(List<User> users, Function<User,String> function){
users.stream().map(function).forEach(System.out::println);
}
}
结果:
3.4 进阶
文章开头我们提到过,函数式接口只有一个抽象方法,但是可以有多个非抽象方法,现在我们来看看 Function 接口的三个非抽象方法:compose、andThen、identity
3.4.1 compose
从它的源码可以知道,compose 这个方法会先执行 before 的逻辑,再执行它自己的逻辑,然后只要有一个有异常,都会给调用者。也就是说它会先执行入参的逻辑,再把它的结果当作自己的入参,再处理自己的逻辑,最后把自己的逻辑给返回回去。这是一种嵌套执行的操作,顺序是由里到外,但值得注意的是,compose 和 before 这两个Function 都是操作,所以还需要再调一个 apply() 方法来接收一个源数据,然后才能进行这一些列的处理。
举个栗子,求一个包含数字的字符串中所有数字的和,可以怎么实现呢?比如说 "1q-2w-3e" 这里有一个简单的小思路,那就是先拿到所有数字,然后再挨个加起来(哈哈,没想到吧.jpg )。代码如下:
import cn.hutool.core.util.ObjectUtil;
import org.junit.Test;
import java.util.Arrays;
import java.util.function.Function;
public class 测试 {
@Test
public void test() {
String str = "1q-2w-3e";
Integer sum = composeTest(str, // 源数据
(strType) -> strType.split("[^0-9]"), // 拿到所有数字,实现方法把所有非数字进行分割,但是可能会有空字符串 "" 存在
// 第三个参数就是先把空字符串 "" 过滤掉,然后把所有的 String型的数字转成 Integer 型的,最后对其进行归约求和操作
// 归约操作会得到一个 Optional 型的容器对象,所以调用其 get() 就可以拿到里面的值啦
(strArrType) -> Arrays.stream(strArrType).filter(ObjectUtil::isNotEmpty).map(Integer::valueOf)
.reduce(Integer::sum).get());
System.out.println(sum);
}
/**
* @param data 源数据
* @param function1 对数据进行预处理(筛选出数字)
* @param function2 求和
* @return sum
*/
Integer composeTest(String data, Function<String, String[]> function1, Function<String[], Integer> function2) {
return function2.compose(function1).apply(data);
}
}
结果如下:
从代码中可以观察到,compose 的入参类型要和 before 的结果类型保存一致,before 的入参类型要和 apply() 的入参一样才行。
3.4.2 andThen
和 compose 类似,只不过执行的顺序是相反的。 andThen 的执行顺序是先执行自己的逻辑,再执行入参的逻辑。但同样的都需要apply() 来接收源数据,且 andThen 和其入参的 Function 这两个的入参和结果也需要相对应才行。
举个梨子来对比一下这两个的区别:对一个源数据 2 ,一个操作是对其 +1,一个操作是对其 *3。然后分别用这两个方法来实现一下看看结果如何:
import org.junit.Test;
import java.util.function.Function;
public class 测试 {
@Test
public void test() {
Integer num1 = composeTest(2, (param1) -> param1 + 1, (param2) -> param2 * 3);
Integer num2 = andThenTest(2, (param1) -> param1 + 1, (param2) -> param2 * 3);
System.out.println("使用 compose 方法的结果为:" + num1);
System.out.println("使用 andThen 方法的结果为:" + num2);
}
/**
* 由里到外,先执行里面的
*/
Integer composeTest(Integer num, Function<Integer, Integer> function1, Function<Integer, Integer> function2) {
return function2.compose(function1).apply(num);
}
/**
* 由外到里,先执行外面的
*/
Integer andThenTest(Integer num, Function<Integer, Integer> function1, Function<Integer, Integer> function2) {
return function2.andThen(function1).apply(num);
}
}
可以预测一下,因为 compose 是先执行里面的所以其值是(2+1)*3=9,andThen 是先执行外面的所以它的值为2*3+1=7,来看一下结果:
3.4.3 identity
identity 同一性,输入什么参数就返回什么结果。常用在 List 转 Map 中:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.util.stream.Collectors.toMap;
public class 测试 {
@Test
public void test() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 3; ) {
users.add(new User(++i, "zs" + i, "翻斗花园" + i + "号"));
}
Map<String, User> map = users.stream().collect(toMap(User::getName, Function.identity()));
map.forEach((k, v) -> System.out.println(k + "-" + v));
}
}
结果如下:
无序是因为 toMap() 底层是用 HashMap::new 创建的 hashMap,hashMap 本身就是无序的。
四、Consumer(消费者)
4.1 接口说明
Consumer 消费者,把传进来的参数消费掉,没有返回结果。由它的抽象方法 accept() 接受一个参数,并由实现 Consumer 接口的操作来处理。
4.2 应用
我们最常见的 forEach() 传的就是 Consumer 接口,其作用是接收集合中的每个元素,然后交给 Consumer 的操作来处理。
如输出集合中的每个元素:
@Test
public void test() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 3; ) {
users.add(new User(++i, "zs" + i, "翻斗花园" + i + "号"));
}
users.forEach(System.out::println);
}
4.3 进阶
4.3.1 andThen
Consumer 的 andThen 方法同Function 一样,也是先执行外边的,再执行里面的,由外到里,从左到右,由 accept() 方法来接收一个源数据。因为都没有返回值,所以这俩 Consumer 接口彼此之间是没有啥关联的,只是一个执行顺序的不同而已。
比如,对一个字符串 "aBc" ,先输出它全大写,再输出它全小写,代码为:
import cn.hutool.core.util.ObjectUtil;
import org.junit.Test;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.function.Function;
public class 测试 {
@Test
public void test() {
String str = "aBc";
andThenTest(str,
(strData)-> System.out.println(strData.toUpperCase()), // 输出全大写
(strData)-> System.out.println(strData.toLowerCase())); // 输出全小写
}
/**
* 先执行 function1 再执行 function2
*/
void andThenTest(String data, Consumer<String> function1, Consumer<String> function2) {
function1.andThen(function2).accept(data);
}
}
结果为:
五、 Supplier(生产者)
5.1 接口说明
Supplier 供应者,只有一个抽象方法 get()。不需要入参,直接产出一个东西丢给你,好心仁啊!!。
5.2 应用
和 Consumer 相反,Supplier 只管生产东西,比如创建一个对象、生成一个随机数、生成一个UUID这种无中生有的操作都可以交给 Supplier 来处理实现,然后用它的 get() 方法来拿就好了:
import org.junit.Test;
import java.util.Random;
import java.util.UUID;
import java.util.function.Supplier;
public class 测试 {
@Test
public void test() {
Supplier<User> userSupplier = User::new;
Supplier<Integer> intSupplier = () -> new Random().nextInt(100);
Supplier<UUID> uuidSupplier = UUID::randomUUID;
System.out.println(userSupplier.get());
System.out.println(intSupplier.get());
System.out.println(uuidSupplier.get());
}
}
结果为:
六、 Predicate(参数断言)
6.1 接口说明
Predicate 断言,通过实现的操作来判断传入的参数是真是假。古人有云,有没有电我一摸就知道了。
6.2 应用
这个断言使用的场景就很广了,只要有需要判断的地方都可以进行判断,如:
import cn.hutool.core.util.ObjectUtil;
import org.junit.Test;
import java.util.function.Predicate;
public class 测试 {
@Test
public void test() {
Predicate<Object> objPredicate = ObjectUtil::isEmpty; // 判断对象是否是 null 或 ""
Predicate<String> strPredicate = (str) -> str.contains("abc"); // 判断字符串是否包含 "abc"
Predicate<Integer> intPredicate = (num) -> num > 6; // 判断数值是否大于6
System.out.println(objPredicate.test(new User())); // false
System.out.println(strPredicate.test("神龙大侠")); // false
System.out.println(intPredicate.test(22)); // true
}
}
6.3 进阶
Predicate 接口有4个默认的方法:
and(Predicate<? super T> other):对两个 Predicate 的test 取交集,也就是 与运算 &&
or(Predicate<? super T> other):对 两个 Predicate 的test 取并集,也就是 或运算符 ||
negate():对 Predicate 的结果取反,也就是 非运算符 !
isEqual(Object targetRef) : 判断传入 test() 的对象是否等于此方法的对象,此方法为静态方法
需要注意的是,对两个 Predicate 接口的操作其接收的类型得一样才行。举个小李子:
import cn.hutool.core.util.ObjectUtil;
import org.junit.Test;
import java.util.function.Predicate;
public class 测试 {
@Test
public void test() {
Predicate<Object> objPredicate = ObjectUtil::isEmpty; // 判断对象是否是 null 或 ""
Predicate<String> strPredicate1 = (str) -> str.contains("abc"); // 判断字符串是否包含 "abc"
Predicate<String> strPredicate2 = (str) -> str.length() > 2; // 判断字符串是否包含 "abc"
System.out.println(strPredicate1.and(strPredicate2).test("神龙大侠")); // false
System.out.println(strPredicate1.or(strPredicate2).test("神龙大侠")); // true
System.out.println(objPredicate.negate().test(new User())); // true
System.out.println(Predicate.isEqual(new User()).test(new User())); // true
}
}