跳至主要內容

Java8 方法引用结合lambda最佳实践

Cactus li...大约 8 分钟lambdalambda

详解lambda中的方法引用

我们现在有一个苹果类,其代码定义如下:

@Data
@AllArgsConstructor
public class Apple  {
    private int weight;
    
}

因为重量单位的不同,所以得出的重量的结果可能是不同的,所以我们将计算重量的核心部分抽象成函数式接口,如下function所示,它要求我们传入Apple返回Integer

private static int getWeight(Apple apple, Function<Apple,Integer> function) {
        return function.apply(apple);
    }

假设我们对重量无需任何单位换算即原原本本返回重量本身,那么我们的表达式则直接是(a)->a.getWeight(),对应代码如下:

 Apple apple=new Apple(1);
 System.out.println(getWeight(apple,(a)->a.getWeight()));

其实这个表达式还不是最精简的,按照方法引用的语法糖,如果我们的lambda表达式符合:(arg)->arg.method(),即传入的lambda就是(实例变量)->实例变量.实例方法(),那么这个表达式就可以直接缩写为arg ClassName::invokeMethod

图片
图片

于是我们的代码就可以精简成下面这样:

System.out.println(getWeight(apple,Apple::getWeight));

除了上述这个公式以外,其实还有另外两种公式,如下所示我们的map映射希望将流中的字符串转为整型,然后输出:

  Arrays.asList("1").stream()
                .map(s -> Integer.parseInt(s))
                .forEach(i -> System.out.println(i));

按照jdk8的语法糖,对应的静态类调用静态方法的表达式(args)->className.staticMethod(args)可以直接缩写为className->staticMethod(args),于是我们的整型转换的就可以直接缩写为Integer::parseInt

图片
图片
 Arrays.asList("1").stream()
                .map(Integer::parseInt)
                .forEach(i -> System.out.println(i));

最后一种则是针对多参数的如下所示,这是一个常规的排序lambda编程:

 List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

按照Java8的语法糖:(arg1,arg2)->arg1.instanceMethod(arg2)可以直接转换为arg1ClassName::invokeInstanceMethod,于是我们的就有了下面的推导:

图片
图片

最终我们的表达式就变成了这样:

List<String> str = Arrays.asList("a","b","A","B");
        str.sort(String::compareToIgnoreCase);

方法引用对于含参构造器的抽象

我们再来一个难一点的例子,假设我们的现在的类有重量和颜色两种属性,并指明使用全参构造器完成实例创建,我们如何将这个构造器转换为方法引用呢?

@Data
@AllArgsConstructor
public class Apple  {
    private int weight;

    private String color;

}

这里我们不妨简单梳理一下,我们的构造器为传参顺序为weightcolor然后创建Apple实例,对此我们可以大体抽象出函数式接口的签名为(Integer,String)->Apple,基于这个签名我们可以直接套用公式BiFunction,它的签名为(T,U)->R,参数列表符合要求,我们直接将类型代入完成函数式接口抽象:

private static Apple createApple(Integer weight,String color,BiFunction<Integer, String, Apple> func) {
        return func.apply(weight, color);
    }

基于上述的签名的参数列表和预期返回值,我们得出下面这样一条lambda表达式作为入参传入,由此得到一个Apple实例:

 createApple(1,"yellow",(w,s)->new Apple(w,s));

按照上文所说的公式,于是我们的表达式又可以转为方法引用:

 createApple(1,"yellow",Apple::new);

lambda和方法引用的结合

我们希望对苹果类进行排序,对此我们给出苹果类的实例集合:

List<Apple> appleList = Arrays.asList(new Apple(80, "green"),
                new Apple(200, "red"),
                new Apple(155, "yellow"),
                new Apple(120, "red"));

查看函数式接口Comparator的抽象方法 int compare(T o1, T o2);得出对应的函数签名为(T,T)->Integer,代入我们的Apple类,那么这个比较器的函数描述符则是(Apple,Apple)->Integer,于是我们就有了下面这条lambda表达式:

 Comparator<Apple> comparator = (a1,a2)->a1.getWeight()-a2.getWeight();

我们键入如下代码进行调用输出:

 appleList.sort(comparator);
 appleList.forEach(System.out::println);

和预期比较结果一致:

Apple(weight=80, color=green)
Apple(weight=120, color=red)
Apple(weight=155, color=yellow)
Apple(weight=200, color=red)

实际上我们还可以做的更加精简,因为JDK8中的Comparator已经为比较器提供了一个方法comparing,查看其源码可以看到他要求传入一个入参keyExtractor,从语义上就可以知道这个参数是作为比较的条件,以我们的例子就是Appleweight。 这个keyExtractorFunction接口,查看其泛型我们也可以知晓它的函数式签名为T->R,由此我们可以推理出该方法本质就是通过Function接口变量keyExtractor生成比较变量的实例然后调用compareTo进行比较并返回结果:

//要求传入keyExtractor即作为比较的条件
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        //......
        return (Comparator<T> & Serializable)
         //通过keyExtractor生成key值调用其compareTo方法进行比较
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

基于上述分析我们就可以开始编写这个比较器的keyExtractorlambda表达式了,如下图,通过keyExtractor泛型得出函数描述符为(T)->R,基于我们的场景推导出公式是apple实例->apple实例的weight,最后comparing回基于这个函数接口生成的R对象(我们的场景是weight即int类型)调用compareTo进行比较:

图片
图片

于是我们就有了这样一条lambda表达式,但这还不是最精简的:

Comparator<Apple> comparator = Comparator.comparing(a->a.getWeight());

按照lambda的语法糖:instance->instance.method 可以直接转为instanceType::method,我们最终的表达式如下,预期结果也和之前一致:

 Comparator<Apple> comparator = Comparator.comparing(Apple::getWeight);

当然有时候我们希望能够对结果进行反向排序,我们也只需在comparing方法后面加一个reversed即实现,从语义和使用上是不是都很方便呢?

Comparator<Apple> comparator = Comparator.comparing(Apple::getWeight).reversed();

复合表达式

复合比较器

自此我们基本将方法引用的推导和使用都讲完了,接下来我们还是基于lambda做一些实用的拓展,先来说说复合比较器,以上文的苹果为例,假设我们希望当重量一样时,在比较颜色进行进一步比较,那么我们就可以直接通过thenComparing生成复合表达式:

 Comparator<Apple> comparator = Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor);

谓词复合

还是用上面的例子,我们希望根据不同的条件从苹果集合中过滤出复合条件的苹果,对此我们基于Predicate即断言函数式接口编写了一个filterApple方法:

private static List<Apple> filterApple(List<Apple> appleList, Predicate<Apple> predicate) {
        List<Apple> list = new ArrayList<>();
        for (Apple apple : appleList) {
            //复合predicate设定条件的苹果存入集合中
            if (predicate.test(apple)) {
                list.add(apple);
            }
        }
        return list;
    }

假如客户需要过滤出红色的苹果,基于predicate的签名我们得出这样一个表达式,这里就不多介绍了:

 filterApple(appleList, apple -> apple.getColor().equals("red"));

假如这时候我们有需要过滤出不为红色的苹果呢?其实JDK8为我们提供了一个非常强大的谓词negate,我们完全可以基于上面的代码进行改造从而实现需求,如下所示negate就相当于!"red".equals(a.getColor());,语义是不是很清晰呢?

Predicate<Apple> predicate = apple -> apple.getColor().equals("red");
        filterApple(appleList, predicate.negate());

但是我们需要再次变化了,我们希望找出红色且重量大于150,或者颜色为绿色的苹果,这时候又怎么办呢?我们说过JDK8提供了andor等谓词,我们的代码完全可以写成下文所示,可以看到代码语义以及流畅度都相比JDK8之前的各种&& ||拼接for循环来说优雅非常多:

 //过滤出红色的苹果
        Predicate<Apple> predicate = apple -> apple.getColor().equals("red");
        //过滤出红色且大于150 或者绿色的苹果
        Predicate<Apple> redAndHeavyAppleOrGreen = predicate.and(apple -> apple.getWeight() > 150).
                or(apple -> apple.getColor().equals("green"));


        filterApple(appleList, redAndHeavyAppleOrGreen);

函数复合

我们都说代码和数学息息相关,其实java8也提供很多函数式接口可以运用于数学公式上,例如,我们现在需要计算f(g(x)),这个公式学过高数的同学都知道,是先计算g(x)再将g(x)的结果作为入参交给f(x)计算,对应题解案例如下:

我们假设g(x)=x * 2
f(x)=x+1
假如x=1
那么f(g(x))最终就会等于4

了解数学公式之后,我们完全可以使用java代码表示出来,首先我们先声明一下f(x)g(x)

//f(x)
 Function<Integer, Integer> f = x -> x + 1;
 //g(x)
 Function<Integer, Integer> g = x -> x * 2;

在表示g(f(x)),通过复合表达式andThen表达了数学的计算顺序,即显得出f(x)结果,然后(andThen)代入g(x)中:

 //意味先计算f(x)在计算g(x)
 Function<Integer, Integer> h = f.andThen(g);
System.out.println(result); //输出 4

基于上面的例子,如果我们还需要计算f(g(x))要怎么办呢?从f(x)角度来看,g(x)的结果组合到f(x)上,所以我们可以直接实用compose方法:

 Function<Integer, Integer> gfx = f.compose(g);
 Integer result = gfx.apply(1);
 System.out.println(result);// 输出 3

选其中一种好理解的实用就行了。

小结

自此我们将方法引用的推导和实用,以及各种表达式组合的内容都介绍完了,希望对你有帮助。

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3