关于Lambda表达式的整理

by admin on 2020年1月14日

当今世界主流编程语言无不吸纳强大的闭包概念,但有个例外,它就是Java。数年来,Java语言中增加闭包特征的工作看起来毫无进展。

关于“Java
8为Java带来了函数式编程”已经有了很多讨论,但这句话的真正意义是什么?

前段时间的面试基本结束了,最后也有了不错的结果,之后一段时间到入职打算好好整理一些东西。想到马上Java9也要出来了,Android也马上支持Java8,自己都没有好好整理过java8比较重要的知识点,可以说8很多改动都是为了Lambda服务的,所以整理一下Lambda的内容。

早在15年之前,Scala语言和TypeSafe框架的作者Martin Odersky和Phillip
Wadler发布了实验性的“Pizza”项目,由此,人们开始试图将闭包纳入编程语言的基本特征之一。尽管这看起来有点过于复杂,Java社区大概在2008年就有了接纳闭包概念的想法。但由于Oracle对Sun微系统公司的匆忙收购,Java被冷落,Java语言新版本的发布不断的被推迟。

本文将讨论函数式,它对一种语言或编程方式意味着什么。在回答“Java
8的函数式编程怎么样”之前,我们先看看Java的演变,特别是它的类型系统,我们将看到Java
8的新特性,特别是Lambda表达式如何改变Java的风景,并提供函数式编程风格的主要优势。

首先闭包是指,将当前作用域中的变量通过值或者引用的方式封装到lambda表达式中,成为表达式的一部分,它使你的lambda表达式从一个普通的函数变成带隐藏参数的函数,当然闭包也可以不通过lambda实现

但在Java8中,事情有了很大的变化,Java语言终于为Java编程部队配备了闭包的武器。“也许这是Java编程语言有史以来最重要的一次升
级,”Oracle的Java语言架构师Brian
Goetz说。他指出,在Java中引入闭包概念对Java程序开发方法的影响甚至会大于Java5中引入的泛型特征对编程方式带来的影响。“就像泛型能使开发人员对数据类型进行抽象,Lambda的目的是让程序员能够对程序行为进行抽象。”

函数式编程语言是什么?

函数式编程语言的核心是它以处理数据的方式处理代码。这意味着函数应该是第一等级(First-class)的值,并且能够被赋值给变量,传递给函数等等。

事实上,很多函数式语言比这走得更远,将计算和算法看得比它们操作的数据更重要。其中有些语言想分离程序状态和函数(以一种看起来有点对立的方式,使用面向对象的语言,这通常会将它们联系得更紧密)。

Clojure编程语言就是一个这样的例子,尽管它运行于基于类的Java虚拟机,Clojure的本质是函数式语言,并且在高级语言源程序中不直接公布类和对象(尽管提供了与Java良好的互操作性)。

下面显示的是一个Clojure函数,用于处理日志,是一等公民(First-class
citizen),并且不需要绑定一个类而存在。

(defn build-map-http-entries [log-file]
 (group-by :uri (scan-log-for-http-entries log-file)))

当写在函数中的程序,对给定的输入(不论程序中的其它状态如何)总是返回相同的输出,并且不会产生其它影响,或者改变任何程序状态,这时候函数式编程是最有用的。它们的行为与数学函数相同,有时候把遵循这个标准的函数称为“纯”函数。

纯函数的巨大好处是它们更容易推论,因为它们的操作不依赖于外部状态。函数能够很容易地结合在一起,这在开发者工作流风格中很常见,例如Lisp方言和其它具有强函数传统的语言中很普遍的REPL(Read,
Execute, Print, Loop)风格。

简单理解为闭包就是定义在函数内部的函数

Lambda这个名称来自于把闭包绑定到Java编程语言的Lambda项目。Lambda以及闭包的引入能做些什么?你可以这样想,它能够让程序
员把一段程序代码当做数据一样使用。一个方法可以像定义和使用一个变量那样的方式被定义和使用,定义出的方法可以被当作参数传递到其它方法内,就像它们是
一个对象实例或一个类型数据一样。“看起来这好像也没什么,但实际上它影响巨大,”Goetz说。“这将从根本上改变我们开发java程序的方式。”

