浅谈 Java 中的 Lambda 表达式

news2024/11/19 5:36:51

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验

Lambda 表达式是一种匿名函数,它可以作为参数传递给方法或存储在变量中。在 Java8 中,它和函数式接口一起,共同构建了函数式编程的框架。


什么是函数式编程


函数式编程是一种编程范式,也是一种思想。

它将计算视为函数求值的过程,并强调函数的纯粹性和不可变性。在函数式编程中,函数被视为一等公民,可以作为参数传递、存储在变量中,并且函数的执行不会产生副作用。

例如,我们想要输出 List 中的全部元素,命令式编程看起来是下面这样:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer item : list) {
            System.out.println(item);
        }       
    }
}

而在函数式编程的思想下,代码则看起来是下面这样:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(System.out::println);
    }
}

从以上的两个例子中,可以看出,命令式编程需要我们自己去实现具体的逻辑细节。而函数式编程则是调用 API 完成需求的实现,将原本命令式的代码写成一系列嵌套的函数调用。

由此可见,在函数式编程的思想下,我们将功能的具体细节隐藏,将其抽象为了函数式接口,这就使得具有规范、稳定、可组合、高复用的特点。


Lambda 与匿名内部类


既然函数式编程需要将功能抽象为接口,那么我们来回顾一下接口的使用。

接口作为 java 中的一种抽象类型,它定义了一组方法的签名(方法名、参数列表和返回类型),但没有具体的实现

因此,要使用接口,就必须提供相应的实现类,或者包含实现接口的对象返回。例如,要想使用 List 接口,我们可以使用实现了该接口的实现类 ArrayListLinkdeList 等,或者像上节例子一样,使用 Arrays.asList 的工厂方法返回了一个实现了 List 接口的 ArrayList 对象。

其中,对于实现类来说,由于接口只需要实现某种功能,我们完全可以使用匿名内部类来实现,例如,我们把输出 List 的全部元素抽象为一个接口 Show,其中提供了一个函数方法 ShowAllItems

public interface Show {
    void ShowAllItems(List<Integer> arrayList);
}

继续沿用之前代码示例,现在要求输出 List 中的全部元素:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Show show = new Show() {
            @Override
            public void ShowAllItems(List<Integer> arrayList) {
                for (Integer item : arrayList) System.out.println(item);
            }
        };
        show.ShowAllItems(list);
    }
}

上述代码中,由于接口 Show 其中只有一个抽象方法 ShowAllItems,如果单独为该接口实现一个类未免显得太过笨拙,因此我们在使用时直接使用匿名内部类的实现,通过这种方式创建一个临时的实现子类,这就令接口的使用更加灵活。

那么问题来了,如果我们后续仍要使用多次该接口,每次使用都以匿名内部类的方式来实现,会导致我们的代码太过臃肿,有没有更好的解决办法呢?

当然有的,这就是我们今天讨论的主人公—— Lambda 表达式,如果一个接口中有且只有一个待实现的抽象方法,那么我们可以将匿名内部类简写为 Lambda 表达式:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Show show = param -> {
            for (Integer item : param) System.out.println(item);
        };
        show.ShowAllItems(list);
    }
}

也许上面的示例会使你感到困惑,下面我们来详细探讨一下 Lambda 表达式的基础语法。


Lambda 表达式基础语法


  • 标准格式为:([参数类型 参数名称,]...) ‐> { 代码语句,包括返回值 }
  • 和匿名内部类不同,Lambda 表达式仅支持接口,不支持抽象类。
  • 接口内部必须有且仅有一个抽象方法(可以有多个方法,但是必须保证其他方法有默认实现,必须留一个抽象方法出来)
  • Lambda 表达式可以在函数体中引用外部的变量,从而实现了闭包,但对进入闭包的变量有 final 的限制。

接下来,我们看一个简单示例,假设接口 Test 中有且仅有如下抽象方法:

public interface Test {
    String showTestNumber(Integer param);
}

利用上述接口,我们使用如下匿名内部类来实现该方法:

public class Main {
    public static void main(String[] args) {
        Test test = new Test() {
            @Override
            public String showTestNumber(Integer param) {
                return "Test number is " + param;
            }
        };
        System.out.println(test.showTestNumber(114));
    }
}

