Java 8 vs. Scala之Lambda表达式

【编者的话】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
  
    parseInt = (String s) -> Integer.parseInt(s);

  

因为参数类型可以从Function对象的类型声明中推断出来,所以类型和小括号都可以省略:

//Java 8
Function
  
    parseInt = s -> Integer.parseInt(s);

  

对于需要两个参数的函数,Java 8提供了BiFunction:

//Java 8
BiFunction
  
    multiplier = 
  (i1, i2) -> i1 * i2; //you can’t omit parenthesis here!

  

对于需要3个及以上参数的接口,Java 8并没有提供相应的TriFunction、QuadFunction等定义,但是用户可以定义自己的TriFunction,如下:

//Java 8
@FunctionalInterface
interface TriFunction
  
    {  
  public R apply(A a, B b, C c);
}

  

在引入了之前定义好的接口之后就可以这样声明Lambda表达式:

//Java 8
TriFunction
  
    sumOfThree 
  = (i1, i2, i3) -> i1 + i2 + i3;

  

对于语言的设计者为什么会止步于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
  
    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


  

如果需要更多的参数:

//Java 8
PentFunction
  
    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;

  

可以看到,Scala的语法更简洁,可读性更好,开发者不需要声明接口类型,通过参数列表中的类型就能看出对象的类型。

//Java 8
PentFunction
  
    
  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;

  

对于上面这段代码,开发者一打眼就能看出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
  
    {  
  public R apply(A a, B b, C c, D d, E e);
}

  

Hussachai Puripunpinyo认为Scala在函数式方面做的更好,一方面是因为Scala本身就是一门函数式语言,另一方面是因为Java语言的设计者在引入新东西的时候必须要考虑兼容性,因而有很多约束。但是即使如此,Java 8依然引入了一些非常酷的特性,例如方法引用,该特性就能够让Lambda表达式的声明更加简短:

//Java 8
Function
  
    parseInt = s -> Integer.parseInt(s);
//使用方法引用可以简写为:
//Java 8
Function
   
     parseInt = Integer::parseInt; 
   
  

在Java 8中,方法引用的构建规则有3种:

  1. (args) -> ClassName.staticMethod(args);
    可以重写为ClassName::staticMethod;
  2. Function
       
         intToStr = String::valueOf;
       
  3. (instance, args) -> instance.instanceMethod(args);
    可以重写为ClassName::instanceMethod;
  4. BiFunction
       
         indexOf = String::indexOf;
    
       
  5. (args) -> expression.instanceMethod(args);
    可以重写为expression::instanceMethod;
  6. Function
       
         indexOf = new String()::indexOf;
    
       

流API就大量使用了这种语法来简化代码的编写:

pets.stream().map(Pet::getName).collect(toList());
// The signature of map() function can be derived as
// 
  
    Stream
   
     map(Function
     mapper) 
   
  

编后语

《他山之石》是InfoQ中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到[email protected]

感谢徐川对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected]。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者(已满),InfoQ读者交流群(#2)InfoQ好读者)。

你可能感兴趣的:(Java 8 vs. Scala之Lambda表达式)