非函数式编程语言中的函数式编程

一种语言是不是函数式并不是非此即彼的状态,实际上,语言存在于图谱上。在最末端,基本上是强制函数式编程,通常禁止可变的数据结构。Clojure就是一种不接受可变数据的语言。

不过,也有一些其它语言,通常以函数方式编程,但语言并不强制这一点。Scala就是一个例子,它混和了面向对象和函数式语言。允许函数作为值,例如:

val sqFn = (x: Int) => x * x

同时保留与Java非常接近的类和对象语法。

另一个极端,当然,使用完全非函数式语言进行函数式编程是可能的,例如C语言,只要维持好合适的程序员准则和惯例。

考虑到这一点,函数式编程应该被看作是有两个因素的函数,其中一个与编程语言相关,另一个是用该语言编写的程序:

1)底层编程语言在多大程度上支持,或者强制函数式编程?

2)这个特定的程序如何使用语言提供的函数式特性?它是否避免了非函数式特性,例如可变状态?

  • Lambda 表达式与匿名类的主要不同在于两者的编译方法
  • 对于匿名类,关键词 this 解读为匿名类,而对于 Lambda 表达式,关键词
    this 解读为写就 Lambda 的外部类

我们等待了太久,但随着Java8的发布,Lambda终于成为Java规格说明书里的正式特征之一。一种由于过于复杂而最初被传统程序员放弃的语法将最终成为一种每个现代Java应用程序里都能看到的标准技术。

Java的一些历史

Java是一种固执己见的语言,它具有很好的可读性,初级程序员很容易上手,具有长期稳定性和可支持性。但这些设计决定也付出了一定的代价:冗长的代码,类型系统与其它语言相比显得缺乏弹性。

然而,Java的类型系统已经在演化,虽然在语言的历史当中相对比较慢。我们来看看这些年来它的一些形式。

任何可以接受一个函数式接口的地方都可以用lambda表达式lambda作用在于

[本文英文原文链接:Lambda in Java 8: A fundamental change in how Java
programs are
developed
]

Java最初的类型系统

Java最初的类型系统至今已经超过15年了。它简单而清晰,类型包括引用类型和基本类型。类、接口或者数组属于引用类型。

  • 类是Java平台的核心,类是Java平台将会加载、或链接的功能的基本单位,所有要执行的代码都必须驻留于一个类中。
  • 接口不能直接实例化,而是要通过一个实现了接口API的类。
  • 数组可以包含基本类型、类的实例或者其它数组。
  • 基本类型全部由平台定义,程序员不能定义新的基本类型。

从最早开始,Java的类型系统一直坚持很重要的一点,每一种类型都必须有一个可以被引用的名字。这被称为“标明类型(Nominative
typing)”,Java是一种强标明类型语言。

即使是所谓的“匿名内部类”也仍然有类型,程序员必须能引用它们,才能实现那些接口类型:

Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };

换种说法,Java中的每个值要么是基本类型,要么是某个类的实例。

  • 逻辑更紧凑
  • 引入闭包,更简洁的实现闭包
  • 允许函数方法作为对象传递

本文转载自: 外刊IT评论

命名类型(Named Type)的其它选择

其它语言没有这么迷恋命名类型。例如,Java没有这样的Scala概念,一个实现(特定签名的)特定方法的类型。在Scala中,可以这样写:

x : {def bar : String}

记住,Scala在右侧标示变量类型(冒号后面),所以这读起来像是“x是一种类型,它有一个方法bar返回String”。我们能用它来定义类似这样的Scala方法:

def showRefine(x : {def bar : String}) = { print(x.bar) }

然后,如果我们定义一个合适的Scala对象:

object barBell { def bar = "Bell" }

然后调用showRefine(barBell),这就是我们期待的事:

showRefine(barBell) Bell

这是一个精化类型(Refinement
typing)的例子。从动态语言转过来的程序员可能熟悉“鸭子类型(Duck
typing)”。结构精化类型(Structural refinement
typing)是类似的,除了鸭子类型(如果它走起来像鸭子,叫起来像鸭子,就可以把它当作鸭子)是运行时类型,而这些结构精化类型作用于编译时。

