博客地址 http://colobu.com
Java 8发布有一段日子, 大家关注Java 8中的lambda可能更早, 对Java 8中这一最重要的语言变化也基本熟悉了。这篇文章将深入研究Java 8中的lambda特性以及Stream接口等, 讨论一些深层次的技术细节。
比如, 一个lambda表达式序列化反序列化后, 对捕获的上下文变量的引用的情况。 Lambda表达式递归。 类方法的引用和实例方法的引用的区别。 菱形继承的问题。 Stream接口的Lazy和eager模式。 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 |
如果body只有一个表达式,可以省略body的·大括号 和 return
。
1
|
Function |
参数可以声明类型,也可以根据类型推断而省略。
1
|
Function |
但是不能部分省略。
1
|
Function |
单个的参数可以省略括号。
1
2
|
Function
Function |
但是不能加上类型声明。
1
|
Function |
如果没有参数, 括号是必须的。
1
2
|
() ->
1995
() -> { System.gc(); }
|
匿名函数是没有名字的, 但是Lambda表达式可以赋值给一个变量或者作为参数传递, 这意味着它有”名字”。 那么可以利用这个名字进行递归吗?
lambdafaq网站说可以。
1
2
|
Function
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
{
return (E[]) Array.newInstance(clazz, size);
}
public
static
void
main(String[] args)
throws InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException {
//Function
Function
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
Function |
就像本地类和匿名类一样, 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
。
方法引用是一个有趣的特性, 方法类似指针一样可以被直接引用。 新的操作符”::”用来引用类或者实例的方法。
1
2
3
4
|
BiFunction
Long a =
10L;
Long b =
11L;
System.out.println(bf.apply(a, b));
|
1
2
|
Consumer
c.accept(
"hello colobu");
|
以上两种情况引用的方法签名应和 target type的方法签名一样,方法的名字不一定相同。
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)。
另一种特殊的方法引用是对构造函数的引用。
对构造函数的引用类似对静态方法的引用,只不过方法名是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
。
原则:
基本上,你可以根据以下三条原则判断多继承的实现规则。
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又被称作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,因为没有声明一个方法。
``` java
interface Foo {
int m();
Object clone();
}
也不是一个functional interface, 因为Object.clone不是public类型的。
Oracle公司的性能工程师Sergey Kuksenko有一篇很好的性能比较的文档: JDK 8: Lambda Performance study, 详细而全面的比较了lambda表达式和匿名函数之间的性能差别。这里是视频。 16页讲到最差(capture)也和inner class一样, non-capture好的情况是inner class的5倍。
lambda开发组也有一篇ppt, 其中也讲到了lambda的性能(包括capture和非capture的情况)。看起来lambda最差的情况性能内部类一样, 好的情况会更好。
Java 8 Lambdas - they are fast, very fast也有篇文章 (可能需要),表明lambda表达式也一样快。