Java函数式编程详解


JDK 8Java 引入许多全新功能特性,如 streamtime 包等等,今天则让我们共同了解函数编程。

针对函数编程,根据不同的出入参特性在 JDK 中一共提供了四类,下面分别进行介绍各自声明使用方式。

一、Function

Function 即通过匿名函数方式将传统的方式转化为一个对象,如此设计最大的优点就是可以将一个方法封装为参数传入另一个方法,在一些开发场景中提高代码的复用率。

1. 声明方式

Function<T, R> 在初始化时需要制定两个类型,其中 T 为函数入参类型, R 为函数返回类型。

定义之后的 Function 通过 apply() 方法调用,传入的数据类型由初始化确定。

@Test
public void demo() {
    Function<Integer, String> function = (it) -> {
        int sum = 0;
        for (int i = 0; i < it; i++) {
            sum += i;
        }
        return "Function total: " + sum;
    };

    // 通过 apply() 调用
    String result1 = function.apply(5);
    System.out.println(result1);
}

如果上述的代码结构你仍然有点不混淆,下面就将上述代码转化为常用的方法格式。

@Test
public void demo() {
    // 普通方法调用
    String result1 = calculate(5);
    System.out.println(result1);
}

public String calculate(int num) {
    int sum = 0;
    for (int i = 0; i < it; i++) {
        sum += i;
    }
    return "Function total: " + sum;
}

2. 多参数

通过上述的例子相信你已经了解了 Function 函数的基本使用了,但从刚才的例子你或许也发现了一个问题,那就是其默认只能传一个参数,如果需要传入多个参数呢?

为此 JDK 中提供了 Function<T[], R> 用于传入一组同类型参数,但此方式将大大降低代码的可读性,不建议在实际开发中使用。

下面就介绍另外两种多参数初始化方式。

(1) BiFunctiuon

BiFunctiuon<T1, T2, R>Function 的基础上进行了扩展,允许传入两个参数,其中 T1 是第一个参数类型,T2 为第二个参数类型, R 仍然为计算的结果类型。

其同样是通过 apply() 方法调用,下面通过一个示例演示效果:

public void demo1() {
    BiFunction<Integer, Integer, String> biFunction = (val1, val2) -> {
        int sum = 0;
        for (int i = val1; i < val2; i++) {
            sum += i;
        }
        return "BiFunction total: " + sum;
    };

    String result = biFunction.apply(5, 10);
    System.out.println(result);
}
(2) TriFunction

TriFunction<T1, T2, T3, R> 故名思义,即允许传入三个参数,基本上能够覆盖开发中的大部分场景,这里就不重复介绍了。

二、Consumer

在上面中我们初步介绍了 Function 的作用,可以在之前的例子中看出其都是有返回值的,而 Consumer 基本作用与 Function 类型,但最大的不同之处在于其没有返回值,下面直接通过例子进行说明。

1. 声明方式

Consumer<T> 因为没有返回值,所以初始化时参数只有一个,没有返回值类型。

Consumer 中调用函数是通过 accept() 方法,这点与 Function 有所区别。

@Test
public void demo() {
    Consumer<Integer> consumer = (it) -> {
        int sum = 0;
        for (int i = 0; i < it; i++) {
            sum += i;
        }
        System.out.println("Consumer total: " + sum);
    };

    // 通过 accept() 调用
    consumer.accept(5);
}

2. 多参数

ConsumerFunction 类似,同样提供 BiConsumer 用于传入两个参数。

注意 Consumer 不提供两个参数以上的初始化方式,具体的初始化声明方式与 BiFunction 类似,这里不进行过多的阐述。

三、Predicate

1. 声明方式

Predicate<T> 允许接收单个参数并最终返回 boolean 类型,与 Function<T, Boolean> 等价。

通过 test() 方法触发 Predicate 函数,下面看一个具体示例:

