山寨Stream API设计分析

news2024/11/17 9:42:25
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

私以为,Java8最显著的新特性就2个:

  • Lambda表达式
  • Stream API

其中,Lambda表达式最“难”。它的“难”在于抽象,初学者根本不知道为什么要这么写、写了又如何起作用。

好在《Lambda表达式》已经解答了这个问题。

世界上有两种答案,真理or你期望的答案。个人觉得,对于Lambda表达式和Stream API,我们只要得到期望的答案即可。Lambda表达式底层到底是不是匿名内部类呢?其实不是。但这不妨碍我们认为Lambda表达式是“掐头去尾”的匿名内部类,然后顺手再把Stream API学了,马上应用到实战中。这,才是最重要的。

Lambda底层原理其实是自动生成一个私有静态方法,并实现函数式接口,然后在函数式接口实现类的方法中调用这个私有静态方法。如果感兴趣可以自己百度一下,会有一些反编译操作。

之前提到过,我曾放弃学习Java8,因为太抽象了,被各种视频绕晕了。首先,视频没有把Lambda表达式讲清楚,其次Stream API也讲得不好。初学者老想知道底层发生了什么,而老师却连每次调用的方法做了什么也解释得很含糊。

接下来,希望通过对山寨版的Stream API的分析,帮大家更好地理解Java8的Stream API。

本篇文章具有一定难度,请确保已经看过Lambda表达式及山寨Stream API。

模拟Stream API:filter()

请按顺序阅读以下代码:

/**
 * 新建Predicate接口
 *
 * @param <T>
 */
@FunctionalInterface
interface Predicate<T> {
    /**
     * 定义了一个test()方法,传入任意对象,返回true or false,具体判断逻辑由子类实现
     *
     * @param t
     * @return
     */
    boolean test(T t);
}


/**
 * Predicate接口的实现类,泛型规定只处理Person
 */
class PredicateImpl implements Predicate<Person> {

    /**
     * 判断逻辑是:传入的person是否age>18,是就返回true
     *
     * @param person
     * @return
     */
    @Override
    public boolean test(Person person) {
        return person.getAge() > 18;
    }
}


@Data
@AllArgsConstructor
class Person {
    private String name;
    private Integer age;
}

目前为止,和Stream API没任何关系,相信大家都能看得懂。

测试:

public class MockStream {

    public static void main(String[] args) {

        Person bravo = new Person("bravo", 18);

        // 1.具体实现类,调用它的test()方法
        Predicate<Person> predicate1 = new PredicateImpl();
        // test()内部代码是:bravo.getAge() > 18
        myPrint(bravo, predicate1); // false

        // 2.匿名类,调用它的test()方法
        Predicate<Person> predicate2 = new Predicate<Person>() {
            @Override
            public boolean test(Person person) {
                return person.getAge() < 18;
            }
        };
        myPrint(bravo, predicate2); // false

        // 3.Lambda表达式,调用它的test()方法。
        // 按照Lambda表达式的标准,只要你是个函数式接口,那么就可以接收Lambda表达式
        Predicate<Person> predicate3 = person -> person.getAge() == 18;
        myPrint(bravo, predicate3); // true
    }

    public static void myPrint(Person person, Predicate<Person> filter) {
        if (filter.test(person)) {
            System.out.println("true");
        } else {
            System.out.println("false");
        }
    }
}

上面的代码,估计有一部分基础稍差的同学开始晕了,我们来理一下。

myPrint(Person person, Predicate<Person> filter)的精髓是,用Predicate函数式接口先行占坑。我们无需关注Predicate传入myPrint()后将被如何调用,只要关注如何实现Predicate,又由于Predicate只有一个test()方法,所以最终我们只需关注如何实现test()方法。

而实现test()的诀窍在于,盯牢它的入参出参。它有一个入参(待测试目标),而返回值是一个boolean(测试结果)。这还不简单,我随随便便就能瞎写一个:

// 1.普通的方法
public boolean test(Person p) {
    // 啥逻辑都没有,直接返回true,你也可以按照实际业务写,比如p.age>18
    return true; 
}

// 2.Lambda形式
p -> return true;

之前提到过,在函数式接口的前提下,接口多态可以直接看作是方法多态,那么Filter#test()其实就是抽象方法占坑,实现类、匿名类或者Lambda表达式就可以指向它。就像之前说的,不管你先放鸡翅还是先倒可乐,我先用twoStep()占坑,你后面确定了自己传进来。

上面3个例子,重要的不是结论,而是想揭露一个事实

不论具体实现类、匿名类还是Lambda表达式,其实做的事情本质上是一样的:

1.先让函数式接口占坑

2.自己不慌不忙制定映射规则,规则可以被藏在具体类、匿名类中,或者Lambda表达式本身

