Lambda表达式 与 方法引用

Lambda表达式

Lambda 表达式是使用最小可能语法编写的函数定义:

  1. Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。

  2. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。

我们在 Strategize.java 中看到了一个 Lambda 表达式,但还有其他语法变体:

// functional/LambdaExpressions.java

interface Description {
  String brief();
}

interface Body {
  String detailed(String head);
}

interface Multi {
  String twoArg(String head, Double d);
}

public class LambdaExpressions {

  static Body bod = h -> h + " No Parens!"; // [1]

  static Body bod2 = (h) -> h + " More details"; // [2]

  static Description desc = () -> "Short info"; // [3]

  static Multi mult = (h, n) -> h + n; // [4]

  static Description moreLines = () -> { // [5]
    System.out.println("moreLines()");
    return "from moreLines()";
  };

  public static void main(String[] args) {
    System.out.println(bod.detailed("Oh!"));
    System.out.println(bod2.detailed("Hi!"));
    System.out.println(desc.brief());
    System.out.println(mult.twoArg("Pi! ", 3.14159));
    System.out.println(moreLines.brief());
  }
}

输出结果:

Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()

我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性)。但是,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。

任何 Lambda 表达式的基本语法是:

  1. 参数。

  2. 接着 ->,可视为“产出”。

  3. -> 之后的内容都是方法体。

  • [1] 当只用一个参数,可以不需要括号 ()。 然而,这是一个特例。

  • [2] 正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。

  • [3] 如果没有参数,则必须使用括号 () 表示空参数列表。

  • [4] 对于多个参数,将参数列表放在括号 () 中。

到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式简化相应语法的另一种方式。

[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return

Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。

递归

递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。

这两个示例都需要一个接受 int 型参数并生成 int 的接口:

// functional/IntCall.java

interface IntCall {
  int call(int arg);
}

整数 n 的阶乘将所有小于或等于 n 的正整数相乘。 阶乘函数是一个常见的递归示例:

// functional/RecursiveFactorial.java

public class RecursiveFactorial {
  static IntCall fact;
  public static void main(String[] args) {
    fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
    for(int i = 0; i <= 10; i++)
      System.out.println(fact.call(i));
  }
}

输出结果:

1
1
2
6
24
120
720
5040
40320
362880
3628800

这里,fact 是一个静态变量。 注意使用三元 if-else。 递归函数将一直调用自己,直到 i == 0。所有递归函数都有“停止条件”,否则将无限递归并产生异常。

我们可以将 Fibonacci 序列用递归的 Lambda 表达式来实现,这次使用实例变量:

// functional/RecursiveFibonacci.java

public class RecursiveFibonacci {
  IntCall fib;

  RecursiveFibonacci() {
    fib = n -> n == 0 ? 0 :
               n == 1 ? 1 :
               fib.call(n - 1) + fib.call(n - 2);
  }
  
  int fibonacci(int n) { return fib.call(n); }

  public static void main(String[] args) {
    RecursiveFibonacci rf = new RecursiveFibonacci();
    for(int i = 0; i <= 10; i++)
      System.out.println(rf.fibonacci(i));
  }
}

输出结果:

0
1
1
2
3
5
8
13
21
34
55

Fibonacci 序列中的最后两个元素求和来产生下一个元素。

方法引用

Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 :: [^4],然后跟方法名称。

// functional/MethodReferences.java

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!");
  }
}

输出结果:

call()
Hello, Bob
valuable information
Help!

