在Java 8之前,专家们可能会告诉你,必须利用线程才能使用多个内核。问题是,线程用起来很难,也容易出现错误。从Java的演变路径来看,它一直致力于让并发编程更容易、出错更少。
什么是行为参数化
所谓行为参数化,是指在函数签名里,参数不仅仅是类对象,同样可以将行为作为参数,在函数调用之间传输,也就是说,传递的是代码逻辑,而非具体对象。作为面向对象语言,Java 原本是不支持传递函数的,而 Java 8 则为它增添了这种能力。
举个例子:公司员工名单过滤
这样讲可能有些抽象,我们用一个例子来说明:
Q:你现在手上有一张公司员工名单,需要对名单进行过滤,找出那些35岁以上的员工们。
一个简单的实现是,直接 for 循环,在循环内部进行判断:
// 找出资深员工
fun findSeniorEmployees(allEmployees: List<Employee>): List<Employee> {
val seniorEmployees = mutableListOf()
for (employee in allEmployees) {
if (employee.age > 35) {
seniorEmployees.add(employee)
}
}
return seniorEmployees.toList()
}
一个初级程序员(在熟悉 Kotlin 语言的前提下),可能会给出上面的实现。如何评价呢?
- 它的确解决了需求里提出的问题
- 它不具备任何可扩展性
这意味着,在未来的需求演化过程中,一旦要对判断条件进行调整,例如改为选出那些学历在985、211的员工,你将不得不再用同样的模式,实现一个名为 findCollegeEmployees,而它与上一段代码相似度达到九成:
// 找出9895、211员工
fun findCollegeEmployees(allEmployees: List<Employee>): List<Employee> {
val collegeEmployees = mutableListOf()
for (employee in allEmployees) {
if (employee.college is 985 or 211 ) { // ===> 伪代码
collegeEmployees.add(employee)
}
}
return collegeEmployees.toList()
}
毫无疑问,随着判断条件增多,此类难以复用的代码将充斥项目,带来巨大的理解成本和维护支出,代码必然劣化。
使用接口,将判断条件进行抽象
在第一次做一件事时尽管去做,在第二次做同样事时留个心眼,在第三次做这件事就要考虑重构了。
前文是一名 Java 入门选手给出的方案,一个中等级别的程序员,在接触 Java 8 之前,通常会借助 接口 将判断条件进行抽象。这里可以抽象出 EmployeeMatcher 接口:
// 员工匹配器接口,判断员工是否满足某种条件
interface EmployeeMatcher {
fun match(employee: Employee): Boolean
}
随后针对年龄>35岁、是985、211员工,提供接口的两种实现:
// 年龄>35岁的匹配器
class SeniorMatcher(val ageLimit: Int) : EmployeeMatcher {
override fun match(employee: Employee): Boolean {
return employee.age > ageLimit // ===> 传入35
}
}
// 985、211匹配器
class CollegeMatcher : EmployeeMatcher {
override fun match(employee: Employee): Boolean {
return employee.college is 985 or 211 // ===> 伪代码
}
}
接下来就可以使用这两个匹配器,对员工原始列表进行过滤:
// 找出符合某种条件的员工
fun findMatchedEmployees(allEmployees: List<Employee>, matcher: EmployeeMatcher): List<Employee> {
val seniorEmployees = mutableListOf()
for (employee in allEmployees) {
if (matcher.match(employee)) {
someEmployees.add(employee)
}
}
return someEmployees.toList()
}
// 使用方:找出资深员工
fun findSeniorEmployees(allEmployees: List<Employee>) = findMatchedEmployees(allEmployees, SeniorMatcher(35))
对这名中等水平的程序员给出的方案,我的评价是:
- 它的确解决了需求里提出的问题
- 它具备一定可扩展性
- 它仍然留有大量的冗余代码
就拿资深员工匹配器这个类来说,
// 资深员工匹配器
class SeniorMatcher(val ageLimit: Int) : EmployeeMatcher {
override fun match(employee: Employee): Boolean {
return employee.age > ageLimit // ===> 传入35
}
}
算上首尾括号这里一共是5行代码,而作为需求的实现方,我们所关注的其实只有 employee.age > ageLimit 这一个条件,其它的实现接口、参数和返回值声明、甚至是首尾括号,都是为了满足 Java 自身的格式标准所添加的样板代码,它们不服务于需求本身,是冗余的。
也可以通过匿名类实现资深员工筛选,仍然免不了写大量模板代码。
// 匿名类的实现
fun findSeniorEmployees(allEmployees: List<Employee>, ageLimit: Int) = findMatchedEmployees(allEmployees, object: EmployeeMatcher {
override fun match(employee: Employee): Boolean {
return employee.age > ageLimit // ===> 传入35
}
})
这也就是函数式编程(行为参数化)所存在的意义,它摒弃了多余的样板代码,聚焦于核心业务逻辑的传达。
行为参数化的实现:过滤员工
使用 Lambda 表达式进行行为参数化以后,可以将代码从5行缩减到1行。
fun findSeniorEmployees(allEmployees: List<Employee>, ageLimit: Int) = findMatchedEmployees(allEmployees, (employee: Employee) -> employee.age > ageLimit)
知识点:SAM 接口
像 Runnable、Comparable、OnClickListener 这种,只含有一个抽象函数的接口,称为 SAM(Single Abstract Method,单一抽象函数)接口。
行为参数化的本质
行为参数化的本质是设计模式中的策略模式(Strategy Pattern)。行为参数化将一个行为(一段代码)封装起来,并通过传递和使用所创建的行为(代码)将方法的行为参数化。行为参数化能够轻松地适应不断变化的需求,从而使方法的行为具有更强的灵活性和可重用性。
将行为作为参数,传给另一个函数进行调用,这也是一种高阶函数的实践。
Java 8 提供了 Lambda 表达式,是目前阶段行为参数化的最简实现。
行为参数化的常见应用场景
对集合进行排序
对应 Comparator 接口。
// 用年龄对职员进行排序
employees.sort(
(e1: Employee, e2: Employee) -> e1.age > e2.age
)
执行点击事件
对应 OnClickListener 接口。
button.setOnClickListener((v: View) -> textView.setText("clicked!"))
在工作线程执行代码块
对应 Runnable 接口。
val t = Thread(() -> System.out.println("Hello world"))
参考资料
- Java 8 in Action