Java函数式接口讲解与应用

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

Consumer案例

需求背景

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

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

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

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

编码实现

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

@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函数式接口,执行下传进来的方法,并且是带一个参数的,相当于动态调用了不同对象的Set方法,把当前用户某些属性设置到对象中。

不同的Consumer参数类型是可以不一样的,但是同一个字段,在不同对象中类型需要一样。其实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转化成List,对每一个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

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

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

打开看文件内容

实例方法作为Function函数式接口

静态方法作为Function函数式接口

每个Lambda表达式对应一个类,这个类实现了Function接口,FunctionTestKaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲2这个类是静态方法当做Func…Lambda$2这个类的实例,这个实例是通过调用工厂方法得到的,doFunction中就是调用具体的实现类的apply方法,参数也传到具体方法里面去,这样就实现了动态方法调用。

FunctionTestKaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲1这个类比FunctionTe…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

编译后的内容

这个案例和案例一的区别就是被动态调用的方法是没有参数的,apply方法是必须要传一个参数,所以这里的参数变成了被动态调用方法所属的实例。从代码上看,区别就是FunctionTest2KaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲1的apply方法参数是被转成…Lambda$1中apply方法的参数是原封不动地传到hasOneParam的形参里面去。

其他函数式接口

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

  • BiFunction<T, U, R> 传两个参数,带一个返回值
  • Predicate 传一个参数,返回一个布尔类型的值
  • Supplier 没有参数,直接获取返回值
  • 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 lists)。

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


版权声明:本文为AE86JayChou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>