Java 8 效率精进指南(2)初探行为参数化

168 阅读5分钟

在Java 8之前,专家们可能会告诉你,必须利用线程才能使用多个内核。问题是,线程用起来很难,也容易出现错误。从Java的演变路径来看,它一直致力于让并发编程更容易、出错更少。

22.jpg

什么是行为参数化

所谓行为参数化,是指在函数签名里,参数不仅仅是类对象,同样可以将行为作为参数,在函数调用之间传输,也就是说,传递的是代码逻辑,而非具体对象。作为面向对象语言,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 语言的前提下),可能会给出上面的实现。如何评价呢?

  1. 它的确解决了需求里提出的问题
  2. 它不具备任何可扩展性

这意味着,在未来的需求演化过程中,一旦要对判断条件进行调整,例如改为选出那些学历在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))

对这名中等水平的程序员给出的方案,我的评价是:

  1. 它的确解决了需求里提出的问题
  2. 它具备一定可扩展性
  3. 它仍然留有大量的冗余代码

就拿资深员工匹配器这个类来说,

// 资深员工匹配器
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
    }
})

image.png

这也就是函数式编程(行为参数化)所存在的意义,它摒弃了多余的样板代码,聚焦于核心业务逻辑的传达。

行为参数化的实现:过滤员工

使用 Lambda 表达式进行行为参数化以后,可以将代码从5行缩减到1行。

fun findSeniorEmployees(allEmployees: List<Employee>, ageLimit: Int) = findMatchedEmployees(allEmployees, (employee: Employee) -> employee.age > ageLimit)

知识点:SAM 接口

RunnableComparableOnClickListener 这种,只含有一个抽象函数的接口,称为 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