java lambda深度研究

目录 [−]

  1. Java Lambda语法
  2. Java Lambda递归
  3. 捕获变量
  4. 序列化
    1. 一般序列化/反序列化
    2. 带捕获变量的序列化/序列化
  5. 方法引用
    1. static method reference
    2. instance method reference
    3. arbitrary instance reference
    4. constructor reference
  6. 多继承
    1. 三角继承
    2. 菱形继承
    3. 叉型继承
  7. Stream接口的lazy方法和eager方法
  8. Functional interface只能有一个方法吗?

Java 8发布有一段日子, 大家关注Java 8中的lambda可能更早, 对Java 8中这一最重要的语言变化也基本熟悉了。这篇文章将深入研究Java 8中的lambda特性以及Stream接口等, 讨论一些深层次的技术细节。
比如, 一个lambda表达式序列化反序列化后, 对捕获的上下文变量的引用的情况。 Lambda表达式递归。 类方法的引用和实例方法的引用的区别。 菱形继承的问题。 Stream接口的Lazy和eager模式。 Lambda的性能。

Java Lambda语法

尽管你已经很熟悉了, 我们还是先回顾一下lambda表达式的语法。

“A lambda expression is like a method: it provides a list of formal parameters and a body—an expression or block—expressed in terms of those parameters,”
JSR 335

1
Function<Integer, Integer> fun = (Integer x, Integer y) -> {return x + y;}

如果body只有一个表达式,可以省略body的·大括号 和 return

1
Function<Integer, Integer> fun = (Integer x, Integer y) -> x + y

参数可以声明类型,也可以根据类型推断而省略。

1
Function<Integer, Integer> fun = (x, y) -> x + y

但是不能部分省略。

1
Function<Integer, Integer> fun = (x, Integer y) -> x + y //wrong

单个的参数可以省略括号。

1
2
Function<Integer, Integer> fun = (x) -> x+1
Function<Integer, Integer> fun = x -> x+1

但是不能加上类型声明。

1
Function<Integer, Integer> fun = Integer x -> x+1 //wrong

如果没有参数, 括号是必须的。

1
2
() -> 1995
() -> { System.gc(); }

Java Lambda递归

匿名函数是没有名字的, 但是Lambda表达式可以赋值给一个变量或者作为参数传递, 这意味着它有"名字"。 那么可以利用这个名字进行递归吗?
lambdafaq网站说可以。

1
2
Function<Long, Long> fib = x -> {if (x ==1 || x == 2) return 1L; else return fib.apply(x -1) + x;};
System.out.println(fib.apply(3L));

实际你并不能编译这段代码, 因为编译器认为fib可能没有初始化。

1
The local variable fib may not have been initialized

没办法递归了吗?
有一些hacked方法, 如

1
2
IntToDoubleFunction[] foo = { null };
foo[0] = x -> { return ( x == 0)?1:x* foo[0].applyAsDouble(x-1);};

