Java8的函数式编程简介

注:本文主要参考了《Java 8实战》这本书。

在这里插入图片描述

环境

  • Ubuntu 22.04
  • jdk-17.0.3.1 (兼容Java 8)

背景

已知苹果类定义如下(每个苹果有颜色、重量等属性):

class Apple {
    private String color;
    private double weight;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public Apple() {
    }

    public Apple(String color, double weight) {
        this.color = color;
        this.weight = weight;
    }

现在有一堆苹果:

public class Test0913 {
    public static void main(String[] args) {
        List<Apple> list0 = new ArrayList<>();

        list0.add(new Apple("Red", 200));
        list0.add(new Apple("Green", 300));
        list0.add(new Apple("Red", 220));
        list0.add(new Apple("Red", 280));
        list0.add(new Apple("Green", 220));
        ......
    }
}

要求查找满足一定条件的苹果,比如说颜色是红色的苹果。

方法

方法1:Java 7(传统方法)

在Java 8之前,我们可能会这么写:

    public static List<Apple> getRedApples(List<Apple> list) {
        List<Apple> result = new ArrayList<>();
        for (Apple e: list) {
            if ("Red".equals(e.getColor())) {
                result.add(e);
            }
        }
        return result;
    }

注:这里使用了 static 关键字,仅仅是为了方便,可直接使用 Test0913.getRedApples() 来调用该方法,而无需创建对象实例。后续代码中的 static 也同理。

调用该方法来查找红苹果:

        List<Apple> list1 = Test0913.getRedApples(list0);

方法2:Java 7 (策略模式)

方法1的缺点是,如果要查找重量超过250的苹果,则需要添加一个与 getRedApples() 类似的方法,如下:

    public static List<Apple> getHeavyApples(List<Apple> list) {
        List<Apple> result = new ArrayList<>();
        for (Apple e: list) {
            if (e.getWeight() > 250) {
                result.add(e);
            }
        }
        return result;
    }

显然,二者的重复代码非常多。

为了减少重复,可以采取设计模式中的“策略模式”,把公共部分提取出来。

  • 定义一个接口 AppleStrategy ,它只有一个 test() 方法,传入一个苹果实例,返回true/false,代表了对“苹果是否满足要求”的抽象:
interface AppleStrategy {
    boolean test(Apple apple);
}
  • 接口的实现类,该类实现了对“红苹果”的测试:
class AppleStrategy_Red implements AppleStrategy {

    @Override
    public boolean test(Apple apple) {
        return "Red".equals(apple.getColor());
    }
}
  • 接口的实现类,该类实现了对“重苹果”的测试:
class AppleStrategy_Heavy implements AppleStrategy {

    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 250;
    }
}
  • 策略之外的不变部分,可抽象为如下方法:
    public static List<Apple> filterApples(List<Apple> list, AppleStrategy strategy) {
        List<Apple> result = new ArrayList<>();
        for (Apple e: list) {
            if (strategy.test(e)) {
                result.add(e);
            }
        }
        return result;
    }
  • 调用该方法来查找红苹果:
        List<Apple> list2 = Test0913.filterApples(list0, new AppleStrategy_Red());

注:如果不想显式定义 AppleStrategy_Red / AppleStrategy_Heavy 等接口的实现类,也可以在需要时直接使用匿名类,如下:

        List<Apple> list2 = Test0913.filterApples(list0, new AppleStrategy() {
            @Override
            public boolean test(Apple apple) {
                return "Red".equals(apple.getColor());
            }
        });

方法3:Java 8的Lambda

方法2比方法1要灵活很多,但是方法2需要定义接口、实现类(或匿名类),创建对象实例,等等,代码较为复杂。

如何才能兼顾灵活和简单呢?显然,方法2的精华部分在于“策略”。定义的接口和实现类,就是为了实现不同的策略。要想精简代码,Java 8 提供了一种方法,可以直接把策略以代码的方式(而非对象)传递给调用者:

        List<Apple> list3 = Test0913.filterApples(list0, e -> "Red".equals(e.getColor()));

仔细对比一下方法3和方法2的 filterApples() 方法,重点比较一下第二个参数(其类型是 AppleStrategy ),如下:

  • 方法2:
        new AppleStrategy() {
            @Override
            public boolean test(Apple apple) {
                return "Red".equals(apple.getColor());
            }
        }
  • 方法3:
        e -> "Red".equals(e.getColor())

这就是Java 8的Lambda,它是一个匿名方法,它由三部分组成:

  • 参数列表:即 e ,完整形式是 (Apple e)
  • 箭头符号:即 -> ,分隔参数和代码
  • 代码:即 "Red".equals(e.getColor()) ,完整形式是 {return "Red".equals(e.getColor());} (注意花括号和分号),可以有多条语句

简而言之,使用Lambda,就可以通过“直接传代码”来简化复杂度,达到和匿名类同样的效果。

本例中, e -> "Red".equals(e.getColor()) 就代表了一个实现了 AppleStrategy 接口的匿名类的实例。

