当前位置: 首页 > news >正文

Java8的函数式编程简介

文章目录

  • 环境
  • 背景
  • 方法
    • 方法1:Java 7(传统方法)
    • 方法2:Java 7 (策略模式)
    • 方法3:Java 8的Lambda
    • 方法4:Java 8内建的函数式接口Predicate
    • 方法5:Java 8的方法引用
    • 方法6:Java 8的Stream
      • 流的例子
  • 总结
  • 参考

注:本文主要参考了《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 {@Overridepublic boolean test(Apple apple) {return "Red".equals(apple.getColor());}
}
  • 接口的实现类,该类实现了对“重苹果”的测试:
class AppleStrategy_Heavy implements AppleStrategy {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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/

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 切换淘宝最新npm镜像源
  • 整个场面要hold住-《分析模式》漫谈32
  • 【Rust练习】14.流程控制
  • 详细分析linux中的MySql跳过密码验证以及Bug(图文)
  • Vue3+TypeScript+Vite+Less 开发 H5 项目(amfe-flexible + postcss-pxtorem)
  • MySQL数据的增删改查(二)
  • git update-ref
  • Axure科技感大屏系统设计:智慧农场管理平台
  • TDengine 签约寓信科技,推动智慧公寓的数字化转型
  • 升级VMware
  • 【计算机网络 - 基础问题】每日 3 题(一)
  • 【大模型专栏—实战篇】基于RAG从0到1搭建AI科研知识库
  • 原型模式:克隆对象的艺术
  • 如何训练机器学习力场
  • 绑定变量对于SQL性能的影响
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • Javascript 原型链
  • Java读取Properties文件的六种方法
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • Vue全家桶实现一个Web App
  • 从@property说起(二)当我们写下@property (nonatomic, weak) id obj时,我们究竟写了什么...
  • 力扣(LeetCode)22
  • 你不可错过的前端面试题(一)
  • 前端
  • 使用 Docker 部署 Spring Boot项目
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • 推荐一个React的管理后台框架
  • 新版博客前端前瞻
  • 追踪解析 FutureTask 源码
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • ​​​​​​​开发面试“八股文”:助力还是阻力?
  • ​secrets --- 生成管理密码的安全随机数​
  • # Apache SeaTunnel 究竟是什么?
  • # 移动硬盘误操作制作为启动盘数据恢复问题
  • #1015 : KMP算法
  • #14vue3生成表单并跳转到外部地址的方式
  • #我与Java虚拟机的故事#连载01:人在JVM,身不由己
  • (~_~)
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (poj1.3.2)1791(构造法模拟)
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (理论篇)httpmoudle和httphandler一览
  • (十二)Flink Table API
  • (五)Python 垃圾回收机制
  • (原創) 是否该学PetShop将Model和BLL分开? (.NET) (N-Tier) (PetShop) (OO)
  • (转)Sql Server 保留几位小数的两种做法
  • (转)平衡树
  • (轉)JSON.stringify 语法实例讲解
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .NET 跨平台图形库 SkiaSharp 基础应用
  • .ui文件相关
  • ?.的用法
  • @Builder注释导致@RequestBody的前端json反序列化失败,HTTP400