如果将其转换为 Lambda 的标准格式,则为:

public class Main {
    public static void main(String[] args) {
        Test test = (Integer param) -> {
            return "Test number is " + param;
        };
        System.out.println(test.showTestNumber(514));
    }
}

由于该方法只需传递一个参数,因此可以省略参数类型及其括号:

public class Main {
    public static void main(String[] args) {
        Test test = param -> {
            return "Test number is " + param;
        };
        System.out.println(test.showTestNumber(1919));
    }
}

又因为方法实现只有一条 return 语句,则后面的 { ... } 也可以省略:

public class Main {
    public static void main(String[] args) {
        Test test = param -> "Test number is " + param;
        System.out.println(test.showTestNumber(810));
    }
}

此外,如果方法已经实现,我们可以利用方法引用:

public class Main {
    public static void main(String[] args) {
        Test test = Main::showTestNumber;
        System.out.println(test.showTestNumber(721));
    }

    // 提取方法实现
    private static String showTestNumber(Integer param) {
        return "Test number is " + param;
    }
}

在上述示例代码中,Main::showTestNumber是一个方法引用,它引用了 Main 类中的静态方法 showTestNumber。该方法被赋值给 Test接口的实例变量 test

关于方法引用的使用,我们在后面还会重新提到。但这里我需要先介绍一下关于闭包的特性。

闭包是一个函数(或过程),它可以访问并操作其作用域外部的变量。在 Java 中,可以通过 Lambda 表达式或方法引用来创建闭包。

其实,在 main 方法中,我们还可以通过调用 test.showTestNumber 来调用闭包。闭包中的方法 showTestNumber 可以访问并操作其作用域外部的变量。

为了更清晰地展示 Lambda 的闭包过程,我们使用如下示例:

public class Main {
    public static void main(String[] args) {
        String Claim = "Test number is ";
        Test test = param -> Claim + param;
        System.out.println(test.showTestNumber(2333));
    }
}

在上述示例代码中,Lambda 表达式捕获了外部变量 Claim,并在 Lambda 表达式的范围之外(main()方法内部)调用闭包时仍然可以访问和使用该变量。

注意Java8 不要求显式将闭包变量声明为 final,但如果你尝试修改闭包变量的值,则会报错。

public class Main {
    public static void main(String[] args) {
        String Claim = "Test number is ";
        Claim = "Yeah~ The number is ";  // 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量
        Test test = param -> Claim + param;  
        System.out.println(test.showTestNumber(2333));
    }
}

Lambda 的应用


好了,你已经学会 1 + 1 = 2 1 + 1 = 2 1+1=2 了,现在来康康更实际的东西吧(


无参的函数式接口


以最常用的 Runnable 接口为例:

Java8 之前,如果需要新建一个线程,使用匿名内部类的写法是这样:

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("哼哼哼啊啊啊~");
            }
        };
        runnable.run();
    }
}

如果使用 Lambda 表达式则看起来是这样;

public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("哼哼哼啊啊啊~");
        runnable.run();
    }
}

我们来看一下具体的 Runnable 接口:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

可以看到该接口上面有 @FunctionalInterface 注解,该注解标识了一个接口是函数式接口。因此,我们可以使用 Lambda 表达式将匿名内部类进行替换。

值得注意的是,@FunctionalInterface 注解并不是必须的,它只是作为一种提示和约束的工具。当我们在定义接口时,如果希望该接口只包含一个抽象方法,以便可以使用 Lambda 表达式或方法引用进行函数式编程,可以选择添加 @FunctionalInterface 注解来明确表达这个意图。

即使没有添加 @FunctionalInterface 注解,只要该接口符合函数式接口的定义(只有一个抽象方法),它仍然可以用于函数式编程。


带参的函数式接口


这里假设我们需要对一个数组进行排序:

Java8 之前,对数组进行排序可以使用 Arrays.sort 方法,如果需要指定排序规则,只需要实现其中的 Comparator 方法即可:

public class Main {
    public static void main(String[] args) {
        Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
        Arrays.sort(array, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });
        System.out.println(Arrays.toString(array)); //按从小到大的顺序排列
    } 
}

转换为 Lambda 表达式可以是下面这样:

