美文网首页
Java函数式接口讲解与应用

Java函数式接口讲解与应用

作者: 日楚东山 | 来源:发表于2022-04-04 15:09 被阅读0次

    最近业务代码编写中使用到了一个函数式接口 Consumer<T>,巧妙地解决了代码复用的问题,既解决了业务需求,代码风格又优雅,而且高度内聚。下面直接上代码案例,然后再深入介绍Java8中的几个函数式接口:Function<T, R>,Consumer<T>,Predicate<T>,Supplier<T>。最后结合使用场景以及Java逆向移植工具Retrolambda(点这了解Retrolambda)帮助读者加深对函数式接口的理解。

    Consumer<T>案例

    需求背景

    因涉及系统敏感信息,案例是经过脱敏、简化后的,不影响实际理解与使用,示例代码也是根据简化后的需求从头开始编写的。

    有一个订单列表的需求,不同的用户查看到的订单列表数据是不一样的,规则如下:

    • 超级管理员能查看所有订单,超级管理员能够根据不同的条件进行筛选,比如查看全部A类,比如查看单个企业,比如查看单个团队;
    • A类管理员只能查看所有A类企业的订单,也能根据单个企业、或者单个团队的条件进行筛选;B类管理员只能查看所有B类企业的订单,其他和A类管理员一样
    • 企业管理员能查看该企业下的所有订单,企业下面有很多团队,企业管理员也能根据团队进行筛选,
    • 团队管理员只能查看本团队的所有订单

    所以需要根据权限将订单列表进行过滤掉,也就是说需要根据当前用户角色,设置不同的WHERE条件,传到数据库里面去查询对应的数据。

    编码实现

    下面列出关键代码,主要关注点在Consumer<T>的使用,像设计、编码是否合理可以忽略。

    @RestController
    @RequestMapping("/order")
    public class OrderController {
    
        @Autowired
        private OrderService orderService;
    
        @GetMapping("/admin")
        public List<AdminOrderListVO> getAdminOrderList(AdminOrderListCommand command) {
            List<Order> orders = orderService.getAdminOrderListByParam(command.to());
            return orders.stream().map(AdminOrderListVO::from).collect(Collectors.toList());
        }
    
        @GetMapping("/type-admin")
        public List<TypeAdminOrderListVO> getTypeAdminOrderList(TypeAdminOrderListCommand command) {
            List<Order> orders = orderService.getTypeAdminOrderListByParam(command.to());
            return orders.stream().map(TypeAdminOrderListVO::from).collect(Collectors.toList());
        }
    
        @GetMapping("/enterprise-admin")
        public List<EnterpriseAdminOrderListVO> getEnterpriseAdminOrderList(EnterpriseAdminOrderListCommand command) {
            List<Order> orders = orderService.getEnterpriseOrderListByParam(command.to());
            return orders.stream().map(EnterpriseAdminOrderListVO::from).collect(Collectors.toList());
        }
    }
    
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
    
        public List<Order> getAdminOrderListByParam(AdminOrderListParam adminParam) {
            OrderService.fillCommonCondition(adminParam::setEnterpriseType, adminParam::setEnterpriseId, adminParam::setTeamId);
            return orderMapper.findAdminOrderListByParam(adminParam);
        }
        
        public List<Order> getTypeAdminOrderListByParam(TypeAdminOrderListParam typeAdminParam) {
            OrderService.fillCommonCondition(typeAdminParam::setEnterpriseType,
                    typeAdminParam::setEnterpriseId, typeAdminParam::setTeamId);
            return orderMapper.findTypeAdminOrderListByParam(typeAdminParam);
        }
    
        public List<Order> getEnterpriseOrderListByParam(EnterpriseAdminOrderListParam enterpriseAdminParam) {
            OrderService.fillCommonCondition(enterpriseAdminParam::setEnterpriseType,
                    enterpriseAdminParam::setEnterpriseId, enterpriseAdminParam::setTeamId);
            return orderMapper.findEnterpriseAdminOrderListByParam(enterpriseAdminParam);
        }
    
        public static void fillCommonCondition(Consumer<String> setEnterpriseType,
                                               Consumer<Integer> setEnterpriseId, Consumer<Long> setTeamId) {
            if (setEnterpriseType != null) {
                setEnterpriseType.accept(CurrentUserUtil.currentEnterpriseType());
            }
            if (setEnterpriseId != null) {
                setEnterpriseId.accept(CurrentUserUtil.currentEnterpriseId());
            }
            if (setTeamId != null) {
                setTeamId.accept(CurrentUserUtil.currentTeamId());
            }
        }
    }
    

    分析

    上面列出了Controller和Service,Controller有三个订单列表的接口,他们有不同的参数对象,接口逻辑都是先将参数对象转成Service的入参对象,调用Service的逻辑,最后将Service返回数据转成对应VO。重点是在Service里面,三个Service方法都共同调用了fillCommonCondition方法,这个方法的功能就是:动态地向不同对象中设置属性值,实现原理就是根据传进来的Consumer<T>函数式接口,执行下传进来的方法,并且是带一个参数的,相当于动态调用了不同对象的Set方法,把当前用户某些属性设置到对象中。

    不同的Consumer<T>参数类型是可以不一样的,但是同一个字段,在不同对象中类型需要一样。其实fillCommonCondition方法不仅适用在订单列表,其实整个系统的权限控制都是这个逻辑,这种写法适用于所有需要权限控制的场景,不限对象类型,实现了代码高度复用,不然需要在每个接口手动调用当前参数对象的SET方法来设置值。

    在上面例子中,我理解的就是将set方法作为参数传到另一个方法里面,然后去执行传进来的set方法,其他的函数式接口也是类似,只是根据方法参数和返回值分了类。以前实现动态方法调用基本就是使用反射,用起来比较繁琐,而且代码很僵硬。使用了函数式接口代码十分简洁,由此想深入理解下Java8中的几个函数式接口。

    Function<T, R>

    Function<T, R>首先是一个接口,里面有一个抽象方法,三个默认实现的方法,主要是R apply(T t)方法,实现Function接口就需要实现apply方法,比如x -> 2 * x就是一个函数式接口,可以转换成JDK1.7内部类,重写了apply方法的形式,代码如下

    Function<Integer, Integer> lambda = x -> 2 * x;
    
    Function<Integer, Integer> function = new Function<Integer, Integer>() {
        @Override
        public Integer apply(Integer x) {
            return 2 * x;
        }
    };
    

    jdk源码里面的一个方法

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    

    这是java.util.stream.Stream的map方法,参数就是一个Function接口。在上面Consumer的案例中,最后一步转成VO的时候,使用了Stream中的map方法,传进去了from的静态方法。效果就是将List<Order>转化成List<AdminOrderListVO>,对每一个Order,都会调用传进去的from方法。

    下面通过两个Function<T, R>的例子来演示不同的调用方式,第一个案例是实际传的Function是有参数的,第二个时没有参数的。

    案例一

    public class FunctionTest {
    
        public static void main(String[] args) {
    
            FunctionTest functionTest = new FunctionTest();
            String s = functionTest.doFunction(functionTest::hasOneParam, "s");
            Integer integer = functionTest.doFunction(functionTest::increase, 6);
            System.out.println(s);
            System.out.println(integer);
        }
    
        public <T, R> R doFunction(Function<T, R> function, T param) {
            return function.apply(param);
        }
    
        public <T> String hasOneParam(T param) {
            return param.toString();
        }
    
        public Integer increase(Integer i) {
            return i + 1;
        }
    }
    
    //运行结果
    s
    7
    Process finished with exit code 0
    

    <kbd>doFunction</kbd>就是执行传进来的方法,而且该方法的参数也是传进来的,相当于动态调用了一遍方法。我们通过Retrolambda工具将上面的代码编译成JDK6的Class文件,然后用IDEA反编译打开看下里面的内容。

    上面FunctionTest类编译以后的是三个文件,因为有两个Lambda表达式,在JDK6中是使用内部类来实现的,而内部类编译后是单独Class文件。

    d90ac0b206824d01a73f4fbd2bb19630.png

    打开看文件内容


    3a5bad5822c74bccbe7553aec7d2b2f6.png 201b046d27c54c50a5da4b9944ab2f7f.png c996af4ffcd5435cbcd16e0f4b484727.png

    每个Lambda表达式对应一个类,这个类实现了Function接口,FunctionTest$$Lambda$2这个类是静态方法当做Function的Lambda表达式,里面有一个静态的工厂方法,重写的apply方法,当前类实例的静态属性,调用doFunction方法时是传进去FunctionTest$$Lambda$2这个类的实例,这个实例是通过调用工厂方法得到的,doFunction中就是调用具体的实现类的apply方法,参数也传到具体方法里面去,这样就实现了动态方法调用。

    FunctionTest$$Lambda$1这个类比FunctionTest$$Lambda$2多了一个属性,这个属性是被调用方法所属的类,通过工厂方法传进来,因为实例方法的调用必须指明是哪个实例,静态方法可以直接通过类名来调用。

    案例二

    public class FunctionTest2 {
    
        public static void main(String[] args) {
            FunctionTest2 functionTest2 = new FunctionTest2();
            String s = functionTest2.doFunction(FunctionTest2::hasNoParam, functionTest2);
            System.out.println(s);
        }
    
        public <T, R> R doFunction(Function<T, R> function, T param) {
            return function.apply(param);
        }
    
        public String hasNoParam() {
            return "A";
        }
    }
    //运行结果
    A
    Process finished with exit code 0
    

    编译后的内容

    936f22608e3a424d9f659b1e20e78bfd.png 287eb3fd981348609d805b83e7a1b48e.png

    这个案例和案例一的区别就是被动态调用的方法是没有参数的,apply方法是必须要传一个参数,所以这里的参数变成了被动态调用方法所属的实例。从代码上看,区别就是FunctionTest2$$Lambda$1的apply方法参数是被转成FunctionTest2类型然后在直接调用FunctionTest2的hasNoParam()方法,而FunctionTest$$Lambda$1中apply方法的参数是原封不动地传到hasOneParam的形参里面去。

    其他函数式接口

    案例二的写法有点类似Supplier<T>的功能,没有参数但是提供一个返回值。如果使用参数,不使用Function的返回值,就变成了Consumer<T>,所以其他的一些函数式接口原理都是类似的,有些变换了形式,有些通过继承、添加默认实现方法扩展了功能,像下面这些:

    • BiFunction<T, U, R> 传两个参数,带一个返回值
    • Predicate<T> 传一个参数,返回一个布尔类型的值
    • Supplier<T> 没有参数,直接获取返回值
    • BiPredicate<T, U> 传两个参数,返回一个布尔类型的值

    自定义函数式接口

    JDK的java.util.function包提供了很多函数式接口,如果不满足业务需求,可以自定义函数式接口,比如下面是一个函数式接口,接收三个参数,带一个返回值

    @FunctionalInterface
    public interface MyFunction<T, V, R, P> {
        R apply(T t, V v, P p);
    }
    

    也可以将一些参数设置成固定的类型,如String,Integer或者具体对象类型,如 R apply(T t, String v, List<String> lists)。

    函数式接口的使用还算简单的,就是把方法当做参数传到方法里面,只是以前我们是传值类型的参数。函数式接口里面还可以有逻辑,甚至可以函数式接口嵌套或者叠加使用,可以根据自己想象力和业务需求玩出更骚、更花的一些操作,总的来说函数式接口确实方便了编码,可以先学起来,多实践,慢慢理解。

    相关文章

      网友评论

          本文标题:Java函数式接口讲解与应用

          本文链接:https://www.haomeiwen.com/subject/rqpcsrtx.html