samsai00 2020-05-06
首先,我们来看下策略模式,这是网上找到的一个关于策略模式的解释:策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。策略模式通常把一个系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是:“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
这里不详细介绍策略模式,只是简单的用策略模式来引入函数式编程的例子。
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?结论是:只要能将代码传递给方法,我们就可以控制它的行为。
interface Strategy { String approach(String msg); } class DefaultStrategy implements Strategy { public String approach(String msg) { return msg.toLowerCase() + "?"; } }public class Strategize { Strategy strategy = new DefaultStrategy(); String msg; Strategize(String msg) { this.msg = msg; } void communicate() { System.out.println(strategy.approach(msg)); } void changeStrategy(Strategy strategy) { this.strategy = strategy; } }
Strategize有一个默认的策略,当我们调用communicate时,就是使用默认的策略来处理msg。当然我们也可以更换Strategy从而让communicate表现不同的行为。在java 8之前我们除了像DefaultStrategy那样定义一个类之外,还可以通过定义一个匿名内部类来实现。
public static void main(String[] args) { Strategize s = new Strategize("Hello there"); Strategy s = new Strategy() { public String approach(String msg) { return msg.toUpperCase() + "!"; } }; s.communicate(); }
在java 8中引入函数式编程后,上面的例子就可以用Lambda 表达式进行改写
public static void main(String[] args) { Strategize s = new Strategize("Hello there"); Strategy s = msg -> msg.toUpperCase() + "!"; s.communicate(); }
代码少多了,实现的效果和上面一样。
如果你想使用的策略已经存在其他类或对象中,还可以直接使用方法引用的方式来实现。如下面的例子。
public static void main(String[] args) { Strategize s = new Strategize("Hello there"); Strategy s = String::toLowerCase; s.communicate(); }
Lambda 表达式和方法引用产生函数,而不是类。 虽然实际上在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,java 8在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。意识到这一点很重要,我们使用Lambda 表达式和方法引用进行函数式编程时,不要和类联系起来,仅仅当它们是函数就可以了,这样更容易理解代码。
如果一个接口只有一个方法,那么就可以称为函数时接口,在java 8之前很容易通过匿名内部类来实现。在java 8中将匿名内部类转成Lambda 表达式也是很简单的,只需要多练习几次就很容易掌握。
关于方法引用可能就比较难以理解。简单来说就是返回值和方法参数一致,就可以用方法引用,具体细节参考下面的例子。
import java.util.*; interface Callable { // [1] void call(String s); } class Describe { void show(String msg) { // [2] System.out.println(msg); } } public class MethodReferences { static void hello(String name) { // [3] System.out.println("Hello, " + name); } static class Description { String about; Description(String desc) { about = desc; } void help(String msg) { // [4] System.out.println(about + " " + msg); } } static class Helper { static void assist(String msg) { // [5] System.out.println(msg); } } public static void main(String[] args) { Describe d = new Describe(); Callable c = d::show; // [6] c.call("call()"); // [7] c = MethodReferences::hello; // [8] c.call("Bob"); c = new Description("valuable")::help; // [9] c.call("information"); c = Helper::assist; // [10] c.call("Help!"); } }
[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
[2] show()
的签名(参数类型和返回类型)符合 Callable 的 call()
的签名。
[3] hello()
也符合 call()
的签名。
[4] help()
也符合,它是静态内部类中的非静态方法。
[5] assist()
是静态内部类中的静态方法。
[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show()
方法,而是 call()
方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callable 的 call()
方法的签名。
[7] 我们现在可以通过调用 call()
来调用 show()
,因为 Java 将 call()
映射到 show()
。
[8] 这是一个静态方法引用。
[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用。
[10] 最后,获取静态内部类的方法引用的操作与 [8] 中外部类方式一样。
上面提到了绑定方法引用,那么对应的还有未绑定方法引用。下面是一个例子
class X { String f() { return "X::f()"; } } interface MakeString { String make(); } interface TransformX { String transform(X x); } public class UnboundMethodReference { public static void main(String[] args) { // MakeString ms = X::f; // [1] TransformX sp = X::f; X x = new X(); System.out.println(sp.transform(x)); // [2] System.out.println(x.f()); // 同等效果 } }
我们已经知道了与接口方法同名的方法引用。 在 [1],我们尝试把 X
的 f()
方法引用赋值给 MakeString。结果:即使 make()
与 f()
具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:this
。 你不能在没有 X
对象的前提下调用 f()
。 因此,X :: f
表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 X
对象,所以我们的接口实际上需要一个额外的参数的接口,如上例中的 TransformX。 如果将 X :: f
赋值给 TransformX,这在 Java 中是允许的。这次我们需要调整下心里预期——使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。
[2] 的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 transform()
,将其传递给 X
,并以某种方式导致对 x.f()
的调用。 Java 知道它必须采用第一个参数,这实际上就是 this
,并在其上调用方法。
本文参考 on java 8