public class Main {
    public static void main(String[] args) {
        Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
        Arrays.sort(array, (o1, o2) -> o1 - o2);
        System.out.println(Arrays.toString(array)); //按从小到大的顺序排列
    }
}

方法引用


Java 方法引用是一种简化 Lambda 表达式的语法,用于直接引用已经存在的方法。方法引用可以通过以下几种方式来表示:

  1. 静态方法引用:引用静态方法,使用类名或者接口名作为前缀,后面跟上方法名。例如我们在之前例子中介绍过的 Main::showTestNumber

  2. 实例方法引用:引用非静态方法,使用对象名或者对象引用作为前缀,后面跟上方法名。例如,objectName::instanceMethodName

  3. 特定类的任意对象方法引用:引用特定类的实例方法,使用类名作为前缀,后面跟上方法名。例如,ClassName::instanceMethodName

  4. 构造方法引用:引用构造方法,使用类名后面跟上 new 关键字。例如,ClassName::new

  5. 数组构造方法引用:引用数组的构造方法,使用数组类型后面跟上 new 关键字。例如,TypeName[]::new

需要注意的是,方法引用的适用条件是被引用的方法的签名(参数类型和返回类型)必须与函数式接口中的抽象方法的参数类型和返回类型相匹配。

我们使用上节数组排序的情景进行举例,即使我们已经利用 Lambda 表达式进行了大幅度的简化,但是这还不够,我们观察 Integer 类,其中有一个叫做 compare 的静态方法:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

该方法是一个静态方法,但是它却和 Comparator 需要实现的方法返回值和参数定义一模一样,因此我们直接进行方法引用:

public class Main {
    public static void main(String[] args) {
        Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
        Arrays.sort(array, Integer::compare);
        System.out.println(Arrays.toString(array)); //按从小到大的顺序排列
    }
}

如果不使用静态方法,而使用普通的成员方法,即在 Comparator 中,我们需要实现的方法为:

public int compare(Integer o1, Integer o2) {
     return o1 - o2;
}

其中 o1o2 都是 Integer 类型,而在 Integer 类中有一个 compareTo 方法:

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

我们可以将之前的匿名内部类实现替换为 Lambda 如下:

public class Main {
    public static void main(String[] args) {
        Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
        Arrays.sort(array, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        });
        System.out.println(Arrays.toString(array)); //按从小到大的顺序排列
    }
}

由于该方法并非静态方法,而是所属的实例对象所有,如果我们想要引用该方法,我们需要进行实例方法引用:

public class Main {
    public static void main(String[] args) {
        Integer[] array = new Integer[]{4, 5, 9, 3, 2, 8, 1, 0, 6};
        Arrays.sort(array, Integer::compareTo);
        System.out.println(Arrays.toString(array)); //按从小到大的顺序排列
    }
}

虽然看起来和刚才的静态方法引用没有什么区别,但实际上,当我们使用非静态方法时,会使用抽象方参数列表的第一个作为目标对象,后续参数作为目标对象成员方法的参数,即 o1 作为目标对象,o2 作为参数,正好匹配了 compareTo 方法。

对于构造方法引用,假设接口 Test 中有抽象方法 newTest

public interface Test {
    String newTest(String param);
}

对于普通的 Lambda 替换,代码如下:

public class Main {
    public static void main(String[] args) {
        Test test = param -> param;
        System.out.println(test.newTest("哼哼哼啊啊啊~"));
    }
}

而我们注意到该方法其实就是 String 中的构造方法,因此我们直接进行构造方法引用:

public class Main {
    public static void main(String[] args) {
        Test test = String::new;
        System.out.println(test.newTest("哼哼哼啊啊啊~"));
    }
}

Lambda 表达式的本质


经过上面的学习,相信你已经可以熟练地使用 Lambda 表达式了,看起来 Lambda 只是一种简化匿名内部类进行实现接口的语法糖,但实际上,它们是两种本质不同的事物:

  • 匿名内部类本质是一个类,只是不需要我们显示地指定类名,编译器会自动为该类取名。
  • Lambda 表达式本质是一个函数,当然,编译器也会为它取名,在 JVM 层面,这是通过 invokedynamic 指令实现的,编译器会将 Lambda 表达式转化为一个私有方法,并在需要的时候动态地生成一个函数式接口的实例。

