函数式接口 Consumer、Function、Supplier、Predicate的理解与应用

前言:stream 流式编程是操作集合常用的手段,里面包含的方法离不开大量的函数式接口,本文主要介绍这四种常用的函数式接口,并简要的谈谈它们的应用。

目录

一、什么是函数式接口

二、方法引用

三、Function(类型转换)

3.1  接口说明

3.2  应用

3.3  优化        

3.4  进阶

3.4.1  compose

3.4.2  andThen

3.4.3  identity​

四、Consumer(消费者)

4.1  接口说明

4.2  应用

4.3  进阶

4.3.1  andThen

五、 Supplier(生产者)

5.1  接口说明

5.2 应用

六、 Predicate(参数断言)

6.1  接口说明

6.2  应用

6.3  进阶


一、什么是函数式接口

        函数式接口(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
    }

}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神龙大侠学java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值