有了上面的铺垫,我们来仔细看看之前山寨Stream API对filter()的实现:

class MyList<T>{
    private List<T> list = new ArrayList<>();
    
	// 1.外部调用add()添加的元素都会被存在list
    public boolean add(T t) {
        return list.add(t);
    }

    /**
     * 过滤方法,接收过滤规则
     * @param predicate
     * @return
     */
    public List<T> filter(Predicate<T> predicate){
        List<T> filteredList = new ArrayList<>();

        for (T t : list) {
            // 2.把规则应用于list
            if (predicate.test(t)) {
                // 3.收集符合条件的元素
                filteredList.add(t);
            }
        }

        return filteredList;
    }
}
  • filter(Predicate predicate)方法需要一个过滤规则,但这个规则不能写死,所以随便搞了一个接口占坑
  • 具体的过滤规则被延迟到传入具体类实例或Lambda时才确定

你可以理解为就是对策略模式的应用,但函数式接口更喜欢接收Lambda表达式,代码更加简洁。

Predicate接口已经提前占好坑位,而Lambda们则是去填坑。对于filter()这个案例而言,它需要外界传入具体的“判断器”,然后filter()内部让元素挨个排队去做检查,检查通过就加入filteredList,然后一起返回。

不关注Predicate接口接收的是谁(匿名对象/具体实现/Lambda),只关心它能干什么(方法)。这也是面向对象和函数式编程的思维差异

最终代码(最好在SpringBoot环境下运行):

public class MockStream {

    public static void main(String[] args) throws JsonProcessingException {

        MyList<Person> personMyList = new MyList<>();
        personMyList.add(new Person("李健", 46));
        personMyList.add(new Person("周深", 28));
        personMyList.add(new Person("张学友", 59));

        // 过渡1:把Lambda赋值给变量,然后在传递
        Predicate<Person> predicate = person -> person.getAge() > 40;
        List<Person> filteredList = personMyList.filter(predicate);
        prettyPrint(filteredList);

        System.out.println("\n---------------------------------------------\n");

        // 不像Stream API?这样写呢?
        List<Person> filter = personMyList.filter(person -> person.getAge() > 40);
        prettyPrint(filter);
        // 不要问我为什么没有stream()和collect(),问就是不会写。

        System.out.println("\n---------------------------------------------\n");

        // 有请真正的Stream API(要用真的List了,不能用山寨MyList)
        List<Person> list = new ArrayList<>();
        list.add(new Person("李健", 46));
        list.add(new Person("周深", 28));
        list.add(new Person("张学友", 59));

        List<Person> collect = list.stream().filter(person -> person.getAge() > 40).collect(Collectors.toList());
        prettyPrint(collect);
    }


    /**
     * 按JSON格式输出
     *
     * @param obj
     * @throws JsonProcessingException
     */
    private static void prettyPrint(Object obj) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String s = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        System.out.println(s);
    }

}

// 省略Predicate、Person、MyList

模拟Stream API:map()

再模拟一个map()。因为实际编程中,个人认为filter()和map()是最常用的,希望大家能理解它们的“原理”。

/**
 * 定义一个Function接口
 * 从接口看Function<E, R>中,E(Enter)表示入参类型,R(Return)表示返回值类型
 * 
 * @param <E> 入参类型
 * @param <R> 返回值类型
 */
@FunctionalInterface
interface Function<E, R> {
    /**
     * 定义一个apply()方法,接收一个E返回一个R。也就是把E映射成R
     *
     * @param e
     * @return
     */
    R apply(E e);
}

/**
 * Function接口的实现类,规定传入Person类型返回Integer类型
 */
class FunctionImpl implements Function<Person, Integer> {

    /**
     * 传入person对象,返回age
     *
     * @param person
     * @return
     */
    @Override
    public Integer apply(Person person) {
        return person.getAge();
    }
}
public class MockStream {

    public static void main(String[] args) {
        Person bravo = new Person("bravo", 18);

        // 1.具体实现类,Function<Person, Integer>中,Person是入参类型,Integer是返回值类型
        Function<Person, Integer> function1 = new FunctionImpl();
        myPrint(bravo, function1);

        // 2.匿名类
        Function<Person, Integer> function2 = new Function<Person, Integer>() {
            @Override
            public Integer apply(Person person) {
                return person.getAge();
            }
        };
        myPrint(bravo, function2);


        // 3.Lambda表达式 person(入参类型) ->  person.getAge()(返回值类型)
        Function<Person, Integer> function3 = person -> person.getAge();
        myPrint(bravo, function3);
    }

    public static void myPrint(Person person, Function<Person, Integer> mapper) {
        System.out.println(mapper.apply(person));
    }
}

目前为止应该还蛮好理解的。