假设我们使用上述 Runnabke 的匿名内部类的代码进行编译,可以看到结果如下:

image-20230827223157677

可以看到, Main$1.class 实际上就是 Main 类中生成的匿名内部类文件,而将其替换为 Lambda 表达式后编译的结果如下:

image-20230827224417232

没有生成单独的类文件,即,匿名内部类对应的是一个 class 文件,而 Lambda 表达式对应的是它所在主类的一个私有方法。


参考文献


  • Java中的函数式编程
  • Java Lambda 表达式介绍
  • 在Java代码中写Lambda表达式是种怎样的体验

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

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

相关文章

DebugInfo 模块功能系统介绍 文本上色 文本与表格对齐 分隔线 秒表计算器 语义日期

背景 今天系统性的为大家介绍一下 DebugInfo 模块。这个模块提供了一些丰富的基本功能的封装&#xff0c;希望能给有需要的人带来些许帮助。 文本上色 DebugInfo 模块引入了 colorama提供文本颜色支持。 # -*- coding:UTF-8 -*-# region 引入必要依赖 from DebugInfo.DebugI…

Modbus转Profinet网关在大型自动化仓储项目应用案例

在自动化仓储项目中&#xff0c;Modbus是一种常见的通信协议&#xff0c;用于连接各种设备&#xff0c;例如传感器、PLC和人机界面。然而&#xff0c;Modbus协议只支持串行通信&#xff0c;并且数据传输速度较慢。为了提高通信效率和整体系统性能&#xff0c;许多大型仓储项目选…

Vue的使用(2)

一个简单的Vue项目的创建 创建一个UserList.vue组件 在App.vue中使用该组件 使用组件的第一步&#xff0c;导入组件使用组件的第二部&#xff0c;申明这个组件使用组件的第三步&#xff1a;引用组件 结果&#xff1a; 事件和插值语法 <template> <div><!-- te…

五、性能测试之linux分析命令

linux分析命令 一、服务器基础知识二、linux文件结构三、linux文件权限四、linux命令1、安装应用fedora家族: 如centosdebain家族&#xff1a;如ubuntu 2、获取帮助第一种&#xff1a;command --help第二种&#xff1a;man command第三种&#xff1a;info 3、服务器性能分析基础…

VBA技术资料MF47:VBA_文件夹命令MkDir

【分享成果&#xff0c;随喜正能量】善待这漫长又短暂的岁月&#xff0c;劝君莫惜金缕衣&#xff0c;劝君惜取少年时。世事总是无常&#xff0c;人生没有如果&#xff0c;用一颗云水禅心&#xff0c;过平淡充实的日子&#xff0c;只要且行且珍惜&#xff0c;人生就会无限美好&a…

Android 编译系统(Build System)剖析

Android Build System剖析 Android预构建应用是如何制作的&#xff0c;背后的构建系统又是什么&#xff1f; 本文旨在分享关于Android构建系统以及与原始设备制造商&#xff08;OEM&#xff09;集成的知识&#xff0c;简化理解AOSP复杂机制的过程。与手动查阅各种文件及其内部…

MySQL安装记录

背景 Windows系统重装了, 想恢复一下之前的MySQL环境, 而且本地数据库也是比较常用的, 刚好本次也在安装, 做一个简单的记录. 也算是自己的学习记录输出. 遇到的问题当然也可以同时记录在这里, 方便后 续回顾. 资料包 百度网盘 // TODO 估计放了也会被CSDN屏蔽, 这里就不放…

7 集群基本测试

1. 上传小文件到集群 在hadoop路径下执行命令创建一个文件夹用于存放即将上传的文件&#xff1a; [atguiguhadoop102 ~]$ hadoop fs -mkdir /input上传&#xff1a; [atguiguhadoop102 hadoop-3.1.3]$ hadoop fs -put wcinput/work.txt /input2.上传大文件 [atguiguhadoop1…

数学分析:场论

我们之前知道的是里斯表示定理。 这里看到&#xff0c;对于多重线性映射&#xff0c;里斯表示定理会从内积变成混合积。当然我们还是只考虑三维以内的情况。 于是我们可以把不同的1形式和2形式的下标写上&#xff0c;表示他们相当于内积或者混合积对应的那个向量。 然后还差0形…

