浅谈Java 7的闭包与Lambda表达式之优劣

开发 后端
前几天Oracle推出了Java 7官方的闭包与Lambda表达式的第一个实现,这基本上也是最终在正式版中的样式了。看了这个实现之后,我的第一感觉便是“丑”,后来再仔细看了看又想了想,发现Java 7的实现也并非毫无可取之处,但似乎又感到某些做法上有一些问题。

前几天Oracle推出了Java 7官方的闭包Lambda表达式的第一个实现,这基本上也是最终在正式版中的样式了。看了这个实现之后,我的第一感觉便是“丑”,当然不排除这是因为看惯了其他语言中实现的缘故。后来再仔细看了看又想了想,发现Java 7的实现也并非毫无可取之处,但似乎又感到某些做法上有一些问题。总之整个过程颇为有趣,决定将我的想法记录下来,希望可以吸引人来一起讨论一下。

Java 7中的Lambda表达式

Java 7中的Lambda表达式有两种形式,首先是第一种:

  1. #int() func1 = #()(3); // "func1.()" returns 3  
  2. #int(int) func2 = #(int x)(x + 1); // "func2.(3)" returns 4  
  3. #int(int, int) func3 = #(int x, int y)(x - y); // "func3.(5, 3)" returns 2 

然后是第二种,含义与上面等价:

  1. #int() func1 = #(){ return 3; };  
  2. #int(int) func2 = #(int x){ return x + 1; };  
  3. #int(int, int) func3 = #(int x, int y){ return x – y; }; 

如果Lambda的body是“单个表达式”的话,便可以使用“小括号”,并省去最后的return关键字;如果body中需要包含多条语句的话,则必须使用“大括号”,而大括号内部可以包含多条语句,就像一个普通的方法体一样。这两种写法在C#中也有对应物,如在“单个表达式”的情况下:

  1. // C#  
  2. Func<int> func1 = () => 3; // "func1()" returns 3  
  3. Func<int, int> func2 = x => x + 1; // "func2(3)" returns 4   
  4. Func<int, int, int> func3 = (x, y) => x - y; // "func3(5, 3)" returns 2  

第二种,即多条语句:

  1. // C#  
  2. Func<int> func1 = () => { return 3; };  
  3. Func<int, int> func2 = x => { return x + 1; };  
  4. Func<int, int, int> func3 = (x, y) => { return x – y; };  
  5.  

Java和C#的Lambda表达式都由两部分组成:“参数列表”和“表达式体”,但是它们有如下区别:

◆在Java中参数列表和表达式体之间没有分隔符号,而C#使用“=>”分隔。

◆对于“单个表达式”的Lambda来说,C#可以无需使用括号包含表达式体,而Java必须使用小括号。

◆如果只有单个参数,那么C#的参数列表可以省去小括号,而Java必须保留。

◆C#对参数列表会进行“类型推断”,而Java必须写清参数类型。

这些区别说大可大,说小可小,但是Java语言的设计的确让我感觉较C#为“丑”,这可能是个人主观因素,但我认为也不尽然。例如,如果我们需要对一个用户对象数组按照“年龄”进行排序,在C#里可以写作:

  1. // C#  
  2. users.Sort(u => u.Age);  
  3.  

而在Java中则必须写为:

  1. Arrays.sort(users, #(User u)(u.Age));  
  2.  
  3.    

这句C#代码语义清晰:按照“u的Age进行排序”,而在Java代码中便显得比较累赘,语义似乎也不够清晰。Anders在设计C#语法的时候非常注重“声明式”代码,由此可见一斑。此外,我不明白为什么Java选择不对参数进行类型推断,在我看来这对于写出优雅代码十分重要(关于这点,在“Why Java Sucks and C# Rocks”系列中会有更详细的讨论)。不过Java也不是没有“推断”,例如从上面的代码片断中可以得知,Java对于Lambda表达式的返回值还是进行了类型推断。事实上,Java还推断了“异常类型”,这点稍后会有更多讨论。

当然,Java中可以“无中生有”地定义“匿名函数类型”(这点和VB.NET相对更为接近),而不需要像C#一样需要基于特定的“委托类型”,显得更为灵活。

#p#

SAM类型支持及闭包

SAM的全称是Single Abstract Method,如果一个类型为SAM类型,则意味着它 1) 是抽象类型(即接口或抽象类),且 2) 只有一个未实现的方法。例如这样一个Java接口便是个SAM类型:

  1. public interface Func<T, R> {  
  2.     R invoke(T arg);  
  3. }  
  4.  
  5.    
  6.  