之前我们在MyList中写了个filter()方法并用Predicate接口占了坑,它接收一个“过滤器”来过滤元素。而现在,map()方法用Function接口占坑,它需要接收一个“转换器”来帮元素“变身”:

class MyList<T> {
    private List<T> list = new ArrayList<>();

    public boolean add(T t) {
        return list.add(t);
    }


    /**
     * 把MyList中的List<T>转为List<R>
     * 不要关注Function<T, R>接口本身,而应该关注apply()
     * apply()接收T t,返回R t。具体实现需要我们从外面传入,这里只是占个坑
     *
     * @param mapper
     * @param <R>
     * @return
     */
    public <R> List<R> map(Function<T, R> mapper) {
        List<R> mappedList = new ArrayList<>();

        for (T t : list) {
            // mapper通过apply()方法把T t 变成 R t,然后加入到新的list中
            mappedList.add(mapper.apply(t));
        }

        return mappedList;
    }

}

传入Lambda,把坑填上

无论filter(Predicate predicate)还是map(Function mapper),其实就是接口占坑、Lambda填坑的过程,其中函数式接口只有唯一方法,所以可以直接把接口多态看做方法多态。比如Predicate只有一个抽象方法boolean test(),那么你写一个符合的具体实现即可:一个入参,返回值为boolean。

以后但凡遇到函数式接口占坑的,只要关注入参和出参即可:

Person ==> {坑} ==> boolean

很明显,入参是Person,出参是Integer。那Person类型怎么变成Integer类型呢?填坑的方案并不唯一,你可以编写任意逻辑,比如:

Person ==> {person -> person.getAge()} ==> Integer

当然,你也可以传递person.getAge()+1。总之,坑已经占好了,注意一下入参和出参满足条件即可。

相信经过本次分析,大家已经能自己写出山寨Stream API了。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

多类场景、遍布各地,融云 IM 支撑多款应用全球增长

&#xff08;全网都在找的《社交泛娱乐出海作战地图》&#xff0c;点击获取&#x1f446;&#xff09; 无论是面向企业场景的工作流协同还是消费场景的网络效应形成&#xff0c;商务社交还是陌生人社交&#xff0c;IM 都是必备组件。IM 遍布互联网各角落&#xff0c;出现在所有…

超声波雪深监测站冬季雪景的智能守护者

随着冬季的到来&#xff0c;白雪皑皑的景象让人感到无比美丽。然而&#xff0c;雪景的美丽却给人们的生活和出行带来了一定的困扰。雪的深度和分布是影响道路交通、公共安全和旅游体验的关键因素。为了解决这一问题&#xff0c;WX-XS1 超声波雪深监测站在冬季应运而生&#xff…

Nvidia VPI 双目相机生成深度图

nVidia VPI&#xff08;Vision Programming Interface&#xff09;提供了多种后端&#xff0c;用于执行图像处理和计算机视觉操作。不同的后端针对不同的硬件和用例进行了优化。这些后端包括&#xff1a; 1. CPU: 这是最通用的后端&#xff0c;它运行在标准的中央处理器&#…

【2023CANN训练营第二季】——Ascend C自定义算子工程介绍及实验

一、自定义算子工程介绍与创建 自定义算子工程是一个包含用户编写的host侧和kerne|侧算子实现文件的&#xff0c;用于编译和安装自定义算子run包的工程框架。 CANN软件包中提供了工程创建工具msopgen&#xff0c;开发者可以输入算子原型定义文件生成Ascend C算子开发工程。 需…

AMP State Evolution的计算:以伯努利先验为例

AMP State Evolution (SE)的计算 t 1 t1 t1时&#xff0c; E ( t ) E [ X 2 ] \mathcal E^{(t)} \mathbb E [X^2] E(t)E[X2]&#xff0c;SE的迭代式为 τ r ( t ) σ 2 1 δ E ( t ) E ( t 1 ) E ∣ η ( t ) ( X Z ) − X ∣ 2 , Z ∼ N ( 0 , τ r ( t ) ) \begin{a…

盘点72个Android系统源码安卓爱好者不容错过

盘点72个Android系统源码安卓爱好者不容错过 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 链接&#xff1a;https://pan.baidu.com/s/1qiWeLjF2i4dlgmTYgPPSvw?pwd8888 提取码&#xff1a;8888 项目名称 A keyboardlisten…

AIGC系列之:升级版的Stable Diffusion之SDXL介绍

目录 AIGC工具对比 DALL-E MidJourney Stable Diffusion 相关资料 SDXL介绍 SDXL生图效果 SDXL训练LoRA流程 AIGC工具对比 在目前的三大新兴文本转图像模型中&#xff0c;Stable Diffusion诞生得最晚&#xff0c;但由于拥有发展良好的开源社区&#xff0c;它的用户关注…

Linux系统安装Docker-根据官方教程教程(以Ubuntu为例)

