| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 深入理解Java Lambda表达式,匿名函数,闭包 -> 正文阅读 |
|
[Java知识库]深入理解Java Lambda表达式,匿名函数,闭包 |
前言对于Lambda表达式一直是知其然不知其所以然,为了搞清楚什么是Lambda表达式,以及Lambda表达式的用法和作用,本文应运而生当做学习笔记分享出来,欢迎指正交流。 什么是Lambda让我们来较较真,Google翻译输入Lambda进行翻译: 好吧,啥都没有?没办法,百度百科搜一下: 如图所示,对于编程而言,我们应该关注的是Lamdba表达式。 什么是Lambda表达式搜索Lamdba表达式看看: 图文并茂,还有视频,这下舒服了。摘抄Lambda表达式的解释如下: Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。 来源于:Lambda表达式 从以上描述得到除了Lambda 表达式之外的几个关键词:匿名函数,λ演算,闭包。这些都是什么意思?让我们继续探索吧。 注:λ是希腊字母表中排序第十一位的字母,对应大写为Λ,英语名称为Lambda 什么是λ演算看起来λ演算比较关键,因为它是起源,了解这个有助于我们从本质上去了解Lambda表达式。 λ演算(英语:lambda calculus,λ-calculus)是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义、函数如何被应用以及递归的形式系统。它由数学家阿隆佐·邱奇在20世纪30年代首次发表。lambda演算作为一种广泛用途的计算模型,可以清晰地定义什么是一个可计算函数,而任何可计算函数都能以这种形式表达和求值,它能模拟单一磁带图灵机的计算过程;尽管如此,lambda演算强调的是变换规则的运用,而非实现它们的具体机器。 来源于:λ演算_百度百科 上面提到,Lambda演算是一个形式系统。什么是形式系统?在逻辑与数学中,一个形式系统(英语:Formal system)是由两个部分组成的,一个形式语言加上一个推理规则或转换规则的集合。 数学、逻辑和计算机科学中,形式语言(英语:Formal language)是用精确的数学或机器可处理的公式定义的语言。 如语言学中语言一样,形式语言一般有两个方面: 语法和语义。专门研究语言的语法的数学和计算机科学分支叫做形式语言理论,它只研究语言的语法而不致力于它的语义。在形式语言理论中,形式语言是一个字母表上的某些有限长字符串的集合。一个形式语言可以包含无限多个字符串。 而对于一个演算,需要定义两个东西:语法,它描述了如何在演算中写出合法的表达式(对应形式系统中的形式语言);一组规则,让你符号化地操纵表达式(对应形式系统中的一个推理规则或转换规则的集合)。 以上来源于:Good Math/Bad Math的Lambda演算系列的中文翻译 组成Lambda演算由 3 个元素组成:变量(name)、函数(function)和应用(application):
希腊字母λ(发音:Lambda),和点(.)。 λ和点用于描述(定义)匿名函数。函数由λ和变量开头,跟上一个点,然后是函数主体。λ没有任何特别的含义,它只是说函数由此开始。在λ后面,在点之前的字母,我们称作的变量,点之前的部分,被称作头部(head),点后面的表达式,被称作体(body)部。 提问:为什么是λ? 回答:偶然因素。也许一开始邱奇画了一个顶部符号上去,像这样:(? xy) ab。在手稿中,他写成了这样(?y.xy) ab。最后排字工人,把它变成了这样(λy.xy) ab。 最基本的函数是恒等函数:λx.x它等价于 f(x) = x. 第一个“x”是函数的参数,第二个是函数体。 自由与约束变量
因为函数可以是其他函数的一部分,所以一个变量可以同时是约束变量,又是自由变量。 表达式Lambda演算的核心概念是“表达式”(“expression”)。一个表达式可能只是一个变量(name)或一个函数(function)或一个应用(application)。 Lambda演算表达式的定义如下: < expression > := < name >|< function >|< application > < function > := λ < name > . < expression > < application > := < expression >< expression > 译文形式 <表达式> := <标识符>|<函数>|<应用> <函数> := λ<标识符> . <表达式> <应用> := <表达式><表达式> 柯里化在Lambda演算中有一个技巧:如果你看一下上面的定义,你会发现一个函数(Lambda表达式)只接受一个参数。这似乎是一个很大的局限 —— 你怎么能在只有一个参数的情况下实现加法? 这一点问题都没有,因为函数就是值。你可以写只有一个参数的函数,而这个函数返回一个带一个参数的函数,这样就可以实现写两个参数的函数了——本质上两者是一样的。这就是所谓的柯里化(Currying),以伟大的逻辑学家Haskell Curry命名。 例如我们想写一个函数来实现x + y。我们比较习惯写成类似:lambda x y . plus x y之类的东西。而采用单个参数函数的写法是:我们写一个只有一个参数的函数,让它返回另一个只有一个参数的函数。于是x + y就变成一个单参数x的函数,它返回另一个函数,这个函数将x加到它自己的参数上: lambda x. ( lambda y. plus x y ) 现在我们知道,添加多个参数的函数并没有真正添加任何东西,只不过简化了语法,所以下面继续介绍的时候,我会在方便的时候用到多参数函数。 运算法则Lambda演算只有两条真正的法则:称为Alpha和Beta。Alpha也被称为「转换」,Beta也被称为「规约」。 Alpha转换Alpha是一个重命名操作; 基本上就是说,变量的名称是不重要的:给定Lambda演算中的任意表达式,我们可以修改函数参数的名称,只要我们同时修改函数体内所有对它的自由引用。 所以 —— 例如,如果有这样一个表达式: lambda x . if (= x 0) then 1 else x ^ 2 我们可以用Alpha转换,将x变成y(写作alpha[x / y]),于是我们有: lambda y . if (= y 0) then 1 else y ^ 2 这样丝毫不会改变表达式的含义。但是,正如我们将在后面看到的,这一点很重要,因为它使得我们可以实现比如递归之类的事情。 Beta规约Beta规约才是精彩的地方:这条规则使得Lambda演算能够执行任何可以由机器来完成的计算。 Beta基本上是说,如果你有一个函数应用,你可以对这个函数体中和对应函数标识符相关的部分做替换,替换方法是把标识符用参数值替换。这听起来很费解,但是它用起来却很容易。 假设我们有一个函数应用表达式: 一个稍微复杂的例子: 来源于:我的最爱Lambda演算——开篇 · cgnail's weblog 闭包(closure)或者叫完全绑定(complete binding)A term is closed if it has no free variables; otherwise it is open. Lambda演算表达式如果不包含自由变量,那它就是封闭的,否则就是开放的。 来源于:http://www.cs.yale.edu/homes/hudak/CS201S08/lambda.pdf 如果一个标识符是一个闭合Lambda表达式的参数,我们则称这个标识符是绑定(约束)的;如果一个标识符在任何封闭上下文中都没有绑定,那么它是自由的。 λx. x y:在这个表达式中,y是自由的,因为它不是任何闭合的Lambda表达式的参数;而x是绑定(约束的,因为它是函数定义的闭合表达式x y的参数。 λxy.x y :在这个表达式中x和y都是被绑定(约束)的,因为它们都是函数定义中的参数。 λy . (λx. x y z):在内层演算λx. x y z中,y和z是自由的,x是绑定(约束)的。在完整表达中,x和y是绑定(约束)的:x受内层演算绑定(约束),而y由剩下的演算绑定(约束)。但z仍然是自由的。 在对一个Lambda演算表达式进行求值的时候,不能引用任何未绑定(约束)的标识符(变量),因为无法知道自由变量的值,除非有其它方式能提供这些自由变量的值。但是,当我们抛开上下文,关注于一个复杂表达式的子表达式时,自由变量是允许存在的——这时候搞清楚子表达式中的哪些变量是自由的就显得非常重要了。 求值求值是通过由一个单一转换规则(变量替换,通常被叫做β-Reduction,β变换)完成的,它本质上是词法范围的替换。 在计算表达式(λx.x)a时,我们将函数体中所有出现的“x”替换为“a”。
您甚至可以创建高阶函数:
虽然 lambda 演算传统上只支持单参数函数,但我们可以使用一种称为 currying的技术创建多参数函数。
有时λxy.<body>可与以下内容互换使用:λx.λy.<body> 来源于:Learn X in Y Minutes: Scenic Programming Language Tours 什么是匿名函数什么是匿名函数?这个很确定,匿名函数就是没有名称的函数。至于什么是函数,笔者就不啰嗦了。注意,在这里笔者讨论的是Java编程语言中的匿名函数。 匿名函数让你想起了什么?匿名类,对吧?匿名类是没有名称的类。匿名函数是没有名称且不属于任何类的函数。 Java一直都是面向对象的编程语言。 这意味着Java编程中的所有内容都围绕对象(为了简单起见,某些基本类型除外)。在Java 8之前,函数是Class的一部分,我们需要使用class / object来调用函数。Java 8及之后的版本,出现了一种不属于任何类的函数,你会肯定会好奇,这不是违背了OOP的思想吗?如果我们研究其他编程语言,例如C ++,C#,JavaScript; 会发现它们被称为函数式编程语言,因为我们可以编写函数并在需要时使用它们。 这些语言中的某些语言支持面向对象的编程以及函数式编程。什么是函数式编程?建议阅读阮一峰的《函数式编程初探》一文了解学习。 要深入了解匿名函数,建议是先了解匿名类。匿名类本质上也是一个表达式。匿名类表达式的语法类似于构造函数的调用,不同之处在于代码块中包含类定义。 匿名类表达式包括以下内容:
因为匿名类定义是一个表达式,所以它必须是语句的一部分。这匿名类的解释了为什么右大括号后面有一个分号。 匿名类和本地类一样,匿名类可以捕获变量;它们对封闭范围的局部变量具有相同的访问权限:
Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。 匿名类对其成员也有与本地类相同的限制:
匿名类中可以声明以下内容:
但是,不能在匿名类中声明构造函数。 为什么花这么大篇幅了介绍了匿名类,举个例子: public class HelloTest { private String text = " world"; public interface IHello { void sayHello(); } private void hello() { IHello hello = new IHello() { @Override public void sayHello() { System.out.print("Hello " + text); } }; } public static void main(String[] args) { } } 以上代码定义了一个IHello接口,只包含一个抽象方法 替换后就变成了: public class HelloTest { private String text = " world"; public interface IHello { void sayHello(); } private void hello() { IHello hello = () -> System.out.print("Hello " + text); } public static void main(String[] args) { } } 但如果IHello接口包含了两个或两个以上抽象方法,则编译器不会提示你替换为Lambda表达式,为什么? public class HelloTest { private String text = " world"; public interface IHello { void sayHello(); void printTime(); } private void hello() { IHello hello = new IHello() { @Override public void sayHello() { System.out.print("Hello " + text); } @Override public void printTime() { } }; } public static void main(String[] args) { } } 因为,Java中规定如果一个接口有且仅有一个抽象方法,那么接口就被称为函数接口/功能接口( Functional interface),例如Comparable , Runnable , EventListener , Comparator等。在Java8及之后的版本中,函数接口可以使用Lambda表达式代替匿名类表达式,也就是说函数接口可以使用匿名函数代替匿名类实现,这就是Lambda表达式的特性之一。所以,了解匿名类有助于我们从本质上去了解匿名函数。 但是,为什么我们将这种接口称为函数接口呢?为啥不叫单方法接口(Single Method Interface)? 这是一个很好的问题,如果大家对函数式编程有所了解,就知道它可以传递代码,即函数,就像将数据或对象传递给方法一样。这些接口只有一种抽象方法被用于传递代码,就像函数式编程语言传递函数一样, 这就是为什么它们被称为函数接口 。 一个简单的例子: Runnable runnable = new Runnable(){ @Override public void run(){ System.out.println("Running without Lambda"); } }; new Thread(runnable).start(); // Running with Lambda new Thread(() -> System.out.println("Running without Lambda")).start(); 仔细观察,我们正在使用这些接口将代码传递给Thread的构造函数 ,对于Thread类来说,重要的是run方法里的代码。而且,我们很容易替换run方法的实现,这些接口实际上是策略接口,因为这是策略模式的实现,其中,构成策略的代码被注入到在运行时运行该策略的代码中。 有且仅有一个抽象方法的接口当做函数一样使用,所以叫做函数接口,没毛病吧。 那如果接口中声明多个抽象方法呢?还能这么用吗?答案是不能。因为你要实现所有的抽象方法,这时候Lambda表达式就会报错了。为了防止在函数接口中声明多个抽象方法,Java 8 提供了一个声明函数接口注解 @FunctionalInterface,使用它不是强制性的,但是最好的做法是将其与功能接口一起使用,以避免意外添加其他方法。示例代码如下。 /** * 一个接口如果有且仅有一个抽象方法,这个接口就被称为函数接口/功能接口( Functional interface) * 在Java8及之后的版本中,函数接口可以使用Lambda表达式代替匿名类表达式,也就是说函数接口可以使用匿名函数代替匿名内部类实现。 */ @FunctionalInterface public interface IHelloInner { void sayHello(); } 在接口之前使用 @FunctionalInterface 注解修饰,当试图增加一个抽象方法时编译就会发生编译错误: The target type of this expression must be a functional interface 但是,函数接口可以添加默认方法和静态方法。 /** * 一个接口如果有且仅有一个抽象方法,这个接口就被称为函数接口/功能接口( Functional interface) * 在Java8及之后的版本中,函数接口可以使用Lambda表达式代替匿名类表达式,也就是说函数接口可以使用匿名函数代替匿名内部类实现。 * 可以添加任意数量的默认方法和静态方法 */ @FunctionalInterface public interface IHelloInner { /** * 函数接口/功能接口可以添加静态方法 */ static void staticFunction() { } static void staticFunction2() { } /** * 函数接口/功能接口可以默认方法 */ default void defaultFunction() { } default void defaultFunction2() { } /** * 抽象方法 */ void sayHello(); } 思考一下,为什么非函数接口接口不能支持Lambda表达式? 注意:在Java中,有两种类型的内部类,本地类(局部类)和匿名类。因此,匿名类一定是内部类,所以匿名内部类的说法是指一个类是匿名的内部类。所以,匿名类和匿名内部类是一个意思,不用纠结。而本地类是定义在方法体中的内部类,如果本地类没有名称,那也是一个匿名类。但反过来,匿名类不一定是本地类,因为不一定是在方法体中。 There are two special kinds of inner classes: local classes and anonymous classes. 来源于:https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html There are two additional types of inner classes. You can declare an inner class within the body of a method. These classes are known as local classes. You can also declare an inner class within the body of a method without naming the class. These classes are known as anonymous classes. 来源于:https://docs.oracle.com/javase/tutorial/java/javaOO/innerclasses.html 函数接口是Java 8最重要的概念之一,为Lambda表达式提供了动力,但是许多开发人员没有首先了解函数接口在Java 8中的作用就花了很多精力来理解它,并花时间学习Lambda表达式和Stream API。除非您知道什么是功能接口以及Lambda与它之间的关系,否则您将无法使用Java 8的强大功能,例如Lambda表达式和流API 。 没有函数接口的知识,可能就无法理解在代码中可以使用Lambda的位置,并且很难编写所期望的Lambda表达式,因此,在Java 8中对函数接口有一个很好的了解是非常重要的。可以看到Java 8函数接口和Lambda表达式通过删除许多样板代码来帮助我们编写更小巧,更简洁的代码。 private void hello() { //匿名内部类 IHello hello = new IHello() { @Override public void sayHello() { System.out.print("Hello " + text); } }; //匿名函数(Lambda表达式) IHello hello = () -> System.out.print("Hello " + text); } 注意,对于外部类成员变量的访问,匿名函数( Lambda 表达式)与普通函数没有区别,但是访问函数内的局部变量时,局部变量必须是 final 类型的(不可改变)。 匿名类和匿名函数的区别
让我们看看匿名类和匿名函数编译后的字节码: public class HelloTest { private String text = " world"; public interface IHello { void sayHello(); } private void hello() { IHello helloAnonymousClass = new IHello() { @Override public void sayHello() { System.out.print("Hello helloAnonymousClass " + text); } }; IHello helloAnonymousFunction = () -> System.out.print("Hello helloAnonymousFunction " + text); } public static void main(String[] args) { } } 编译后的字节码: // class version 52.0 (52) // access flags 0x21 public class com/nxg/app/HelloTest { // access flags 0x609 public static abstract INNERCLASS com/nxg/app/HelloTest$IHello com/nxg/app/HelloTest IHello // access flags 0x0 INNERCLASS com/nxg/app/HelloTest$1 null null // access flags 0x19 public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup // access flags 0x2 private Ljava/lang/String; text // access flags 0x1 public <init>()V ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V ALOAD 0 LDC " world" PUTFIELD com/nxg/app/HelloTest.text : Ljava/lang/String; RETURN MAXSTACK = 2 MAXLOCALS = 1 // access flags 0x2 private hello()V NEW com/nxg/app/HelloTest$1 DUP ALOAD 0 INVOKESPECIAL com/nxg/app/HelloTest$1.<init> (Lcom/nxg/app/HelloTest;)V ASTORE 1 ALOAD 0 INVOKEDYNAMIC sayHello(Lcom/nxg/app/HelloTest;)Lcom/nxg/app/HelloTest$IHello; [ // handle kind 0x6 : INVOKESTATIC java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; // arguments: ()V, // handle kind 0x7 : INVOKESPECIAL com/nxg/app/HelloTest.lambda$hello$0()V, ()V ] ASTORE 2 RETURN MAXSTACK = 3 MAXLOCALS = 3 // access flags 0x9 public static main([Ljava/lang/String;)V RETURN MAXSTACK = 0 MAXLOCALS = 1 // access flags 0x1002 private synthetic lambda$hello$0()V GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "Hello helloAnonymousFunction " INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/nxg/app/HelloTest.text : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/String;)V RETURN MAXSTACK = 3 MAXLOCALS = 1 // access flags 0x1008 static synthetic access$000(Lcom/nxg/app/HelloTest;)Ljava/lang/String; ALOAD 0 GETFIELD com/nxg/app/HelloTest.text : Ljava/lang/String; ARETURN MAXSTACK = 1 MAXLOCALS = 1 } 在hello函数中,使用匿名类,最终是创建一个匿名类的对象。 NEW com/nxg/app/HelloTest$1 DUP ALOAD 0 INVOKESPECIAL com/nxg/app/HelloTest$1.<init> (Lcom/nxg/app/HelloTest;)V ASTORE 1 而使用Lambda表达式,会生成一个名为 lambda$hello$0()的函数,匿名函数本质上确实是一个函数。 INVOKEDYNAMIC sayHello(Lcom/nxg/app/HelloTest;)Lcom/nxg/app/HelloTest$IHello; [ // handle kind 0x6 : INVOKESTATIC java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; // arguments: ()V, // handle kind 0x7 : INVOKESPECIAL com/nxg/app/HelloTest.lambda$hello$0()V, ()V ] ASTORE 2 匿名类对应类和对象,匿名函数对应函数,那这两者本质上的区别至少是:类、对象和函数的区别。 什么是闭包这真的难以有一个准确权威且可靠的回答。 闭包翻译自英文单词 closure,这是个不太好翻译的词,在计算机领域,它就有三个完全不相同的意义:编译原理中,它是处理语法产生式的一个步骤;计算几何中,它表示包裹平面点集的凸多边形(翻译作凸包);而在编程语言领域,它表示一种函数。 闭包这个概念第一次出现在 1964 年的《The Computer Journal》上,由 P. J. Landin 在《The mechanical evaluation of expressions》一文中提出了 applicative expression 和 closure 的概念。 在上世纪 60 年代,主流的编程语言是基于 lambda 演算的函数式编程语言,所以这个最初的闭包定义,使用了大量的函数式术语。一个不太精确的描述是“带有一系列信息的λ表达式”。对函数式语言而言,λ表达式其实就是函数。我们可以这样简单理解一下,闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。这个古典的闭包定义中,闭包包含两个部分。环境部分和表达式部分。 以上内容来源于:程劭非(winter) 的重学前端专栏 注意:根据程劭非(winter)大佬的结合,结合笔者搜集到的其它资料。可以确定编程语言中的闭包概念和数学领域上的是不同的。详情见这篇文章《【闭包】你真的理解闭包和lambda表达式吗》以及stackoverflow上的问答What is the difference between a 'closure' and a 'lambda'? 有很多不同的人都对闭包过进行了定义,这里列举给大家看看:
以上内容摘抄自: 这么多定义,真的是公说公有理,婆说婆有理,而且太抽象了,不好理解!不过,在这些定义中都有一些关键字:变量、函数、上下文等。闭包在回调函数、函数式编程、Lambda表达式中有重要的应用,并且闭包在现在的很多流行的语言中都存在,例如 JavaScript,C++、C# 等。为了避免人云亦云,秉承着官方即正义的理念,我们去看看JDK8的里程碑文档(Oracle 2014/03/18发布的JDK8新增了对Lambda表达式的支持): 我们关注的是第126条新增的特性:126? Lambda表达式和虚拟扩展方法,点击链接进去看看: 概括 向 Java 编程语言和平台添加 lambda 表达式(闭包)和支持功能,包括方法引用、增强的类型推断和虚拟扩展方法。 没什么特别的说明,按照JDK官方文档来说,似乎Lambda 表达式等同于闭包。 查找文档里的其它内容,发现了:Closures for the Java Programming Language (BGGA),那也点进去看看。 看到了吗?在Java 编程语言中,下面这玩意就是闭包,这下不抽象了吧(邪魅一笑狗头.jpg)。 开个玩笑,别当真!注意到有一个链接:BGGA closures specification: Closures for the Java Programming Language (v0.5),摘抄部分内容如下: Closures for the Java Programming Language (v0.5)Gilad Bracha, Neal Gafter, James Gosling, Peter von der Ahé Modern programming languages provide a mixture of primitives for composing programs. Most notably Scheme, Smaltalk, Ruby, and Scala have direct language support for parameterized delayed-execution blocks of code, variously called lambda, anonymous functions, or closures. These provide a natural way to express some kinds of abstractions that are currently quite awkward to express in Java. For programming in the small, anonymous functions allow one to abstract an algorithm over a piece of code; that is, they allow one to more easily extract the common parts of two almost-identical pieces of code. For programming in the large, anonymous functions support APIs that express an algorithm abstracted over some computational aspect of the algorithm. For example, they enable writing methods that act as library-defined control constructs. Java 编程语言的闭包 (v0.5)吉拉德·布拉查、尼尔·加夫特、詹姆斯·高斯林、彼得·冯·德·阿赫 现代编程语言为编写程序提供了混合原语。最值得注意的是,Scheme、Smaltalk、Ruby 和 Scala 对参数化延迟执行代码块有直接的语言支持,这些代码块被称为lambda、匿名函数或 闭包. 这些提供了一种自然的方式来表达目前在 Java 中难以表达的某些抽象。对于小型匿名函数的编程,允许人们在一段代码上抽象出一个算法;也就是说,它们允许人们更容易地提取两段几乎相同的代码的公共部分。对于大型的编程,匿名函数支持表达了在算法的某些计算方面抽象的算法的API 。例如,它们允许编写充当 库定义的控制结构的方法。 好吧,也许你需要一个结论(定心丸),在Java编程语言中: Lambda表达式(Lambda Expressions)是 匿名函数(Anonymous Functions) 也是 闭包 (Closures) 它长这样,是一个代码块(blocks of code ): 注意,这个结论可不是笔者瞎掰的,是Java官方文档上整理下来的。如果你觉得这个结论跟你心中的所想不一致,那很正常。因为当闭包这个概念从数据领域应用到编程领域时,闭包的概念已经因为人传人而发生了变化。作为Android程序员,跟着Java官方文档走准备没错。当然,如果你有更好的资料,那真是求之不得,欢迎交流。 是不是还不理解?确实比较难理解,因为就算是Java官方都没有统一给出一个准确的定义(如果有,欢迎指正),毕竟Java一开始也不支持Lambda表达式,也是后面借鉴其它语言的函数式编程思想才加上的。 注意到了吗?笔者并没有用等号(=)来表示Lambda表达式, 匿名函数(Anonymous Functions), 闭包 (Closures)之间的关系,而是用了一个“是”,为什么?因为它们功能(思考的角度)不同。 举个可能不恰当的例子来解释: 你是个程序员,是你老婆的爱人,是你孩子的父亲。 在这里,用程序员 = 爱人 = 父亲这样的式子来表达是不正确的,因为程序员,爱人,父亲并不等同,它们虽然是是你的一个身份,但是从不同的角度去表达的(具有不同的功能)。 就好比有个很有意思的解释:抽象类和接口的区别,抽象类代表是什么,而接口代表能做什么。 同样的,Lambda表达式, 匿名函数(Anonymous Functions), 闭包 (Closures)也是从不同的角度思考,针对不同功能沉淀下来的产物(名称)。 闭包这个概念一直没有被明确的定义, 每个人心中都可以有自己的理解。如果只看闭包本身的语义,看起来有关闭、包围的意思,对应的英文单词closures也是关闭的意思。那我们就得思考,关闭(包围)的到底是什么?为什么要关闭(包围)? 首先,闭包是一个匿名函数,而函数是定义在类中的一段独立的代码块,用来实现某个功能。但是我们却不会把普通函当做闭包,这说明还有其它限制条件。我们尝试列举这些条件:
在Java中,这些条件都是缺一不可的,综上,笔者理解是的:在Java中,Lambda表达式用来表示闭包,以匿名函数的形式存在于代码中。 关闭(包围)的到底是什么? 闭包关闭(包围)的是Lambda表达式中引用的外部变量(即非Lambda表达式本身声明的变量,或者叫自由变量)。 为什么要关闭(包围)? 因为只有关闭(包围)这些外部变量,Lambda表达式才能避免因为外部环境作用域的失效导致外部变量无法正确读取。如果我们不知道外部变量从哪来是什么,Lambda表达式将无法正确执行。关闭(包围)的目的就让Lambda表达式和它所引用的外部变量(或者叫自由变量,来自于上下文,即Lambda所处的执行环境),组成一个完整的封闭的整体。 所以闭包是什么?闭包是使用Lambda表达式语法的绑定上下文(执行环境)的匿名函数?笔者也不确定,看看就行,最重要的是要有自己的思考。 当然,为了避免人云亦云,笔者决定通过对Lambda表达式的使用来加深对Lambda表达式, 匿名函数,闭包的理解,一起探索吧! Lambda表达式因为笔者研究的是Java 8中的Lambda表达式,所以先看看官方文档。 Java8 新增了非常多的特性,本文可能涉及到的特性主要以下几个:
更多的新特性可以参阅官网:What's New in JDK 8 Lambda表达式。让你想起了什么?正则表达式,对吧。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。那Lambda表达式的作用是什么?前面已经有答案了,用来表示闭包。你看,Lambda表达式本质上是一个表达式。 在 Java 中,Lambda表达式是表示函数接口( Functional interface)实例的表达式。 与 Java 中的其他类型类似,Lambda表达式也是有类型的,它们的类型是函数接口类型。 为了推断类型,编译器在Lambda表达式中查看赋值的左侧。 需要注意的是,Lambda表达式本身不包含有关它正在实现的功能接口的信息。 该信息是从使用表达式的上下文中推导出来的。 表达式,是由数字、算符、数字分组符号(括号)、自由变量和约束变量等以能求得数值的有意义排列方法所得的组合。约束变量在表达式中已被指定数值,而自由变量则可以在表达式之外另行指定数值。 Lambda表达式初见俗话说:没吃过猪肉还没见过猪跑吗?是骡子是马拉出来遛遛不就知道了,是吧。看看下面的代码: 没有使用Lambda表达式: button.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent actionEvent){ System.out.println("Action detected"); } }); 使用Lambda表达式: button.addActionListener( actionEvent -> { System.out.println("Action detected"); }); 一个更明显的例子。 没有使用Lambda表达式: Runnable runnable = new Runnable(){ @Override public void run(){ System.out.println("Running without Lambda"); } }; 使用Lambda表达式: Runnable runnable = ()->System.out.println("Running from Lambda"); 可以看到,匿名内部类被Lambda表达式所替代,对比发现,使用Lambda表达式后,不仅让代码变的简单、而且代码量也随之减少很多,但是说提高了可读性这一点,笔者持保留态度(不过,至少代码的层级是比较好看的)。 第一个例子,Lamba表达式作为一个参数传入了addActionListener方法中;第二个例子,Lamba表达式就当做一个函数(表达式)使用。那么,大家觉得使用Lambda表达式后代码的可读性提高了吗? 事实上,如果你按上面使用匿名类的写法,先进的IDE肯定会给你提示为替换Lambda表达式,想一想,为什么呢?让我们看看IDE是如何提示的: Anonymous new IHello() can be replaced with lambda Inspection info: Reports all anonymous classes which can be replaced with lambda expressions. Note that if an anonymous class is converted into a stateless lambda, the same lambda object can be reused by Java runtime during subsequent invocations. On the other hand, when an anonymous class is used, separate objects are created every time. Thus, applying the quick-fix can cause the semantics change in rare cases, e.g. when anonymous class instances are used as HashMap keys. Lambda syntax is not supported under Java 1.7 or earlier JVMs. 匿名 new IHello() 可以替换为 lambda 检查信息:报告所有可以用 lambda 表达式替换的匿名类。 请注意,如果将匿名类转换为无状态 lambda,则 Java 运行时可以在后续调用期间重用相同的 lambda 对象。 另一方面,当使用匿名类时,每次都会创建单独的对象。 因此,在极少数情况下,应用快速修复可能会导致语义发生变化,例如 当匿名类实例用作 HashMap 键时。 Java 1.7 或更早的 JVM 不支持 Lambda 语法。 改造下之前HelloTest的代码验证下: private void hello() { IHello helloAnonymousClass = new IHello() { @Override public void sayHello() { System.out.print(this + " Hello helloAnonymousClass " + text + "\n"); } }; IHello helloAnonymousFunction = () -> { System.out.print(this + " Hello helloAnonymousFunction " + text + "\n"); }; helloAnonymousClass.sayHello(); helloAnonymousFunction.sayHello(); } 运行结果如下: com.nxg.app.HelloTest$1@34ce8af7 Hello helloAnonymousClass world com.nxg.app.HelloTest@b684286 Hello helloAnonymousFunction world com.nxg.app.HelloTest$1@880ec60 Hello helloAnonymousClass world com.nxg.app.HelloTest@b684286 Hello helloAnonymousFunction world 可以发现,每次调用sayHello函数,helloAnonymousClass都是由不同的匿名类HelloTest$1对象实例调用的。而helloAnonymousFunction则是由外部类(封闭类)调用的。根本原因就如前面分析HelloTest编译后字节码提到的那样,这里不再赘述。 Lambda表达式语法Lambda表达式看起来很像函数声明,实际上,Lambda 表达式就是没有名称的匿名函数。 典型的Lambda表达式语法如下所示: (x, y) -> x + y //This function takes two parameters and return their sum. 可以看到,并没有声明x和y的参数类型,因此这个Lambda表达式可以在多个地方使用,参数可以匹配 int、Integer 或简单的 String。 根据上下文,它将添加两个整数或连接两个字符串。 举个例子: @FunctionalInterface interface Operator<T> { T process(T a, T b); } Operator<Integer> addOperation = (a, b) -> a + b; System.out.println(addOperation.process(3, 3)); //Prints 6 Operator<String> appendOperation = (a, b) -> a + b; System.out.println(appendOperation.process("3", "3")); //Prints 33 Operator<Integer> multiplyOperation = (a, b) -> a * b; System.out.println(multiplyOperation.process(3, 3)); //Prints 9 Lambda表达式其他语法是: either (parameters) -> expression //1 or (parameters) -> { statements; } //2 or () -> expression //3 以下是lambda表达式的重要特征:
Lambda表达式实例 Lambda 表达式的简单例子: // 1. 不需要参数,返回值为 5 () -> 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s) 我们看看实际是怎么运用的: public class Java8Tester { interface MathOperation { int operation(int a, int b); } interface GreetingService { void sayMessage(String message); } private int operate(int a, int b, MathOperation mathOperation){ return mathOperation.operation(a, b); } public static void main(String args[]){ Java8Tester tester = new Java8Tester(); // 类型声明 MathOperation addition = (int a, int b) -> a + b; // 不用类型声明 MathOperation subtraction = (a, b) -> a - b; // 大括号中的返回语句 MathOperation multiplication = (int a, int b) -> { return a * b; }; // 没有大括号及返回语句 MathOperation division = (int a, int b) -> a / b; System.out.println("10 + 5 = " + tester.operate(10, 5, addition)); System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction)); System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication)); System.out.println("10 / 5 = " + tester.operate(10, 5, division)); // 不用括号 GreetingService greetService1 = message -> System.out.println("Hello " + message); // 用括号 GreetingService greetService2 = (message) -> System.out.println("Hello " + message); greetService1.sayMessage("Runoob"); greetService2.sayMessage("Google"); } } Lambda表达式的特征Lambda表达式参数可以有零个、一个或多个。 (x, y) -> x + y (x, y, z) -> x + y + z Lambda表达式的主体可以包含零个、一个或多个语句。 如果Lambda表达式的主体只有一条语句,则大括号不是强制性的,并且Lambda表达式的返回类型与主体表达式的返回类型相同。 当主体中有多个语句时,这些语句必须用大括号括起来。 (parameters) -> { statements; } 参数的类型可以显式声明,也可以从上下文中推断出来。多个参数需要用括号括起来并用逗号分隔。空括号用于表示空参数。 () -> expression 当有单个参数时,如果能推断其类型,则不强制使用括号。 a -> return a * a; Lambda表达式不能有 throws clause,参数类型是从参数使用的上下文中和参数所在的主体中推断出来的。 Lambda表达式不能是泛型的,即它们不能声明泛型参数。 Lambda表达式的变量作用域Java 中本地类(局部类)和匿名类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。由于Lambda表达式是匿名函数,因此它变量作用域同匿名类是一致的。 扩展阅读:Java Final与Effectively Final 你可能还听说过自由变量的概念,但是在Java中,笔者并没有找到相关的介绍,很可能是其它编程语言或领域独有的概念。但是者不妨碍我们按照同样的思维去理解自由变量。 在Java中,成员变量:
可以认为,只要不是在Lambda表达式中声明的变量,对于Lambda表达式来说都是自由变量。 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量 变量的作用域成员变量和局部变量的区别: 成员变量: 1、成员变量定义在类中,在整个类中都可以被访问。 2、成员变量随着对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中。 3、成员变量有默认初始化值。 局部变量: 1、局部变量只定义在局部范围内,如:函数内,语句内等,只在所属的区域有效。 2、局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放。 3、局部变量没有默认初始化值 在使用变量时需要遵循的原则为:就近原则,首先在局部范围找,有就使用;接着在成员位置找。
本地类和匿名类为什么能引用外部类的成员变量?因为内部类持有外部类的引用,而且就算是变量声明为private也一样可以引用。举个例子: /** * 外部类(封闭类) */ public class OuterClass { //私有的成员变量 private String privateText = "privateText"; //私有的静态变量 private static String privateStaticText = "privateStaticText"; //公开的静态变量 public static String publicStaticText = "publicStaticText"; /** * 一个接口如果有且仅有一个抽象方法,这个接口就被称为函数接口/功能接口( Functional interface) * 在Java8及之后的版本中,函数接口可以使用Lambda表达式代替匿名类表达式,也就是说函数接口可以使用匿名函数代替匿名内部类实现。 * 可以添加任意数量的默认方法和静态方法 */ @FunctionalInterface public interface IHelloInner { /** * 抽象方法 */ void sayHello(); } /** * 成员函数 * * @param iHelloOuter 局部变量 */ private void hello(IHelloOuter iHelloOuter) { String localText = "localText "; /** * 本地类(局部类) */ class LocalClass implements IHelloInner { @Override public void sayHello() { //可以使用外部类的属性 System.out.print("Hello LocalClass " + privateText); System.out.print("Hello LocalClass " + privateStaticText); System.out.print("Hello LocalClass " + publicStaticText); System.out.print("Hello LocalClass " + localText); } } } } //编译后的LocalClass持有OuterClass的引用 class OuterClass$1LocalClass implements IHelloInner { OuterClass$1LocalClass(OuterClass var1, String var2) { this.this$0 = var1; this.val$localText = var2; } public void sayHello() { System.out.print("Hello LocalClass " + OuterClass.access$000(this.this$0)); System.out.print("Hello LocalClass " + OuterClass.access$100()); System.out.print("Hello LocalClass " + OuterClass.publicStaticText); System.out.print("Hello LocalClass " + this.val$localText); } } Lambda表达式就更不用说了,就是外部类的成员函数,前面讲解闭包的时候有介绍过,不赘述了。 那如果想修改局部变量可以吗?答案是不可以。如果有需要,Effectively final 变量官方建议改成Atomic类来实现。 变量的生命周期变量的作用域指的是变量的存在范围,只有在这个范围内,程序代码才能访问它。当一个变量被定义时,它的作用域就确定了。变量的作用域决定了变量的生命周期,说明作用域不同,生命周期就不一样。 变量的生命周期指的是一个变量被创建并分配内存空间开始,到该变量被销毁并清除其所占内存空间的过程。 上图来源于:java方法 成员变量 局部变量概述_小菜鸟的博客-CSDN博客_java局部变量 可以看出,局部变量在函数返回结果后会被释放。但如果局部变量被Lambda表达式使用,这无疑会延长局部变量的生存时间。这其实也不是什么问题,就像递归一样,函数调用函数罢了。 Lambda表达式的方法引用Method References You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it’s often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name. 方法引用 您使用 lambda 表达式来创建匿名方法。 然而,有时 lambda 表达式除了调用现有方法之外什么都不做。 在这些情况下,通过名称引用现有方法通常更清楚。 方法引用使您能够做到这一点; 它们是紧凑、易于阅读的 lambda 表达式,用于已经有名称的方法。 关于方法引用的描述来源于: https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html 以下是Java 8中方法引用的一些语法,有四种方法引用:
具体就不多说了,官方文档写的比较详细,建议阅读学习。个人感觉,方法引用虽然是引用一个方法而不是调用一个方法,但是方法引用看起来更像是一种语法糖,简化了Lambda表达式的使用,更方便的使用已经存在的函数。 Lambda表达式的优点和作用Lambda 表达式为 Java 带来了函数式编程的许多好处。像大多数 OOP 语言一样,Java 是围绕类和对象构建的,并且只将类视为它们的一等公民。其他重要的编程实体(例如函数)则处于次要地位。 但是在函数式编程中,我们可以定义函数,给它们引用变量,并将它们作为方法参数传递等等。 JavaScript 是函数式编程的一个很好的例子,我们可以将回调方法传递给 Ajax 调用等等。 请注意,在 Java 8 之前,我们可以使用可以使用Lambda表达式执行的匿名类来完成所有操作,但它们使用非常简洁的语法来实现相同的结果。让我们看看使用这两种技术的相同方法实现的比较。 //使用lambda表达式 Operator<Integer> addOperation = (a, b) -> a + b; //使用匿名类 Operator<Integer> addOperation = new Operator<Integer>() { @Override public Integer process(Integer a, Integer b) { return a + b; } }; 面向对象还不错,但是它给程序带来了很多冗长的细节。正如上面的例子所示,实际使用的部分是process()方法中的代码, 剩下的所有代码都是Java语言的结构。 Java 8函数接口和Lambda表达式通过删除许多样板代码来帮助我们编写更小巧,更简洁的代码。就这样?与其去讨论Lambda表达式的优点,倒不如去讨论它的作用。优点吧,肯定是要有比较的才好下结论,但毕竟Lambda表达式是属于函数式编程思想的,如果要比较的话,感觉扯大了,是吧。 那Lambda表达式的作用什么?或者说它的意义(价值)在哪里,用和不用Lambda表达式对于Java编程来说有什么差别吗?难道只是为了减少代码量吗?要了解Lambda表达式价值,得先了解什么是函数式编程,以上问题答案在阮一峰的《函数式编程初探》文章中,全靠你自己领悟。 这里还有相关的文档聊到这个问题:Why do we need Lambda Expression,笔者整理里关键内容如下:
虽然匿名类到处都在使用,但是它们还是有很多问题。第一个主要问题是复杂。这些类让代码的层级看起来很乱很复杂,也称作 Vertical Problem 。Lambda表达式和匿名类一对比,代码量确实是减少了很多,喜欢使用语法糖的同学一定会爱上它。
这个需要Lambda表达式结合FunctionalInterface Lib, forEach, stream(),method reference才能体现出来,具体看Mingqi大佬在某乎上的回答:Lambda 表达式有何用处?如何使用?图文并茂,例子生动。
简单理解就是,Lambda表达式使我们能够将功能视为方法参数,或将代码视为数据。 Lambda 表达式可以更紧凑地表达单方法接口(称为函数式接口)的实例。
这个怎么说呢?反正就是,Lambda表示式通过方法引用的方式引用已经存在的函数,这样写代码更加高效。 来源在这:Java 8 Functional Interfaces - JournalDev Lambda表达式的实现原理在 Java 8 中,Lambda 表达式是借助 invokedynamic 来实现的。具体来说,Java 编译器利用 invokedynamic 指令来生成实现了函数接口的适配器。 就不多做介绍了,感兴趣可以阅读 郑雨迪(Oracle 高级研究员,计算机博士)《深入拆解 Java 虚拟机》,可以免费试读章节: 08 | JVM是怎么实现invokedynamic的?(上) 09 | JVM是怎么实现invokedynamic的?(下) 或者可以阅读这篇: 写在最后关于闭包的概念,在阅读了众多关于闭包的介绍文章后,笔者深感压力。因为对于闭包的理解,真的是一千个读者就有一千个哈姆雷特。本文纯粹是学习Java Lambda表达式,匿名函数,闭包的一个笔记。文章的表述可能会存在错误和遗漏之处,欢迎指正。摘抄的内容也都标记了来源。同时,非常感谢您耐心阅读完整篇文章,坚持写原创且基于实战的文章不是件容易的事,如果本文刚好对您有点帮助,欢迎您给文章点赞评论,您的鼓励是笔者坚持不懈的动力,再次感谢。 参考资料Closures (Lambda Expressions) for the Java Programming Language JEP 126: Lambda Expressions & Virtual Extension Methods Java 8 Functional Interface and Lambda Expression What is a Functional interface in Java 8? @Functional Annotation and Examples |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 12:44:51- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |