微信号:infoqchina

介绍:有内容的技术社区媒体

浅谈Java 8的函数式编程

2014-05-29 20:00 InfoQ

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


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


函数式编程语言是什么?


函数式编程语言的核心是它以处理数据的方式处理代码。这意味着函数应该是第一等级(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)风格。


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


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


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


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


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


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


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


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


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


Java的一些历史


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


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


Java最初的类型系统


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

  • 类是Java平台的核心,类是Java平台将会加载、或链接的功能的基本单位,所有要执行的代码都必须驻留于一个类中。

  • 接口不能直接实例化,而是要通过一个实现了接口API的类。

  • 数组可以包含基本类型、类的实例或者其它数组。

  • 基本类型全部由平台定义,程序员不能定义新的基本类型。


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


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


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


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


命名类型(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,相反地,不支持这样的类型(除了几个稍微怪异的边缘例子)。


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的函数式编程怎么样?”的话题,更多精彩内容,请点击“阅读原文”。


***********************************

本文来自InfoQ微信公众账号:infoqchina

1、回复“今日新闻”,查看今天更新的新闻;

2、回复“今日英文”,查看今天英文站的更新;

3、回复“文章 +关键词”,搜索关键词相关内容;

4、回复“QCon”,了解QCon大会相关信息;

5、回复“活动”,了解最近InfoQ组织的线下沙龙;

6、回复“架构师”,获取《架构师》下载地址;

7、回复“投稿”,了解投稿和加入编辑团队的流程。

***********************************

 
InfoQ 更多文章 Facebook如何实现PB级别数据库自动化备份 学术派Google软件工程师Matt Welsh谈移动开发趋势 Spotify为什么要使用一些“无聊”的技术? 妹纸们放假了,汉纸们做啥? 大多数重构可以避免
猜您喜欢 互联网大佬都曾是程序员,我到底差在哪儿? 《谈判天下》:任何事情都可以好好谈 哈佛最新颠覆新理论【JTBD】助推产品创新 R及其拓展包的安装和使用 JsBridge实现JavaScript和Java的互相调用