或者 (泛型数组的创建有些麻烦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   @SuppressWarnings("unchecked")
private static <E> E[] newArray(Class clazz, int size)
{
return (E[]) Array.newInstance(clazz, size);
}

public static void main(String[] args) throws InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException {
//Function<Long, Long> fib = x -> {if (x ==1 || x == 2) return 1L; else return fib.apply(x -1) + x;};

Function<Long, Long>[] funs = newArray(Function.class, 1);
funs[0] = x -> {if (x ==1 || x == 2) return 1L; else return funs[0].apply(x -1) + x;};
System.out.println(funs[0].apply(10L));

}

或者使用一个helper类。

1
2
BiFunction<BiFunction, Long, Long> factHelper = (f, x) -> {if (x ==1 || x == 2) return 1L; else return x + (long)f.apply(f,x-1);};
Function<Long, Long> fib = x -> factHelper.apply(factHelper, x);

捕获变量

就像本地类和匿名类一样, Lambda表达式可以捕获变量(capture variable)。

In addition, a local class has access to local variables. However, a local class can only access local variables that are declared final. When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter

但是Lambda表达式不强迫你将变量声明为final, 只要它的行为和final 变量一样即可,也就是等价final.
下面的例子s不必声明为final,实际加上final也不会编译出错。

1
2
3
String s = "smallnest";
Runnable r = () -> System.out.println("hello " + s);
r.run();

但是下面的例子s实际已经不是final了,编译会出错。,

1
2
3
4
String s = "smallnest";
Runnable r = () -> System.out.println("hello " + s);
s = "colobu";
r.run();

下面的代码一样也会编译不成功:

1
2
3
String s = "smallnest";
Runnable r = () -> {s = "abc"; System.out.println("hello " + s);};
r.run();

注意final仅仅是变量不能再被赋值, 而变量字段的值是可以改变的。

1
2
3
4
5
Sample s = new Sample();
s.setStr("smallnest");
Runnable r = () -> System.out.println("hello " + s.getStr());
s.setStr("colobu");
r.run();

这里我们可以更改s的str字段的值。

序列化

一般序列化/反序列化

Lambda表达式可以被序列化。下面是一个简单的例子。

1
2
3
4
5
6
7
8
9
10
Runnable r = (Runnable & Serializable)() -> System.out.println("hello serialization");

FileOutputStream fos = new FileOutputStream("Runnable.lambda");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(r);

FileInputStream fis = new FileInputStream("Runnable.lambda");
ObjectInputStream is = new ObjectInputStream(fis);
r = (Runnable) is.readObject();
r.run();

注意(Runnable & Serializable)是Java 8中新的语法。 cast an object to an intersection of types by adding multiple bounds.
一个Lambda能否序列化, 要以它捕获的参数以及target type能否序列化为准。当然,不鼓励在实践中使用序列化。上面的例子r实现了Serializable接口,而且没有captured argument,所以可以序列化。

带捕获变量的序列化/序列化

再看一个带captured argument的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	class Sample implements Serializable {
private String str;

public String getStr() {
return str;
}

public void setStr(String str) {
this.str = str;
}
}

public static void serializeLambda() throws Exception {
Sample s = new Sample();
s.setStr("smallnest");
SampleSerializableInterface r = () -> System.out.println("hello " + s.getStr());
FileOutputStream fos = new FileOutputStream("Runnable.lambda");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(r);
s.setStr("colobu");
}

public static void deserializeLambda() throws Exception {
FileInputStream fis = new FileInputStream("Runnable.lambda");
ObjectInputStream is = new ObjectInputStream(fis);
SampleSerializableInterface r = (SampleSerializableInterface) is.readObject();
r.run();
}

可以看到连同captured argument s一同序列化了。 即使反序列化出来,captured argument也不是原来的s了。
结果输出hello smallnest

方法引用

方法引用是一个有趣的特性, 方法类似指针一样可以被直接引用。 新的操作符"::"用来引用类或者实例的方法。

static method reference

1
2
3
4
BiFunction<Long,Long,Integer> bf = Long::compare;
Long a = 10L;
Long b = 11L;
System.out.println(bf.apply(a, b));

instance method reference

1
2
Consumer<String> c = System.out::println;
c.accept("hello colobu");

以上两种情况引用的方法签名应和 target type的方法签名一样,方法的名字不一定相同。

arbitrary instance reference

1
2
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

这是一个很有趣的使用方法。 可以引用任意的一个类型的实例。等价的lambda表达式的参数列表为(String a, String b),方法引用会调用a.compareToIgnoreCase(b)。

constructor reference

另一种特殊的方法引用是对构造函数的引用。
对构造函数的引用类似对静态方法的引用,只不过方法名是new。 一个类有多个构造函数, 会根据target type选择最合适的构造函数。

多继承

由于Java 8引入了缺省方法(default method)的特性,Java也想其它语言如C++一样遇到了多继承的问题。这里列出两个典型的多继承的情况。

三角继承

三角继承如下图所示。
A
|
|
| B
| /
C

1
2
3
4
5
6
7
8
9
10

class A {
public A () {
System.out.println("A()");
}

public A(int x) {
System.out.println("A(int x)");
}
}
1
2
C c = new C();
c.say(); //B says

菱形继承

菱形继承如下图所示
A
/
/
B C
\ /
D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface A {	 
default void say() {
System.out.println("A says");
}
}

interface B extends A{

default void say() {
System.out.println("B says");
}
}

interface C extends A{

default void say() {
System.out.println("C says");
}
}

class D implements A, B, C{

}

直接编译出错。原因是Duplicate default methods.
你需要在D中重载say方法, 自定义或者使用父类/接口的方法。 注意其写法接口.super.default_method_name

1
2
3
4
5
	class D implements A, B, C{
public void say() {
B.super.say();
}
}

叉型继承

叉型继承如下图所示

B C
\ /
D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface B {	 
default void say() {
System.out.println("B says");
}
}

class C {

public void say() {
System.out.println("C says");
}
}

class D extends C implements B{

}

上面的代码输出C says

原则:
基本上,你可以根据以下三条原则判断多继承的实现规则。

  1. 类优先于接口。 如果一个子类继承的父类和接口有相同的方法实现。 那么接口中的定义会被忽略。 如第三个例子。
  2. 子类型中的方法优先于府类型中的方法。 如第一个例子。
  3. 如果以上条件都不满足, 如第二个例子,则必须显示覆盖/实现其方法,或者声明成abstract。

Stream接口的lazy方法和eager方法

新增加的Stream接口有很多方法。

lazy例子:

1
allStudents.stream().filter(s -> as.age> 16);

filter并不会马上对列表进行遍历筛选, 它只是为stream加上一些"秘方"。当前它的方法实现不会被执行。直到遇到eager类型的方法它才会执行。

eager例子:

1
allStudents.stream().filter(s -> as.age> 16).count();

原则:
看方法的返回值。 如果返回Stream对象,那么它是lazy的, 如果返回其它类型或者void,那它是eager的,会立即执行。

Functional interface只能有一个方法吗?

Functional interface又被称作Single Abstract Method (SAM)或者Role Interface
那么接口中只能声明一个方法吗?
上面的例子也表明, 你可以在functional interface中定义多个default method。 事实上java.util.function下好多functional interface都定义default method.
那么除去default method, functional interface可以声明多个的方法吗?看个例子

1
2
3
4
5
interface MyI {
void apply(int i);

String toString();
}

MyI声明了两个方法apply和toString()。 它能作为一个lambda 表达式的target type吗?

1
2
MyI m = x -> System.out.println(x);	
m.apply(10);

没问题, 代码可以正常编译, 程序正常运行。
但是, 等等, 不是functional interface只能声明一个abstract的方法吗?
事实上你看第二个方法比较特殊,它和Object的方法签名相同。它是对象隐性实现的一个方法,所以可以忽略它。

同样,interface Foo { boolean equals(Object obj); }也不是一个functional interface,因为没有声明一个方法。

interface Foo {
  int m();
  Object clone();
}
也不是一个functional interface, 因为Object.clone不是public类型的。

## Lambda的性能
Oracle公司的性能工程师Sergey Kuksenko有一篇很好的性能比较的文档: [JDK 8: Lambda Performance study](http://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf), 详细而全面的比较了lambda表达式和匿名函数之间的性能差别。这里是[视频](http://medianetwork.oracle.com/video/player/2623576348001)。 16页讲到最差(capture)也和inner class一样, non-capture好的情况是inner class的5倍。

lambda开发组也有一篇[ppt](http://wiki.jvmlangsummit.com/images/7/7b/Goetz-jvmls-lambda.pdf), 其中也讲到了lambda的性能(包括capture和非capture的情况)。看起来lambda最差的情况性能内部类一样, 好的情况会更好。

[Java 8 Lambdas - they are fast, very fast](http://nerds-central.blogspot.tw/2013/03/java-8-lambdas-they-are-fast-very-fast.html)也有篇文章 (可能需要翻墙),表明lambda表达式也一样快。
<img alt="Java 8 Lambda Performance Comparison" src="http://1.bp.blogspot.com/-_LVo-w8ClYQ/UUX7cwNEgzI/AAAAAAAAJOc/yiiSapVJVgI/s400/chart.tiff" title="Java 8 Lambda Performance Comparison" height="235" width="400" border="0">