LeetCode第1~5题解

CONTENTS LeetCode 1. 两数之和&#xff08;简单&#xff09;LeetCode 2. 两数相加&#xff08;中等&#xff09;LeetCode 3. 无重复字符的最长子串&#xff08;中等&#xff09;LeetCode 4. 寻找两个正序数组的中位数&#xff08;困难&#xff09;LeetCode 5. 最长回文子串&am…

【awd系列】Bugku S3 AWD排位赛-9 pwn类型

文章目录 二进制下载检查分析运行二进制ida分析解题思路exp 二进制下载 下载地址&#xff1a;传送门 检查分析 [rootningan 3rd]# file pwn pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for …

android系统启动流程之SystemServer运行过程

SystemServer进程的启动流程&#xff1a;直接看代码&#xff1a; SystemServer是Java中的一个进程&#xff0c;执行入口是SystemServer.java.main(); SystemServer.java.main();-->new SystemServer().run();-->createSystemContext();//创建系统上下文:虽然SystemServe…

Unittest 笔记:unittest拓展生成HTM报告发送邮件

HTMLTestRunner 是一个unitest拓展可以生成HTML 报告 下载地址&#xff1a;GitHub: https://github.com/defnnig/HTMLTestRunner HTMLTestRunner是一个独立的py文件&#xff0c;可以放在Lib 作为第三方模块使用或者作为项目的一部分。 方式1&#xff1a; 验证是否安装成功&…

Flutter 状态管理引子

1、为了更好地了解状态管理&#xff0c;先看看什么是状态。 在类似Flutter这样的响应式编程框架中&#xff0c;我们可以认为U相关的开发就是对数据进行封装&#xff0c;将之转换为具体的U1布局或者组件。借用Flutter官网的一张图&#xff0c;可以把我们在第二部分做的所有开发…

AliOS-Things引入

目录 一、简介 1.1 硬件抽象层 1.2 AliOS-Things内核 rhino ​编辑 1.3 AliOS-Things组件 二、如何进行AliOS-Things开发 三、安装环境 安装python pip git 修改pip镜像源 安装aos-cube 一、简介 AliOS-Things是阿里巴巴公司推出的致力于搭建云端一体化LoT软件。AliOS-…

Linux操作系统--文件与目录结构

我们初步认识了Linux操作系统,下面我们进一步看看linux的文件与目录结构。 1.文件系统和挂载点 (1).当前的操作系统中查看文件系统 位置 > 计算机 这样你就可以看见当前操作系统中的所有目录和文件。如下所示: (2).挂载点 挂载点实际上就是linux中的磁盘文件系统的入口…

快速理解 X server, DISPLAY 与 X11 Forwarding

​ X server X server是X Window System &#xff08;简称X11或者X&#xff09;系统中的显示服务器&#xff08;display server&#xff09;&#xff0c;用于监听X client发送来的图形界面显示请求&#xff0c;并且将图形界面绘制并显示在屏幕&#xff08;screen&#xff09;…

粒子群算法的基本原理和Matlab实现

1.案例背景 1.1 PSO算法介绍 粒子群优化算法(Particle Swarm Optimization,PSO)是计算智能领域,除了蚁群算法,鱼群算法之外的一种群体智能的优化算法,该算法最早是由Kennedy和 Eberhart 在1995年提出的。PSO算法源于对鸟类捕食行为的研究,鸟类捕食时,每只鸟找到食物最简单有效…

webassembly003 ggml GGML Tensor Library part-3

关于pthread_create()和pthread_join() #include <stdio.h> #include <pthread.h>void *thread_func(void *arg) {int *num (int *)arg;printf("Hello from thread! arg%d\n", *num);pthread_exit(NULL); }int main() {pthread_t thread;int arg 10;i…

解锁开发中的创意:用户为中心的设计思维的力量

引言 设计思维&#xff0c;起源于20世纪60年代&#xff0c;是一种解决问题的方法。它不仅仅是设计师的专利&#xff0c;而是一种可以广泛应用于各种行业和领域的方法。设计思维强调了用户至中的重要性&#xff0c;认为任何问题的解决都应该从用户的需求出发。这种方法鼓励我们…