所以,方法3也可以写成:

        AppleStrategy strategy = (Apple e) -> "Red".equals(e.getColor());
        List<Apple> list3 = Test0913.filterApples(list0, strategy);

filterApples() 方法里调用 AppleStrategytest() 方法时,运行的就是Lambda的代码。

那么问题来了,由于 AppleStrategy 接口只有一个 test() 方法,显然Lambda代表的就是这个方法。但是假如接口有多个方法,如果使用Lambda,只传一堆代码的话,怎么能知道是哪个方法?那不是乱套了吗?

确实如此,所以Lambda的使用也是有限制的,它只适用于“函数式接口”。

所谓函数式接口,就是只有一个抽象方法的接口。接口可以定义0个或多个默认方法,只要只定义了一个抽象接口,那就仍然是函数式接口。

函数式接口可以通过 @FunctionalInterface 注解来修饰。如果对接口加上该注解,而实际不是函数式接口,则编译会报错。虽然该注解不是强制的,不过最好还是加上(类似 @Override 注解)。

在本例中, AppleStrategy 就是一个函数式接口(不过没加 @FunctionalInterface 注解)。

总结:Lambda所代表的是对函数式接口的匿名实现,具体来说就是代表接口唯一的那个抽象方法。

方法4:Java 8内建的函数式接口Predicate

说到 AppleStrategy 接口,在方法3中使用了Lambda,节省了 AppleStrategy 接口的实现类,但还是要定义 AppleStrategy 接口,能把这个接口的定义也省掉吗?

实际上Java 8已经内建了很多很实用的接口,需要的时候直接用就行了。本例中的 AppleStrategy 接口,在Java 8中已经有类似的存在了,它的名字叫做 Predicate (谓词),定义在 java.util.function.Predicate 里:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    
    // 下面还有一些默认方法
    ......
}

所以,可以删掉 AppleStrategy 接口(及其实现类),并改写 filterApples() 方法如下:

    public static List<Apple> filterApples(List<Apple> list, Predicate<Apple> p) {
        List<Apple> result = new ArrayList<>();
        for (Apple e: list) {
            if (p.test(e)) {
                result.add(e);
            }
        }
        return result;
    }

注:跑个题:如果保留之前的 filterApples() 方法,然后新添加如上 filterApples() 方法,那么这两个方法虽然同名,但第二个参数不同,这是OK的。但是,在调用时,如果第二个参数使用Lambda表达式,则会编译报错,这是因为编译器在处理Lambda的时候,会去查找对应的函数式接口,而这两个方法的第二个参数都能匹配上,编译器无法做出选择。

调用 filterApples() 方法的代码不变:

        List<Apple> list4 = Test0913.filterApples(list0, e -> "Red".equals(e.getColor()));

注意,第二个参数所代表的函数式接口已经发生了变化:

  • 方法3:代表的是 AppleStrategy 接口(自定义)
  • 方法4:代表的是 Predicate 接口(Java 8内建,推荐)

与方法3同理,方法4也可以写成:

        Predicate<Apple> p = (Apple e) -> "Red".equals(e.getColor());
        List<Apple> list4 = Test0913.filterApples(list0, p);

注:Java 8内建了很多函数式接口,常见的比如:

  • Predicate
  • Consumer
  • Function
  • Supplier
  • UnaryOperator
  • BinaryOperator

方法5:Java 8的方法引用

如果Lambda表达式的代码在原本的代码里已经有实现了:

    public static boolean isRedApple(Apple apple) {
        return "Red".equals(apple.getColor());
    }

    public static boolean isHeavyApple(Apple apple) {
        return apple.getWeight() > 250;
    }

那么只需要把该方法作为Lambda传入即可:

        List<Apple> list5 = Test0913.filterApples(list0, e -> Test0913.isRedApple(e));

这和方法4并没有什么区别。非要找区别的话,方法5的Lambda表达式是一个方法调用。

对于像这样只包含一个方法调用的Lambda,Java 8提供了另外一种被称为“方法引用”的简单写法:

        List<Apple> list5 = Test0913.filterApples(list0, Test0913::isRedApple);

Lambda被称为匿名方法,而方法引用显然是命名方法,其实它本质还是Lambda,只不过是在特定条件下的快捷写法,更方便我们理解代码。

方法引用的格式如下:

  • 左边部分:即 Test0913 ,是类或者对象
  • 双冒号:即 :: ,分隔类/对象和方法
  • 方法:即 isRedApple ,注意不要加括号

当Lambda的内容很长,或者需要复用时,显然封装是一个比较好的做法。这时就可以给它起一个有意义的方法名,然后通过方法引用来调用它。

总结:方法引用是Lambda的快捷简写,可以简化代码,方便理解。

方法6:Java 8的Stream

重磅武器终于要出场了!

前面介绍的内容里,都在 filterApples() 方法里,对List进行了遍历,这种遍历是显式的,称为“外部迭代”。Java 8引入了Stream,可以实现“内部迭代”,不再需要显式遍历集合,以便更加集中关注在业务领域。这有点类似于SQL语句:你只需告诉数据库你想要什么数据,而不用关心数据是怎么得来的。