于是我们便可以:

  1. Func<int, int>[] array = new Func<int, int>[10];  
  2. for (int i = 0; i < array.length; i++) {  
  3.     final int temp = i;  
  4.     array[i] = #(int x)(x + temp);  
  5. }  
  6.  
  7.  
  8.  

可见,我们使用Lambda表达式创建了Func接口的实例,这点是C#所不具备的。这点十分关键,因为在Java类库中已经有相当多的代码使用了SAM类型。不过我发现,在某些使用SAM的方式下似乎会产生一些“歧义”,例如这段代码:

  1. public class MyClass {  
  2.     @Override  
  3.     public int hashCode() {  
  4.         throw new RuntimeException();  
  5.     }  
  6.  
  7.     public void MyMethod() {  
  8.         Func<int, int> func = #(int x)(x * hashCode());  
  9.         int r = func.invoke(5); // throw or not?  
  10.     }  
  11. }  
  12.  
  13.    

在这里我们覆盖(override)了MyClass的hashCode方法,使它抛出RuntimeException,那么在调用MyMethod中定义的func1对象时会不会抛出异常?答案是否定的,因为在这个Lambda表达式中,隐藏的“this引用”代表了func对象,调用它的hashCode不会抛出RuntimeException。那么,假如我们要调用MyClass的hashCode怎么办?那就稍微有些麻烦了:

  1. Func<int, int> func = #(int x)(x * MyClass.this.hashCode());  
  2.  
  3.    
  4.  

不过从另一段示例代码上看:

  1. public class MyClass {  
  2.  
  3.     public int n = 3;  
  4.  
  5.     public void MyMethod() {  
  6.         Func<int, int> func = #(int x)(x + n);  
  7.         int r = func.invoke(5); // 8  
  8.     }  
  9. }  

由于Func对象上没有n,因此这里的n便是MyClass类里定义的n成员了。因此,Java的闭包并非不会捕获字面上下文里的成员,只是在SAM类型的情况下,字面范围内(lexical scope)成员的优先级会低于目标抽象类型的成员。

总体来说,对于SAM类型的支持上,我认为Java是有可取之处的,只是我始终认为这个做法会产生歧义,因为我印象中其他语言里的Lambda表达式似乎都是捕获字面上下文的(当然它们可能也没有SAM支持)。但是,如何在“歧义”和“优雅”之间做出平衡,我一时也找不到令人满意的答案。

硬伤:Checked Exception

Java相当于其他常见语言有一个特别之处,那就是Checked Exception。Checked Exception意味着每个方法要标明自己会抛出哪些异常类型(RuntimeException及其子类除外),这也是方法契约的一部分,编译器会强制程序员写出满足异常契约的代码。例如某个类库中定义了这样一个方法:

  1. public void myMethod() throws AException, BException  
  2.  
  3.    

其中throws后面标注的便是myMethod可能会抛出的异常。于是如果我们要写一个方法去调用myMethod,则可能是:

  1. public void myMethodCaller() throws AException {  
  2.     try {  
  3.         myMethod();  
  4.     } catch (BException ex) {  
  5.         throw new AException(ex);  
  6.     }  
  7. }  
  8.  
  9.    

当我们写一个方法A去调用方法B时,我们要么在方法A中使用try...catch捕获B抛出的方法,要么在方法A的签名中标记“会抛出同样的异常”。如上面的myMethodCaller方法,便在内部处理了BException异常,而只会对外抛出AException。Java便使用这种方法严格限制了类库的异常信息。

Checked Exception是一个有争议的特性。它对于编写出高质量的代码非常重要,因为在哪些情况抛出异常其实都是方法契约的一部分(不仅仅是签名或返回值的问题),应该严格遵守,在类库升级时也不能破坏,否则便会产生兼容性的问题。例如,您关注MSDN里的文档时,就会看到异常的描述信息,只不过这是靠“文档”记录的,而Java则是强制在代码中的;但是,从另一个角度说,Checked Exception让代码编写变得非常麻烦,这导致的一个情况便是许多人在写代码时,自定义的异常全都是RuntimeException(因为不需要标记),每个方法也都是throws Exception的(这样代码中就不需要try...catch了),此时Checked Exception特性也基本形同虚设,除了造成麻烦以外几乎没有带来任何好处。

我之前常说:一个特性如果要被人广泛接受,那它一定要足够好用。现在如Scala和Grovvy等为Java设计的语言中都放弃了Checked Exception,这也算是从侧面印证了Checked Exception的尴尬境地吧。

