前言
java 8 的 lambda 特性较之于先前的泛型加入更能鼓舞人心的,我对 lambda 的理解是它得以让 java 以函数式思维的方式来写代码。而写出的代码是否是函数式,并不单纯在包含了多少 lambda 表达式,而在思维,要神似。
实际中看过一些代码,为了 lambda 表达式而 lambda(函数式),有一种少年不识愁滋味,为赋新词强说愁的味道。从而致使原本一个简单的方调用硬生生的要显式的用类如 apply(), accept(obj) 等形式。不仅造成代码可读性差,且可测试性也变坏了。
为什么说的是快乐的使用 java 8 的 lambda 呢?我窃以为第一个念头声明 lambda 表达式为实例/类变量(像本文第一段代码那样),而不是方法的,一定会觉得如此使用方式很快乐的。所谓独乐乐,不如众乐乐;独乐乐,众不乐定然是更大的快乐; 更极致一些,不管什么时候必须是:我快乐,所以你也快乐。
一方面也在于 java 还没有进化到 javascript 或 scala 那样的水平,javascript 的函数类型变量,不一定要用 apply 或 call, 直接括号就能实现方法调用。scala 的函数类型用括号调用也会自动匹配到 apply 或 update 等方法上去。
看下面的样本代码
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class account {
public bifunction<string, string, string> fullname = (firstname, lastname) -> {
//some logic, i.e. logics of fullname in different countries
return firstname + " " + lastname;
};
public string getname() {
string firstname = "speaker" ;
string lastname = "wolf" ;
return fullname.apply(firstname, lastname);
}
}
|
上面的 fullname lambda 表达式看起来就有点别扭,完全可以写成一个普通方法
1
2
3
|
public string makefullname(string firstname, string lastname) {
//return something with logics
}
|
那么调用起来只需要简单的
1
|
makefullname(firstname, lastname)
|
那么此例中把简单方法写成一个 lambda 表达式来调用有什么不友好之处呢?
- 不利于理解,lambda 表达式的类型充斥着 consumer, function, bifunction 等太宽泛的声明
- 参数类型与形参分离在表达式等号两边,不利于一一对应(右方重复一遍参数类型更不可取),真正的返回值也不明了
- 调用时更得多余的 get(), accept(obj), apply(obj1, obj2) 那样的方法
- 既然有逻辑,就应该有测试,lambda 表达式虽是一个变量也不例如,测试时也不得用 apply 那样的调用
- lambda 表达式为变量的形式,可能会随每一个对象实例有一单独的拷贝。当然声明为静态可以避免。
- 重构时更需大动干戈,比如前面的例子还要考虑 middlename 的情况,表达式要更动为
1
2
3
|
public trifunction<string, string, string, string> fullname = (firstname, middlename, lastname) -> {
//.......
}
|
jdk 中还没有 trifunction, 还得自己创造,不同数量的参数都得更新 lambda 表达式的类型。如果是一个普通方法重构起来就方便多了,跟多一个人多一副碗筷一样。
解释上面第 #5 条,对于方法,实现代码在 jvm 中只有一份,而 lambda 实例变量如果不捕获外部变量的话,与方法是一样的,例如前面的 account 为例
1
2
3
|
account account1 = new account();
account account2 = new account();
system.out.println(account1.fullname == account2.fullname); //true
|
但是 lambda 表达式需捕获外部变量时,例如
1
2
3
4
5
6
7
8
9
|
private string suffix = "sir" ;
public bifunction<string, string, string> fullname = (firstname, lastname) -> {
return firstname + " " + lastname + " " + suffix;
};
.......
account account1 = new account();
account account2 = new account();
account1.fullname == account2.fullname; //就是 false 了, 而如果 suffix 是一个静态的变量时这个等式又是 true 了
|
那么新建的两个 account 对象的 fullname 属性就不是同一个了。因为 lambda 需要捕获外部一个不确定的值,所以它也随宿主实例也变。
难道不应该用 lambda 表达式变量,那倒不是,如果一个方法接受的是一个函数,如
1
2
3
|
public string getname(bifunction<string, string, string> builder) {
return builder.apply(firstname, lastname);
}
|
那么是可以声明一个 lambda 表达式变量,来传入。不过这种情况下用方法引用还是更方便些,方法的测试总是比 lambda 表达式的测试容易。
1
|
string name = getname( this ::makefullname);
|
个人习惯,一般需要 lambda 表达式变量时基本是声明为局部变量,或是调用接受函数参数的方法时以内联的方法书写,像
1
2
3
4
|
string name = getname((firstname, lastname) -> {
//logics
return ......
});
|
对于使用方法引用方式的重构也不难,getname() 的参数类型变为 trifunction, makefullname() 方法再加一个参数就行, 调用形式仍然不变,还是
1
|
string name = getname( this ::makefullname);
|
如果引用的方法是别人写的也不用慌,无须总去创建一样的方法签名来强型上方法引用,也可以和改 lambda 实现代码一样的方式比改动,如下
1
2
3
|
string name = getname((firstname, lastname) ->
makefullname(firstname, lastname) + " " + suffix
)
|
本人希望的是,对函数的 apply(), accept(obj) 这样的显式调用应该是框架设计实现的职责,对框架使用者应该透明,或者说是隐藏背后的细节,只管传入需要的函数类型或方法引用。如果函数实现需要共享的话,写成方法更优于一个 lambda 表达式,方法容易单独测试。特别是用 mockito 捕获到了一个传入某个方法的 lambda 表达式实例时,不那么好验证它的内部实现。
小结一下:
- 函数式思维最关键应该是 data in, data out, 编程语言 lambda 特性可以促使我们达成这一目的; 但不是代码中有了 lambda 表达就是函数式风格。
- 其次代码的首先是人阅读,其次才是机器,所以它应该表达直截,明了,很强的可读性与可测试性。
- 具体讲如何快乐使用 java 8 的 lambda 呢,仅代表本人想法,可以用内联式,或方法引用,或局部的 lambda 表达式变量,最后才是实例/类的 lambda 表达式变量。
补充一个例子,在方法体中重复声明完全相同的不捕获任何外部变量的 lambda 表达式都是新的实例
1
2
3
|
consumer<string> f1 = a -> system.out.println(a);
consumer<string> f2 = a -> system.out.println(a);
system.out.println(f1 == f2); //false
|
以上测试在 java 8 平台上进行的。
lambda表达式优劣
lambda表达式有优点也有缺点,优点在于大大简化代码行数,使代码在一定程度上变的简洁干净,但是同样的,这可能也会是一个缺点,由于省略了太多东西,代码可读性有可能在一定程度上会降低,这个完全取决于你使用lambda表达式的位置所设计的api是否被你的代码的其他阅读者所熟悉。另外的优点,也是lambda表达式比较显眼的优点就是对外部定义的局部变量的使用更加灵活,想象一种极端情况,你的代码中有地方需要接口回调套接口回调,有可能套了好几层,虽然这种情况出现的概率比较低,但是一旦出现这种代码,lambda表达式的这个优点就到了大显身手的时机。虽然我说了,lambda表达式能用的地方非常有限,但是不得不否认,接口中只有一个抽象方法这种情况在接口回调中发生的概率绝对比接口中有多个抽象方法的概率高的多,所以,虽然使用情况很单一,但是能用到的次数却足够的多,如果你决定用lambda表达式替换你项目中接口回调的传统写法,你会发现,这样的情况非常多。
总而言之,接口回调和lambda表达式这两种写法各有优劣,java 8在出现lambda表达式以后不代表原先的写法不能再用了,所以如何选择适合项目的写法,全看各位开发者如何自己选择,现在多了一种写法可选,总归是一件好事。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://yanbin.blog/happy-with-java8-lambda/