关于“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 的函数式编程怎么样?
最后,让我们回到本文开头提出的问题,“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 或其它平台)其它语言,并且毫无疑问,将继续蓬勃发展。
关于作者
Ben Evans是 jClarity 公司的 CEO,这是一家 Java/JVM 性能分析的初创公司。工作之余他是伦敦 Java 社区领导之一,同时是 JCP 执行委员会成员。他之前的项目包括 Google IPO 性能测试、金融交易系统,为 90 年代的一些大电影等等编写获奖网站。
活动推荐:
2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。
评论