jdk8 | Function<T,R>实践应用

2,301 阅读4分钟

自动JDK8诞生,基于stream api的程序编码成为程序员秀代码的平台。事实上,除了stream相关api外,提供的lambda表达式,更新精简了不少代码。然而,对于大多数开发者而言,灵活运用基于函数式编程依然窃取,倘若灵活使用jdk8的相关函数式编程,可以有效精简我们的冗余代码,使得我们的代码可以更灵活的复用。这边,今天来谈谈JDK8中的Function具体该如何更灵活的运用,相信通过这篇文章,你有所收获。

一、场景再现

最近要完成一个小功能,该功能在一个工程中有类似业务逻辑实现,我了解了一下,顺便就可以快速实现这个功能模块开发。

现有一个Business类中有如下一个方法

public void syncVideoToDp(List<Long> ids) {
        int pageNo = 1;
        while (true) {
            try {
                List<RecruitShortVideoBO> videoList = recruitShortVideoDAO.listAllLiveVideo(ids, pageNo);
                if (CollectionUtils.isEmpty(videoList)) {
                    break;
                }
                sendUpdateUrlEvent(videoList);
                pageNo = pageNo + 1;
            } catch (Exception e) {
                logger.error("异常:" + e);
                break;
            }
        }
    }

上述方法,通过梳理逻辑,可以得出该方法的主要功能就是如下

  • 1、方法的参数是一个List<Long>类型,方法是void返回类型。
  • 2、内部有个循环,通过分页查询,委托给recruitShortVideoDAO查询数据集,如果数据集为空,则跳出循环;不为空,则吧查询的数据集进而调用成员方法sendUpdateUrlEvent

而我呢,有个功能业务逻辑跟如上方法类似,区别在于接口的方法参数不一样,调用dao的另外一个方法,依然是分页查询,没有数据集则跳出循环。于是,实现逻辑复制如上代码然后最终实现如下:

public void syncNearlyVideoToDp(Instant fromTime) {
        int pageNo = 1;
        while (true) {
            try {
                List<RecruitShortVideoBO> videoList = recruitShortVideoDAO.pagingListNearlyByModifyTime(pageNo, fromTime);
                if (CollectionUtils.isEmpty(videoList)) {
                    break;
                }
                sendUpdateUrlEvent(videoList);
                pageNo = pageNo + 1;
            } catch (Exception e) {
                logger.error("异常:" + e);
                break;
            }
        }
    }

二、优化思路

我们通过对比如上两个方法syncVideoToDp(List<Long> ids) syncNearlyVideoToDp(Instant fromTime)可以发现整体代码业务逻辑很相似,但是呢代码很冗余,为什么呢,因为整体方法业务逻辑逻辑很相似。

  • 根据参数委托dao分页查询数据集,数据集为空,跳出while循环。
  • 数据集不为空,则委托成员方法sendUpdateUrlEvent处理数据。

这时候,我们思考一下,无非区别在于查询的数据集不同,所以我们可以把代码优化如下。

通过提取一个成员方法,不关注数据集,仅业务逻辑处理。

   /**
     * 分页循环执行处理
     * @param caller
     */
    void doHandlePagingLoop(Function<Integer,List<RecruitShortVideoBO>> caller){
        int pageNo = 1;
        while (true) {
            try {
                List<RecruitShortVideoBO> videoList = caller.apply(pageNo);
                if (CollectionUtils.isEmpty(videoList)) {
                    break;
                }
                sendUpdateUrlEvent(videoList);
                pageNo = pageNo + 1;
            } catch (Exception e) {
                logger.error("异常:" + e);
                break;
            }
        }
    }

上述这个方法,方法参数就是一个Function接口,该接口有个特点,入参是Integer类型,返回参数是List。因为每次循环的时候,pageNo+1,这时候,需要回调把pageNo传给调用方,查询下一页的数据集。

方法一的代码优化如下:

public void syncVideoToDp(List<Long> ids) {
        doHandlePagingLoop(pageNo -> recruitShortVideoDAO.listAllLiveVideo(ids, pageNo));
}

方法二的代码优化如下:

public void syncNearlyVideoToDp(Instant fromTime) {
        doHandlePagingLoop(pageNo -> {
            List<RecruitShortVideoBO> list = recruitShortVideoDAO.pagingListNearlyByModifyTime(pageNo, fromTime);
            logger.info("[syncNearlyVideoToDp],本次处理数据条数={},pageNo={}", list.size(), pageNo);
            return list;
        });
    }

通过对比如上方法一和方法二,我们发现代码经过了精简优雅了许多。本质上而言,就是提取了相同的业务逻辑代码,使得该方法中doHandlePagingLoop不关注数据集是委托dao的某个方法,我只需要调用方给我数据集即可;其次呢,既然dao需要分页查询,新方法中则序号处理每页的数据后,pageNo+1,然后重新给调用法,查询下一页数据即可。

三、扩展思考

我们有没有发现jdk8中的stream api,本质上就是基于函数式编程,使得我么只需要传入一个lambda表达式即可。

譬如 forEach 就是一个Consumer接口,譬如filter 就是个 Predicate接口,譬如map就是一个Function接口,我们通过查看java.util.stream的源码如下。

/**
* @since 1.8
*/
public interface Stream<T> extends BaseStream<T, Stream<T>> {
  
  Stream<T> filter(Predicate<? super T> predicate);
  
  <R> Stream<R> map(Function<? super T, ? extends R> mapper);
  
  <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
  
  Stream<T> peek(Consumer<? super T> action);
  
  void forEach(Consumer<? super T> action);
}

所以我们调用Stream的forEach方法,本身就是把迭代的元素返回给调用方,所以源码中Consumer<? super T> 就是具体的泛型 ,如果我循环的是集合List 那么得到的元素也就是 Student;如果我们调用的map方法,得到的回调元素也是Student,这就是lambda(函数式)的魅力。