内部类与函数式编程

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

相关推荐