在完全支持结构精化类型的语言中,这些精化类型可以用在程序员可能期望的任何地方,例如方法参数的类型。而Java,相反地,不支持这样的类型(除了几个稍微怪异的边缘例子)。

函数式接口

所谓的函数式接口,也叫SAM接口(Single Abstract Method
interfaces
),当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法

  • 函数式接口里允许定义默认方法
  • 函数式接口里允许定义静态方法
  • 函数式接口里允许定义java.lang.Object里的public方法

 //以下都是不会报错的 @FunctionalInterface interface GreetingService{ void sayMessage(String message); default void doSomeMoreWork1() { // Method body } static void printHello(){ System.out.println; } @Override boolean equals(Object obj); }

加不加@FunctionalInterface对于接口是不是函数式接口没有影响,只是提醒编译器去检查该接口是否仅包含一个抽象方法,用于编译级错误检查

  • Predicate:用于判断一个对象是否满足某种条件,只有一个test抽象函数,接受一个泛型T对象返回boolean
  • Consumer:用于对对象进行消费操作,只有一个accept抽象函数,接受一个泛型对象无返回
  • Function:用于对象的转换,只有一个apply抽象函数,接受一个泛型T,返回一个泛型R
  • Supplier:用于创建对象
  • 还有一些衍生的可以自己看包下源码

使用相信大家都会一些,我就不列举,网上大把的文章,下面两篇总结不错

深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

10个实例表达式

Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有静态函数

  • 它使用 Java 7 中新加的
    invokedynamic,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口
  • 生成一个类私有静态函数,在生成的实现类中调用

关于 Java 如何将 Lambda 表达式编译为字节码

国外的一篇写的不错

通俗易懂的解释

interface Print<T> { public void print;}public class Lambda { public static void PrintString(String s, Print<String> print) { print.print; } public static void main(String[] args) { PrintString("test",  -> System.out.println; }}

通过编译最终等价于

interface Print<T> { public void print;}public class Lambda { public static void PrintString(String s, Print<String> print) { print.print; } private static void lambda$0 { System.out.println; } final class $Lambda$1 implements Print{ @Override public void print { lambda$0x); } } public static void main(String[] args) { PrintString("test", new Lambda().new $Lambda$1; }}

Oracle写的lambda表现文档

16页讲到最差也和inner class一样, non-capture好的情况是inner class的5倍

图片 1image1

但是在使用Stream的时候并不是所有情况都比普通迭代快的

下面这篇文章比较了各种情况下imperative code与functional code的性能表现

The effects of programming with Java 8 Streams on algorithm performance

Lambda最佳结合应该就是Stream了,函数式编程与简洁的结合Java
8的Stream内置了许多类似于数据库的操作filter、sort、map、reduce等用法就不贴了,大把文章官方的在这里

Stream优点:

  • 以数据库操作数据的方式,专注于如何做这个某个步骤,表达式的方式

  • 高并发(看到map、reduce就应该能想到了)

刚开始看Stream感觉和RxJava非常像,但是仔细思索后有发现其实是两个不同的东西,只是长得像而已。下面这个最高票回答总结的非常好,总体来说

  • stream是单次使用,是基于被动PULL
  • rx是基于观察者模式,可多次订阅,是基于主动PUSH

Stream与Rxjava的不同

Java 5类型系统

Java 5的发布为类型系统带来了三个主要新特性,枚举、注解和泛型。

