1策略模式之代码传递
软件开发中经常遇到这样的情况,某一功能在不同的条件或执行环境需要采用不同的算法或策略进行执行,而这些策略针对的对象却具有相通性,例如查找、排序等操作。针对这样的场景,程序员往往将不同的策略执行代码封装成不同的函数调用,结合if-else的方式进行策略的调用。的确这是一种很符合软件开发思维的处理方式,但是同时也带来了一些问题,因为这样的处理方式将策略进行硬编码化了,使得策略与程序执行的过程以及执行对象耦合度过高,代码的灵活性大大降低, 同时使得维护也较为困难。例如如果我们需要置定新的策略,往往就需要修改代码来实现了。策略模式的思想就是将执行对象与算法解耦,对象无法感知也无需感知策略的具体执行方式,从而为代码带来的更高的灵活性。其实Java中的基本的线程操作便是一种策略模式的体现,线程对象不可能提前得知线程执行函数的逻辑,所以定义了线程执行的策略接口Runnable,程序员只需按照Runnable接口实现一个线程策略实例,便可以使用Thread类来开启线程的执行了。线程执行代码如下:
如果从另一个角度看待策略模式,实际上策略模式一种行为参数化代码传递的技术,将具体的执行代码(行为)传递给对象制定的执行器进行执行,上面的例子其实可以看做将线程的执行代码传递给了线程对象的执行器去执行的过程。在Java 8中,提供了一种更直接的代码传递方式:Lambda表达式,通过使用Lambda表达式可以将上述代码精简到以下两行:
2
2什么是Lambda表达式
为了为大家带来最直观的Lambda表达式的理解,我们依旧采用前文学生筛选的例子。现在我们需要通过数学成绩进行排序,在以往开发中,我们可以通过实现Comparator接口的方式来实现该策略,如下:
使用Lambda之后可以这样表示比较策略:
不得不承认Lambda表达式使代码看起来更加清晰了!要是现在你觉得Lambda表达式看起来还是一头雾水,没有问题,随着分析的深入,你很快就会熟悉并喜欢上它的。Lambda表达式大体上可以分为三个部分,如下图:
- 参数列表:这里的参数列实际上就是Comparator中的compare方法的参数列表,这里是两个参与比较的Student对象。
- 箭头: 箭头是lambda表达式的标志,用于分割参数列表与表达式主体。
- Lambda主体 :便是执行代码主体,这里是学生比较的策略实现代码。Lambda主体就相当于函数体,其可以使用参数列表中的变量。
Java语言设计者之所以选择这样的语法表达方式,是借鉴了C#和Scala语言中Lambda表达式的成功实践,并且这种方式在开发者中广受欢迎。Lambda的基本语法可以表示成如下模式:
(parameter, ...) -> ( expression )
此外Lambda主体还可以包含多行代码,但需要采用花括号包含 :
(parameter, ... ) -> { statements; ....; }
3
3函数式接口
相信通过前面的分析,读者这里应该对于Lambda表达式有了一个初步的了解,这里我们在来说一下Lambda表达式的返回值问题。Lambda表达式的值就是Lambda主体代码的返回值,例如上面数学成绩比较的表达式就返回一个int变量;线程使用的lambda表达式以void返回。你可能会好奇,java如何对lambda表达式进行类型检查?这里我们放在后节讲解,这里先来看一下什么是函数式接口。现在你可能在问,我们在什么地方可以使用Lambda表达式?在java 8规定在声明了函数式接口的类上可以使用Lambda表达式,例如Runnable接口,Callable接口,Comparator接口。那么到底什么是函数式接口?函数式接口就是只定义了一个抽象函数的接口。
Lambda表达式允许你直接以内联的方式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例,也就是说这个Lambda表达式就是接口具体实现的一个实例。这里我想同学们应该能理解为何函数式接口只允许声明一个抽象函数了吧,假设可以声明多个,那么Lambda表达式在解析时就会存在多态解析问题,毕竟Lambda是匿名函数,这无疑会给java的语言特性以及使用者带来额外的负担。在JDK8中提供了@FunctionInterface注解来专门表示函数式接口,会对接口的声明进行语法检查,当然 注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错,例如“Mutiple non-overriding abstract methods found ininterface xxx”,说明存在多个抽象方法,这是不允许的。
4关于函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符。写过JNI的同学一定会知道Java函数签名。函数签名表示了一个函数的返回值、参数列表、以及函数性质,当然具体使用者将变量名命名成什么或者函数名叫什么都不是函数签名考虑的,读者可以将签名理解为函数类型的一种描述,实质上Java的函数类型检查就是基于签名来做的,只要签名相同,那么就可以认为具有相同的函数声明定义。Lambda表达式也一样,对于表达式的具体实现,一定要与接口的函数签名相匹配,否则会语法报错,这也不难理解。例如Comparator接口的抽象函数表示了一个接受两个对象并返回一个int变量的函数签名。我们将Comparator的函数描述符使用(Object,Object) -> int的形式来表述,在本系列文章中也会沿用这样的描述方式。
Lambda表达式是开启Java 8 新语言特性的起点,之后的语言特性很多都是以Lambda为基础来展示的。这里我们一定要理解Lambda表达式正是函数式接口的具体的实现这一重要性的意义,同时这也是Java 8 对于Lambda表达式语法正确性检查的函数描述符依据。之后我们将会讨论函数描述符、Lambda的类型检查等主题;它们都是建立本文的基础之上来讨论的。