public void demo1() {
    Predicate<Integer> predicate = (it) -> {
        int num = 0;
        num += it;
        return num > 0;
    };

    // true
    boolean result = predicate.test(1);
    System.out.println(result);
}

四、Supplier

1. 声明方式

Supplier<R> 函数接口不允许传入参数,最终返回 R 类型结果,通过 get() 方法触发函数。

Supplier 相应的使用示例如下:

public void demo2() {
    Supplier<String> supplier = () -> {
        return "Hello world!";
    };

    // Hello world!
    String result = supplier.get();
    System.out.println(result);
}

五、工具集成

经过上述的介绍,相信你对函数编辑有了初步的了解,下面就让我们看看在项目中使用场景。

1. 数据去重

在集合容器中,作为三大巨头的 List 遍布系统中每个角落,而 List 不同于 Set 即允许存在重复元素。

因此,List 在许多业务场景下都面临着数据去重的问题,而利用 Function 则可优雅的实现去重效果。

如下述示例中,利用 Function 与 Map 的特性,便轻易的实现数据去重效果。

public static <T, R> List<T> distinct(List<T> list, Function<T, R> fun) {
    if (Objects.isNull(list) || list.isEmpty()) {
        return new ArrayList<>();
    }

    Map<R, T> uniqueMap = new HashMap<>();
    for (T t : list) {
        R key = fun.apply(t);
        uniqueMap.putIfAbsent(key, t);
    }
    return new ArrayList<>(uniqueMap.values());
}

对于上述的工具实现,配合 ::lambda 表达式语法让代码进一步的优雅。

@Test
public void demo1() {
    List<Foo> fooList = new ArrayList<>();
    fooList.add(new Foo(1L, "Alex"));
    fooList.add(new Foo(1L, "Alex"));
    fooList.add(new Foo(2L, "Beth"));

    fooList = FunctionUtils.distinct(fooList, Foo::getId);
    System.out.println(fooList);
}

2. 分批查询

Web 项目中,常涉及的一个场景即 in 的批量查询,而往往不同的数据库对于 in 查询都有着一定限制,且对于单次大批量查询存在一定的性能损耗。

对于此类场景,最常见的解决方案就是分批查询,针对 in 条件集合按分批查询汇总结果。同样的,让我们来看下通过 Function 如何实现分批处理的工具封装。

public static <T, R> List<R> batchSearch(int batchSize,
                                         List<T> list,
                                         Function<List<T>, List<R>> fun) {
    if (Objects.isNull(list) || list.isEmpty()) {
        return new ArrayList<>();
    }

    List<List<T>> lists = partition(list, batchSize);
    List<R> resultList = new ArrayList<>();
    for (List<T> t : lists) {
        List<R> res = fun.apply(t);
        if (Objects.isNull(res) || res.isEmpty()) {
            continue;
        }

        resultList.addAll(res);
    }
    return resultList;
}

基于上述的工具,则可轻易实现分批查询的结果汇总,使用实例如下:

@Test
public void demo2() {
    List<Long> idList = /** 略去值来源 **/;
    List<Foo> result = FunctionUtils.batchSearch(100, idList, this::listById);
    System.out.println(result);
}

3. 批量业务

上面的示例实现了查询的分批汇总,而在数据插入时,针对 insert values 语句的也往往存在长度限制。

因此,借鉴上述的思路,通过 Consumer 函数即可实现类似的批处理。

public static <T> void action(List<T> list, int batchSize, Consumer<List<T>> consumer) {
    if (Objects.isNull(list) || list.isEmpty()) {
        return;
    }

    List<List<T>> lists = partition(list, batchSize);
    for (List<T> batchList : lists) {
        consumer.accept(batchList);
    }
}

同样的,下面以数据插入为例,对应的示例代码如下:

@Test
public void demo2() {
    List<Foo> fooList = new ArrayList<>();
    FunctionUtils.action(1000, fooList, this::batchInsert);
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录