  • 枚举类型(Enum)在某些方面与类相似,但是它的属性只能是指定数量的实例,每个实例都不同并且在类描述中指定。主要用于“类型安全的常量”,而不是当时普遍使用的小整数常量,枚举构造同时还允许附加的模式,有时候这非常有用。
  • 注解(Annotation)与接口相关,声明注解的关键字是@interface,以@开始表示这是个注解类型。正如名字所建议的,它们用于给Java代码元素做注释,提供附加信息,但不影响其行为。此前,Java曾使用“标记接口(Marker
    interface)”来提供这种元数据的有限形式,但注解被认为更有灵活性。
  • Java泛型提供了参数化类型,其想法是一种类型能扮演其它类型对象的“容器”,无需关心被包含类型的具体细节。装配到容器中的类型通常称为类型参数。

Java
5引入的特性中,枚举和注解为引用类型提供了新的形式,这需要编译器特殊处理,并且有效地从现有类型层级结构分离。

泛型为Java的类型系统增加了显著额外的复杂性,不仅仅因为它们是纯粹的编译时特性,还要求Java开发人员应注意,编译时和运行时的类型系统彼此略有不同。

尽管有这些变化,Java仍然保持标明类型。类型名称现在包括List(读作:“List-of-String”)和Map,
CachedObject>(“Map-of-Class-of-Unknown-Type-to-CachedObject”),但这些仍然是命名的类型,并且每个非基本类型的值仍是某个类的实例。

Java 6和7引入的特性

Java
6基本上是一个性能优化和类库增强的版本。类型系统的唯一变化是扩大注解角色,发布可插拔注解处理功能。这对大多数开发者没有任何影响,Java
6中也没有真正提供可插拔类型系统。

Java 7的类型系统没有重大改变。仅有的一些新特性,看起来都很相似:

  • javac编译器中类型推断的小改进。
  • 签名多态性分派(Signature polymorphic
    dispatch),用于方法句柄(Method handle)的实现细节,而这在Java
    8中又反过来用于实现Lambda表达式。
  • Multi-catch提供了一些“代数数据类型”的小跟踪信息,但这些完全是javac内部的,对最终用户程序员没有任何影响。

Java 8的类型系统

纵观其历史,Java基本上已经由其类型系统所定义。它是语言的核心,并且严格遵守着标明类型。从实际情况来看,Java类型系统在Java
5和7之间没有太大变化。

乍一看,我们可能期望Java
8改变这种状况。毕竟,一个简单的Lambda表达式似乎让我们移除了标明类型:

() -> { System.out.println("Hello World!"); }

这是个没有名字、没有参数的方法,返回void。它仍然是完全静态类型的,但现在是匿名的。

我们逃脱了名词的王国?这真的是Java的一种新的类型形式?

也许不幸的是,答案是否定的。JVM上运行的Java和其它语言,非常严格地限制在类的概念中。类加载是Java平台的安全和验证模式的中心。简单地说,不通过类来表示一种类型,这是非常非常难的。

Java
8没有创建新的类型,而是通过编译器将Lambda表达式自动转换成一个类的实例。这个类由类型推断来决定。例如:

Runnable r = () -> { System.out.println("Hello World!"); };

右侧的Lambda表达式是个有效的Java
8的值,但其类型是根据左侧值推断的,因此它实际上是Runnable类型的值。需要注意的是,如果没有正确地使用Lambda表达式,可能会导致编译器错误。即使是引入了Lambda,Java也没有改变这一点,仍然遵守着标明类型。

Java 8的函数式编程怎么样?

最后,让我们回到本文开头提出的问题,“Java 8的函数式编程怎么样?”

Java
8之前,如果开发者想以函数式风格编程,他或她只能使用嵌套类型(通常是匿名内部类)作为函数代码的替代。默认的Collection类库不会为这些代码提供任何方便,可变性的魔咒也始终存在。

Java
8的Lambda表达式没有神奇地转变成函数式语言。相反,它的作用仍是创建强制的强命名类型语言,但有更好的语法支持Lambda表达式函数文本。与此同时,Collection类库也得到了增强,允许Java开发人员开始采用简单的函数式风格(例如filter和map)简化笨重的代码。

Java
8需要引入一些新的类型来表示函数管道的基本构造块,如java.util.function中的Predicate、Function和Consumer接口。这些新增的功能使Java
8能够“稍微函数式编程”,但Java需要用类型来表示它们(并且它们位于工具类包,而不是语言核心),这说明标明类型仍然束缚着Java语言,它离纯粹的Lisp方言或者其它函数式语言是多么的遥远。

除了以上这些,这个函数式语言能量的小集合很可能是所有大多数开发者日常开发所真正需要的。对于高级用户,还有(JVM或其它平台)其它语言,并且毫无疑问,将继续蓬勃发展。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图