使用Stream的一般步骤是:

  1. 创建流:比如 list0.stream() ,把List转换为流
  2. 操作流:比如 filter()map() ,可以是单个操作,也可以是多个操作的复合,操作的结果仍然是流
  3. 从流生成结果:比如 collect(Collectors.toList()) ,把流转换为List

本例中,需求是要查找满足条件的苹果,该应用场景适用于Stream的 filter() 方法,其方法签名如下:

    Stream<T> filter(Predicate<? super T> predicate);

看到 Predicate ,我们就知道,Lambda可以大显身手了。

最终代码如下(连 filterApples() 方法也不需要了):

        List<Apple> list6 = list0.stream().filter(Test0913::isRedApple).toList();

可见,只需一行代码就搞定了!

注: toList() 不是Java 8提供的方法,是Java 16才有的,如果是使用Java 8,需要稍微麻烦一点,写成:

        List<Apple> list6 = list0.stream()
                .filter(Test0913::isRedApple)
                .collect(Collectors.toList());

Stream除了能够精简代码,还有运行性能的提升。比如需要红色的重苹果:

        List<Apple> list6 = list0.stream()
                .filter(Test0913::isRedApple)
                .filter(Test0913::isHeavyApple)
                .toList();

filter() 方法,返回的类型仍然是流,因此可以复合运算。Java会对流的复合运算做优化。比如:

        list0.stream()
                .map(e -> {System.out.println(e); return e;})
                .limit(3)
                .toList();

运行结果里,只会打印前三个苹果的信息,这是因为Java对复合流做了优化。本例中, limit(3) 影响到了前面的 map() 操作。

如果苹果的数量非常多,还可以充分利用多核CPU并行(注意不是并发)运行,只需要把流( stream() )变成并行流( parallelStream() ):

        List<Apple> list6 = list0.parallelStream()
                .filter(Test0913::isRedApple)
                .filter(Test0913::isHeavyApple)
                .toList();

不需要编写任何与多线程有关的代码,都隐藏在并行流里了。

流的例子

流的功能非常强大,内容非常多。本文只是简介,不多做解释,直接看例子。

注:下面的例子使用的是 stream() ,也可以使用 parallelStream()

  • 不是红颜色的苹果:
        List<Apple> list8 = list0.stream()
                .filter(Predicate.not(Test0913::isRedApple))
                .toList();

注:前面提到过, Predicate 是函数式接口,只有一个 test() 抽象方法,而这里的 not() 方法,是其默认方法(在接口里有默认实现)。

  • 把红苹果按重量从大到小排序:
        List<Apple> list7 = list0.stream()
                .filter(Test0913::isRedApple)
                .sorted(Comparator.comparingDouble(Apple::getWeight).reversed())
                .toList();
  • 绿苹果的个数:
        long count = list0.stream()
                .filter(e -> "Green".equals(e.getColor()))
                .count();
  • 打印每个苹果的重量:
        list0.stream()
                .mapToDouble(e -> e.getWeight())
                .forEach(System.out::println);
  • 是否所有苹果的重量都大于200:
        boolean b = list0.stream()
                .allMatch(e -> e.getWeight() > 200);
  • 是否有重量大于300的红苹果:
        boolean b = list0.stream()
                .filter(Test0913::isRedApple)
                .anyMatch(e -> e.getWeight() > 300);
  • 查找第一个(或者任何一个)重量大于200的苹果:
        Optional<Apple> apple = list0.stream()
                .filter(e -> e.getWeight() > 200)
//                .findFirst();
                .findAny();

注意: findFirst()findAny() 的区别在于并行。在并行流条件下, findAny() 效率更高(找到一个就行,不用关心顺序)。

注意:也许有符合条件的苹果,也许没有,所以返回的是 Optional<Apple> 而非 Apple

  • 所有苹果重量的总和、最大值、最小值、平均值等统计信息:
        DoubleSummaryStatistics summary = list0.stream()
                .mapToDouble(Apple::getWeight)
                .summaryStatistics();

结果如下:

DoubleSummaryStatistics{count=5, sum=1220.000000, min=200.000000, average=244.000000, max=300.000000}
  • 把苹果按颜色分类:
        Map<String, List<Apple>> map1 = list0.stream()
                .collect(Collectors.groupingBy(Apple::getColor));
  • 把苹果按颜色分类,并求每种苹果的平均重量:
        Map<String, Double> map2 = list0.stream()
                .collect(Collectors.groupingBy(Apple::getColor,
                        Collectors.averagingDouble(Apple::getWeight)));

注意:该例有点类似于SQL语句: SELECT COLOR, AVG(WEIGHT) FROM LIST0 GROUP BY COLOR

总结

流的优点非常多:

  • 简化代码,更接近人类语言,对于编写,理解,维护都非常方便
  • 复合性:流可以复合,更灵活,而且复合流可以自动优化
  • 并行性:不需了解和编写多线程代码,隐藏了实现细节

总之,Java 8的流很好很强大,一定要多用它。

参考

  • https://livebook.manning.com/book/java-8-in-action/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值