[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。

[2] show() 的签名(参数类型和返回类型)符合 Callablecall() 的签名。

[3] hello() 也符合 call() 的签名。

[4] help() 也符合,它是静态内部类中的非静态方法。

[5] assist() 是静态内部类中的静态方法。

[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show() 方法,而是 call() 方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callablecall() 方法的签名。

[7] 我们现在可以通过调用 call() 来调用 show(),因为 Java 将 call() 映射到 show()

[8] 这是一个静态方法引用。

[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用

[10] 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。

上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。

Runnable接口

Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable

// functional/RunnableMethodReference.java

// 方法引用与 Runnable 接口的结合使用

class Go {
  static void go() {
    System.out.println("Go::go()");
  }
}

public class RunnableMethodReference {
  public static void main(String[] args) {

    new Thread(new Runnable() {
      public void run() {
        System.out.println("Anonymous");
      }
    }).start();

    new Thread(
      () -> System.out.println("lambda")
    ).start();

    new Thread(Go::go).start();
  }
}

输出结果:

Anonymous
lambda
Go::go()

Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run() 的方法 start()注意,只有匿名内部类才需要具有名为 run() 的方法。

未绑定的方法引用

未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象:

// functional/UnboundMethodReference.java

// 没有方法引用的对象

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()); // 同等效果
  }
}

输出结果:

X::f()
X::f()

截止目前,我们看到了与对应接口签名相同的方法引用。 在 [1],我们尝试把 Xf() 方法引用赋值给 MakeString。结果即使 make()f() 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 this。 你不能在没有 X 对象的前提下调用 f()。 因此,X :: f 表示未绑定的方法引用,因为它尚未“绑定”到对象。

要解决这个问题,我们需要一个 X 对象,所以我们的接口实际上需要一个额外的参数,如上例中的 TransformX。 如果将 X :: f 赋值给 TransformX,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法

[2] 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的transform()方法,将一个X类的对象传递给它,最后使得 x.f() 以某种方式被调用。Java知道它必须拿到第一个参数,该参数实际就是this,然后调用方法作用在它之上。

如果你的方法有更多个参数,就以第一个参数接受this的模式来处理。

// functional/MultiUnbound.java

// 未绑定的方法与多参数的结合运用

class This {
  void two(int i, double d) {}
  void three(int i, double d, String s) {}
  void four(int i, double d, String s, char c) {}
}

interface TwoArgs {
  void call2(This athis, int i, double d);
}

interface ThreeArgs {
  void call3(This athis, int i, double d, String s);
}

interface FourArgs {
  void call4(
    This athis, int i, double d, String s, char c);
}

public class MultiUnbound {
  public static void main(String[] args) {
    TwoArgs twoargs = This::two;
    ThreeArgs threeargs = This::three;
    FourArgs fourargs = This::four;
    This athis = new This();
    twoargs.call2(athis, 11, 3.14);
    threeargs.call3(athis, 11, 3.14, "Three");
    fourargs.call4(athis, 11, 3.14, "Four", 'Z');
  }
}

需要指出的是,我将类命名为 This,并将函数式方法的第一个参数命名为 athis,但你在生产级代码中应该使用其他名字,以防止混淆。

构造函数引用

你还可以捕获构造函数的引用,然后通过引用调用该构造函数。

// functional/CtorReference.java

class Dog {
  String name;
  int age = -1; // For "unknown"
  Dog() { name = "stray"; }
  Dog(String nm) { name = nm; }
  Dog(String nm, int yrs) { name = nm; age = yrs; }
}

interface MakeNoArgs {
  Dog make();
}

interface Make1Arg {
  Dog make(String nm);
}

interface Make2Args {
  Dog make(String nm, int age);
}

public class CtorReference {
  public static void main(String[] args) {
    MakeNoArgs mna = Dog::new; // [1]
    Make1Arg m1a = Dog::new;   // [2]
    Make2Args m2a = Dog::new;  // [3]

    Dog dn = mna.make();
    Dog d1 = m1a.make("Comet");
    Dog d2 = m2a.make("Ralph", 4);
  }
}

Dog 有三个构造函数,函数式接口内的 make() 方法反映了构造函数参数列表( make() 方法名称可以不同)。

注意我们如何对 [1][2][3] 中的每一个使用 Dog :: new。 这三个构造函数只有一个相同名称::: new,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。

编译器知道调用函数式方法(本例中为 make())就相当于调用构造函数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值