Linux系统安装Docker-根据官方教程教程&#xff08;以Ubuntu为例&#xff09; 1. 背景介绍2. 环境配置2.1 软件环境要求2.2 软件下载2.3 文档地址2.3 必备命令工具下载 3. 安装Docker3.1 使用root用户操作后续命令3.2 卸载可能存在的旧版本 4. 安装Docker4.1 更新依赖包4.4 配置…

530. 二叉搜索树的最小绝对差 在java中,一个类里面的属性,直接定义在类下面和定义在该类的方法里,有什么区别

530. 二叉搜索树的最小绝对差 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a;错误经验吸取在java中&#xff0c;一个类里面的属性&#xff0c;直接定义在类下面和定义在该类的方法里&#xff0c;有什么区别&#xff1f;&#xff1f;&…

webshell之反射免杀

主要讲解反射在webshell中的利用&#xff0c;以及反射绕过杀软的利用与原理 原始反射马 免杀效果: 特征太明显里面还有java.lang.Runtime&#xff0c;getRuntime&#xff0c;exec这些敏感内容&#xff0c;由于与反射相关的参数都是字符串&#xff0c;由此我们能操作的空间就很…

接口01-Java

接口-Java 一、引入(快速入门案例)二、接口介绍1、概念2、语法 三、应用场景四、接口使用注意事项五、练习题1 一、引入(快速入门案例) usb插槽就是现实中的接口。 你可以把手机、相机、u盘都插在usb插槽上&#xff0c;而不用担心那个插槽是专门插哪个的&#xff0c;原因是做u…

2023年【危险化学品经营单位安全管理人员】考试总结及危险化学品经营单位安全管理人员模拟试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【危险化学品经营单位安全管理人员】考试总结及危险化学品经营单位安全管理人员模拟试题&#xff0c;包含危险化学品经营单位安全管理人员考试总结答案和解析及危险化学品经营单位安全管理人员模拟试题练习。安…

.NET6实现破解Modbus poll点表配置文件

📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:新的征程,我们面对的不仅仅是技术还有人心,人心不可测,海水不可量,唯有技术,才是深沉黑夜中的一座闪烁的灯塔 !序言 Modbus 协议是工控领域常见…

jq+canvas:实现图片上传+裁剪+保存等功能

效果图 上传图片之前&#xff1a; 上传图片之后&#xff0c;点击放大/缩小后的效果&#xff1a; 裁剪之后的效果&#xff1a; 代码实现如下&#xff1a; 1.html部分 <input type"file" id"fileInput" accept"image/png, image/gif, image/j…

GDOUCTF2023-Reverse WP

文章目录 [GDOUCTF 2023]Check_Your_Luck[GDOUCTF 2023]Tea[GDOUCTF 2023]easy_pyc[GDOUCTF 2023]doublegame[GDOUCTF 2023]L&#xff01;s&#xff01;[GDOUCTF 2023]润&#xff01;附 [GDOUCTF 2023]Check_Your_Luck 根据 if 使用z3约束求解器。 EXP&#xff1a; from z3 i…

OkHttp的配置

一、拦截器 1.添加拦截器的作用&#xff1a; 每次在请求过程中就会回调一次intercept方法 2.拦截器的回调方法里我们可以做那些事情&#xff1a; 当前的请求还没有发给服务器&#xff0c;比如我们在与服务器通信的时候&#xff0c;一个应用中很多地方都会跟服务器发起通信。…

WGS84转CGCS2000操作步骤

1、使用一个转换软件实现不同椭球之间转换七参数的求取。打开坐标转换软件如下&#xff1a; 2、点击设置-地图投影&#xff0c; 如下&#xff1a; 3、设置需要投影到的坐标系&#xff0c;如下&#xff1a; 4、【选择目标坐标系】中选择CGCS2000&#xff0c;如下&#xff1a; 5、…

智能优化算法应用:基于正余弦算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于正余弦算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于正余弦算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.正余弦算法4.实验参数设定5.算法结果6.参考文献7.…

深入Rust的模式匹配与枚举类型

今天&#xff0c;我们将深入探讨Rust语言中的两个强大特性&#xff1a;模式匹配&#xff08;Pattern Matching&#xff09;和枚举类型&#xff08;Enums&#xff09;。这两个特性是Rust提供的核心工具之一&#xff0c;它们在处理多种类型的数据和复杂的逻辑控制中发挥着关键作用…

灰度发布专题---2、Dubbo灰度发布

通过上面描述&#xff0c;我们理解了什么是灰度发布&#xff0c;接下来我们基于Dubbo实现灰度发布。Dubbo的灰度发布常见的方式有版本控制灰度发布、路由灰度发布、基于Apollo实现灰度发布&#xff0c;我们把这每种灰度发布都实现一次。 在学习Dubbo灰度发布之前&#xff0c;我…