【编者的话】2014 年 3 月份众人期待已久的 Java 8 发布了,新版本从语言、编译器、类库和工具等方面对 Java 进行了诸多改进与提升,一时间风光无限;而 JVM 体系的另一门语言 Scala 则因为融合了函数式编程语言与面向对象编程语言的优点,从诞生以来就一直备受瞩目,迅速赢得了社区的强烈支持。两门语言孰优孰劣或许不能简单地做出定论,这取决于具体的应用场景、资源约束以及团队偏好等因素,但是无论作何选择首先都需要对它们有深入的了解,本文来自于 Zappos 公司 Hussachai Puripunpinyo 在 Dzone 上发表的一篇文章,介绍了他自己对 Java 和 Scala Lambda 表达式的看法。
Hussachai Puripunpinyo 认为 Java 是一门静态的强类型语言,因此虽然在 Java 8 中函数已经成了一等公民,可以作为函数的参数或者返回值来传递,但是它必须要有一个类型,那就是接口,而 Lambda 表达式就是实现了 Functional 接口的对象。虽然开发人员不需要为函数对象的创建而担心,因为编译器会做这些事情,但是 Java 并没有 Scala 那么出色的类型推理机制,在 Java 中声明 Lambda 表达式必须要指定目标类型。考虑到 Java 必须维持向后兼容性,这样做也是可以让人接受和理解的,事实上在兼容性方面 Java 已经做的足够好了,例如 Thread.stop() 在 JDK 1.0 中就已经有了,虽然被标记为“已废弃”数十年,但是现在依然存在,因而不应该因为其他语言有更好的语法就期望 Java 快速地改变自己的语法。
为了支持 Lambda 表达式 Java 引入了函数式接口,该接口只有一个抽象方法。@FunctionalInterface 是一个注解,用来表明被注解的接口是一个函数式接口,该注解是可选的,只有需要对接口是否符合契约做检查的时候才需要使用。
在 Java 中,Lambda 表达式必须要有一个类型,而且类型必须有且仅有一个抽象方法,而大部分已有的回调接口已经满足这一需求,因此用户不需要对它们做出任何改变就能够重用这些接口。例如:
// 在 Java 8 之前 Runnable r = new Runnable(){ public void run(){ System.out.println(“This should be run in another thread”); } }; //Java 8 Runnable r = () -> System.out.println(“This should be run in another thread”);
对于有一个参数并且有返回值的函数,Java 8 提供了一组通用的函数式接口,这些接口在 java.util.function 包中,使用方式如下:
//Java 8 Function<string integer=""> parseInt = (String s) -> Integer.parseInt(s); </string>
因为参数类型可以从 Function 对象的类型声明中推断出来,所以类型和小括号都可以省略:
//Java 8 Function<string integer=""> parseInt = s -> Integer.parseInt(s); </string>
对于需要两个参数的函数,Java 8 提供了 BiFunction:
//Java 8 BiFunction<integer integer=""> multiplier = (i1, i2) -> i1 * i2; //you can’t omit parenthesis here! </integer>
对于需要 3 个及以上参数的接口,Java 8 并没有提供相应的 TriFunction、QuadFunction 等定义,但是用户可以定义自己的 TriFunction,如下:
//Java 8 @FunctionalInterface interface TriFunction<a b="" c="" r=""> { public R apply(A a, B b, C c); } </a>
在引入了之前定义好的接口之后就可以这样声明 Lambda 表达式:
//Java 8 TriFunction<integer integer=""> sumOfThree = (i1, i2, i3) -> i1 + i2 + i3; </integer>
对于语言的设计者为什么会止步于 BiFunction,Hussachai Puripunpinyo 认为 TriFunction、QuadFunction 等需要更多参数的接口需要太多的类型声明,接口的定义变得非常长,同时又怎么决定定义到哪一个才最合适呢,总不能一直定义到包含 9 个参数和一个返回值类型的 EnnFunction 吧!
以上示例显示参数越多,类型定义越冗长,甚至可能整整一行都是类型声明,那么必须要声明类型么?答案是在 Java 中必须如此,但是在 Scala 中就简单的多了。
Scala 也是一门静态强类型的语言,但是它从诞生开始就是一门函数式语言,完美融合了面向对象范式和函数式语言范式。Scala 中的 Lambda 表达式也有一个类型,但是语言的设计者采用了数字而不是拉丁语来命名,Scala 为开发者提供了 0 到 22 个参数的接口定义(Function0、Function1、… Function22),如果需要更多的参数,那么或许是开发者在设计上就存在问题。在 Scala 中 Function 的类型是特性(trait),类似于 Java 中的抽象类。
Scala 中的 Runnable 示例与 Java 中的实现方式不同:
//Scala Future(println{“This should be run in another thread”}) // 以上代码等同于 //Java 8 //assume that you have instantiated ExecutorService beforehand. Runnable r = () -> System.out.println(“This should be run in another thread”); executorService.submit(r);
在 Scala 中声明一个 Lambda 表达式不必像 Java 那样必须显式指定类型,而且方式也有很多:
//Java 8 Function<string integer=""> parseInt = s -> Integer.parseInt(s); //Scala val parseInt = (s: String) => s.toInt //or val parseInt:String => Int = s => s.toInt //or val parseInt:Function1[String, Int] = s => s.toInt </string>
如果需要更多的参数:
//Java 8 PentFunction<integer integer=""> sumOfFive = (i1, i2, i3, i4, i5) -> i1 + i2 + i3 + i4 + i5; //Scala val sumOfFive = (i1: Int, i2: Int, i3: Int, i4: Int, i5: Int) => i1 + i2 + i3 + i4 + i5; </integer>
可以看到,Scala 的语法更简洁,可读性更好,开发者不需要声明接口类型,通过参数列表中的类型就能看出对象的类型。
//Java 8 PentFunction<string boolean="" double="" integer="" string=""> sumOfFive = (i1, i2, i3, i4, i5) -> i1 + i2 + i3 + i4 + i5; //Scala val sumOfFive = (i1: String, i2: Int, i3: Double, i4: Boolean, i5: String) => i1 + i2 + i3 + i4 + i5; </string>
对于上面这段代码,开发者一打眼就能看出 i3 是 Double 类型的,但是在 Java 8 中开发者必须要数一数才能看出来,如果要在 Java 8 中达到这种效果,那只有从格式上来做文章了:
//Java 8 PentFunction sumOfFive = (Integer i1, String i2, Integer i3, Double i4, Boolean i5) -> i1 + i2 + i3 + i4 + i5;
但是这真是非常糟糕,开发者必须一次次地键入类型,另外,Java 8 并没有定义 PentFunction,你还必须自己定义:
//Java 8 @FunctionalInterface interface PentFunction<a b="" c="" d="" e="" r=""> { public R apply(A a, B b, C c, D d, E e); } </a>
Hussachai Puripunpinyo 认为 Scala 在函数式方面做的更好,一方面是因为 Scala 本身就是一门函数式语言,另一方面是因为 Java 语言的设计者在引入新东西的时候必须要考虑兼容性,因而有很多约束。但是即使如此,Java 8 依然引入了一些非常酷的特性,例如方法引用,该特性就能够让 Lambda 表达式的声明更加简短:
//Java 8 Function<string integer=""> parseInt = s -> Integer.parseInt(s); // 使用方法引用可以简写为: //Java 8 Function<string integer=""> parseInt = Integer::parseInt; </string></string>
在 Java 8 中,方法引用的构建规则有 3 种:
- (args) -> ClassName.staticMethod(args);
可以重写为ClassName::staticMethod;
Function<integer string=""> intToStr = String::valueOf;</integer>
3. (instance, args) -> instance.instanceMethod(args);
可以重写为ClassName::instanceMethod;
BiFunction<string integer="" string=""> indexOf = String::indexOf; </string>
- (args) -> expression.instanceMethod(args);
可以重写为expression::instanceMethod;
Function<string integer=""> indexOf = new String()::indexOf; </string>
流 API 就大量使用了这种语法来简化代码的编写:
pets.stream().map(Pet::getName).collect(toList()); // The signature of map() function can be derived as // <string> Stream<string> map(Function super Pet, ? extends String> mapper) </string></string>
编后语
《他山之石》是 InfoQ 中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到 editors@cn.infoq.com。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。
评论