#p#

而Checked Exception对于如今Lambda或闭包来说,在我看来更像是一种硬伤。为什么这么说?举个例子吧,假如有这么一个map方法,可以把一个数组映射成另一个类型数组:

  1. public R[] map(T[] array, Func<T, R> mapper) { ... }  
  2.  
  3.    

好,那么比如这样一个需求:给定一个字符串数组,保存着文件名,要求获得它的标准路径。从表面上看来,我们可以这样写:

  1. map(files, #(String f)(new File(f).getCanonicalPath())  
  2.  
  3.    

但事实上,这么做无法编译通过。为什么?因为getCanonicalPath方法会抛出IOException,我们在调用时必须显式地使用try...catch进行处理。那么这段代码该怎么写?还真没法写。如果没有Checked Exception的话(如C#),我们还可以这么做(处理第一个抛出的IOException):

  1. public interface FuncThrowsIOException<T, R> {  
  2.     R invoke(T arg) throws IOException;  
  3. }  
  4.  

 

 但是,如果我们要写出之前那种“漂亮”的写法,就不能使用Func<T, R>而必须是这样的接口类型:

  1. public interface FuncThrowsIOException<T, R> {  
  2.     R invoke(T arg) throws IOException;  
  3. }  
  4.  

或者是这样的“匿名函数类型”:

#String(String)(throws IOException) // toCanonicalPath = #(String f)(new File(f).getCanonicalPath())
但是,作为Lambda和闭包的常用场景,如map,filter,fold等“函数式”元素,是不可能为某种特定的“异常类型”而设计的——异常类型千变万化,难道这也要用throws Exception来进行“统一处理”吗?Java虽然已经支持对异常类型的“推断”,但Checked Exception还是对Lambda和闭包的适用性造成了很大影响。

因此,我认为Checked Exception是一个“硬伤”。

其他

Java的Lambda和闭包还有一些特性,例如参数的“泛化”:

  1. #boolean(Integer) f = #(Number n)(n.intValue() > 0);  
  2.  

由于Number是Integer的基类,因此我们可以使用Number来构造一个接受Integer参数的匿名函数类型。由于示例较少,我还不清楚这个特性的具体使用场景和意义所在——不过我猜想,在Java中可能允许这样做吧:

  1. #boolean(Number) f = #(Number n)(n.intValue() > 0);  
  2. #boolean(Integer) ff1 = f; // cast implicitly or explicitly  
  3.  

此外还有一些特性,例如与MethodHandle类型的转化,我就没有特别的看法了。

赵劼,网名老赵,洋名Jeffrey Zhao,目前就职于盛大创新院产品开发部,研究员。InfoQ中文站编辑,多次受邀于微软TechED,MSDN WebCast及各微软官方或社区会议中担任技术议题讲师。

原文地址:http://blog.zhaojie.me/2010/06/first-version-of-lambda-and-closures-in-java-7.html

【编辑推荐】

  1. Oracle推出首个Java 7闭包版本
  2. Lambda表达式动态函数编程:更加灵活强大
  3. 详解一个有歧义的Lambda表达式
  4. 详解如何利用Lambda表达式编写递归函数
责任编辑:佚名 来源: 赵劼blog
相关推荐

2009-09-09 13:01:33

LINQ Lambda

2009-08-27 09:57:50

C# Lambda表达

2012-06-26 10:03:58

JavaJava 8lambda

2024-03-25 13:46:12

C#Lambda编程

2024-01-22 09:51:32

Swift闭包表达式尾随闭包

2009-04-29 09:05:59

Lambda抽象代表.NET

2024-03-12 08:23:54

JavaLambda函数式编程

2013-01-05 02:19:50

JavaLambda表达式JVM

2022-12-05 09:31:51

接口lambda表达式

2009-09-15 15:18:00

Linq Lambda

2009-09-11 09:48:27

Linq Lambda

2013-04-07 15:44:26

Java8Lambda

2021-08-31 07:19:41

Lambda表达式C#

2009-08-27 09:44:59

C# Lambda表达

2009-09-15 17:30:00

Linq Lambda

2009-09-17 10:40:22

Linq Lambda

2009-09-17 09:44:54

Linq Lambda

2023-07-31 07:43:07

Java语言表达式变量

2013-04-07 10:04:03

Java8Lambda

2012-07-18 09:45:32

Java 8ScalaLambda
点赞
收藏

51CTO技术栈公众号