Java8的函数式编程简介

news2024/9/19 14:16:25

文章目录

  • 环境
  • 背景
  • 方法
    • 方法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 {

    @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() 方法里,对list0进行了遍历,这种遍历是显式的,被称为“外部迭代”。Java 8引入了Stream,可以实现“内部迭代”,也就是无需显式遍历集合,以便更加集中关注在业务领域。这有点类似于SQL语句:你只需告诉数据库你想要什么数据,而不用关心数据是怎么得来的。

现在,我们来使用Stream简化代码,这次连 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();
  • 把红苹果按重量从大到小排序:
        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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2134875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE:5、类与对象

1、类的定义与对象的创建 定义属性 创建对象 2、对象的使用 使用一个变量来指代某个对象&#xff0c;只不过引用类型的变量&#xff0c;存储的是对象的引用&#xff0c;而不是对象本身 public class Main {public static void main(String [] argv){Person p1new Person();P…

Oracle发送邮件功能:配置自动化发信指南?

Oracle发送邮件服务设置方法&#xff1f;怎么用Oracle数据库发信&#xff1f; Oracle数据库作为企业级应用的核心&#xff0c;其内置的发送邮件功能为企业提供了强大的自动化工具。AokSend将详细介绍如何配置Oracle发送邮件功能&#xff0c;以实现自动化发信&#xff0c;从而提…

C和指针:指针

内存和地址 程序视角看内存是一个大的字节数组&#xff0c;每个字节包含8个位&#xff0c;可以存储无符号值0至255,或有符号值-128至127。 多个字节可以合成一个字&#xff0c;许多机器以字为单位存储整数&#xff0c;每个字一般由2个或4个字节组成。 由于它们包含了更多的位&…

react native(expo)多语言适配

项目基于 expo框架 开发。请先配置好 expo 开发环境 1.引入i18n-js npx expo install i18n-js 2.新建languages文件夹&#xff0c;其中包括英文、中文等语种目录。结构如下&#xff1a; *.json文件为语种翻译后的json键值对&#xff0c;用于UI中引用; { "appName&q…

【C语言】(指针系列3)数组指针+函数指针+typedef+函数数组指针+转移表

前言&#xff1a;前言&#xff1a;开始之前先感谢一位大佬&#xff0c;清风~徐~来-CSDN博客&#xff0c;由于是时间久远&#xff0c;博主指针的系列忘的差不多了&#xff0c;所以有顺序部分借鉴了该播主的&#xff0c;同时也加入了博主自己的理解&#xff0c;有些地方如果解释的…

MySQL语句案例编写复习

先看我的表数据和结构 1.查询年龄为16,17,18,19岁的女性员工信息。 select * from emp where gender 女 and age in(16,17,18,19); 2.查询性别为 男 &#xff0c;并且年龄在 20-40 岁(含)以内的姓名为三个字的员工。 select * from emp where gender 男 and age between …

猫罐头多久喂一次?营养健康的罐头推荐

一&#xff0e;猫罐头多久喂一次 猫咪长期只食用干粮&#xff0c;容易饮水不足&#xff0c;从而引发上尿道或膀胱结石、堵塞等问题&#xff0c;所以最好每周喂至少2个猫罐头&#xff0c;帮助猫咪补充水分。如果条件允许&#xff0c;全罐喂养&#xff0c;每天都给猫咪吃猫罐头是…

车机中 Android Audio 音频常见问题分析方法实践小结

文章目录 前言1. 无声2. 断音3. 杂音4. 延迟播放5. 焦点问题6. 无声问题(连上 BT )其他完善中…… 前言 本文主要总结了一下车机开发中遇到的 Audio 有关的问题&#xff0c;同时参考网上的一案例&#xff0c;由于Audio 模块出现音频问题的场景很多&#xff0c;对每一个出现的问…

Blender渲染太慢怎么办?blender云渲染已开启

动画行业蓬勃发展&#xff0c;动画制作软件亦持续推陈出新&#xff0c;当制作平台日益丰富&#xff0c;创作难度降低&#xff0c;创作效率提升&#xff0c;如何高效完成复杂动画的渲染就成了从业者更关心的问题。 云渲染技术的出现&#xff0c;无疑为动画制作者提供了前所未有…

家庭理财管理系统

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 家庭理财管理系统拥有多种角色&#xff0c;可以自行设置权限和用户等&#xff0c;主要功能有&#xff1a; 收入管理、支付管理、资产管理、负债详情、统计报表、家庭成员管理、用户管理等…

JavaSE - 易错题集 - 006

1. 哪个正确 A abstract类只能用来派生子类&#xff0c;不能用来创建abstract类的对象。 B final类不但可以用来派生子类&#xff0c;也可以用来创建final类的对象。 C abstract不能与final同时修饰一个类。 D abstract类定义中可以没有abstract方法。 正确答案&#xff1…

决策树算法上篇

决策树概述 决策树是属于有监督机器学习的一种&#xff0c;起源非常早&#xff0c;符合直觉并且非常直观&#xff0c;模仿人类做决策的过程&#xff0c;早期人工智能模型中有很多应用&#xff0c;现在更多的是使用基于决策树的一些集成学习的算法。 示例一&#xff1a; 上表根据…

1.C++中程序的基本结构

在教孩子的学习过程中&#xff0c;使用的开发IDE为小熊猫Dev-C 6.7.5版本&#xff0c;以后的复杂截图&#xff0c;基本上都是基于此版本进行的&#xff0c;同时在适当的时候&#xff0c;录制视频也会基于此版本来完成。 以下为一个最基本的C程序 int main() {// 程序主体retur…

无痛生娃,00后当妈啦

姐妹们&#xff0c;你们家开始催婚了吗&#xff1f;我是00后&#xff0c;大学也才毕业一年啊&#xff0c;我妈已经开始给我物色对象&#xff0c;过年让我去相亲了&#xff01;大学的时候不让谈&#xff0c;说怕异地以后感情不稳定&#xff0c;结果呢&#xff0c;一毕业要我结婚…

频域滤波为什么使用psf2otf函数?线性卷积和循环卷积等效的条件

线性卷积和循环卷积是本质不同的运算。然而&#xff0c;在某些条件下&#xff0c;线性卷积和循环卷积是等效的。建立这种等效关系具有重要意义。对于两个向量 x 和 y&#xff0c;循环卷积等于二者的离散傅里叶变换 (DFT) 之积的逆 DFT 变换。 禹晶、肖创柏、廖庆敏《数字图像处…

基于python+django+mysql+Nanodet检测模型的水稻虫害检测系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

『功能项目』项目优化 - 框架加载资源【41】

我们打开上一篇40播放动画时禁止点击移动的项目&#xff0c; 本章要做的事情是搭建一个资源加载框架&#xff0c;让UI界面&#xff0c;人物模型以及场景都存放在资源文件夹中在运行时加载出来 首先在资源商店加载资源 将怪物模型放置场景中 将普通管线模型切换成URP 重命名为…

重学SpringBoot3-集成RocketMQ(二)

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成RocketMQ&#xff08;二&#xff09; 1. 基础概念2. 准备工作3. 实现事务消息的生产者4. 事务监听器实现5. 消费者示例6. 发送事务消息7. 测试7.1 模…

rust学习——关联类型

什么是关联类型 关联类型是Rust中一种特殊的泛型抽象机制。在trait中&#xff0c;可以定义一个或多个关联类型&#xff0c;这些关联类型与trait的实现类型相关联。关联类型允许我们在trait中使用泛型&#xff0c;但不需要提前指定具体的类型。 不使用关联类型存在的问题 tra…

AI生成头像表情包,一次十分钟,就能实现月入过万的玩法,无脑操作

今天给大家带来的项目是AI生成表情包和头像&#xff0c;这个项目对于我们做ip来说是真心不错&#xff0c;就比如我这个头像。 为什么说每天只需要10分钟呢&#xff0c;那么我们继续往下看。 "项目介绍 这个项目的核心其实就是使用AI生成表情包或者说生成头像&#xff0c…