3.9 数学中的类似思想

如果你上学的时候对数学很擅长,那这一节就从另一个角度来谈谈Lambda表达式和函数传递的思想。你可以跳过它,书中没有任何其他内容依赖这一节,不过从另一个角度看看也挺好的。

3.9.1 积分

假设你有一个(数学,不是Java)函数f,比如说定义是

f(x)=x+10

那么,(工科学校里)经常问的一个问题就是,求画在纸上之后函数下方的面积(把x轴作为基准)。比如对于图3-8所示的面积你会写

\int^7_3 (x){\rm d}x\int^7_3 (x+10){\rm d}x

3.9 数学中的类似思想 - 图6

图 3-8 函数f(x)=x+10x从3到7)下方的面积

在这个例子里,函数f是一条直线,因此你很容易通过梯形方法(画几个三角形和矩形)来算出面积:

1/2 × ((3 + 10) + (7 + 10)) × (7 – 3) = 60

那么这在Java里面如何表达呢?你的第一个问题是把积分号或{\rm d}y/{\rm d}x之类的换成熟悉的编程语言符号。

确实,根据第一条原则你需要一个方法,比如说叫integrate,它接受三个参数:一个是f,还有上下限(这里是3.0和7.0)。于是写在Java里就是下面这个样子,函数f是作为参数被传递进去的:

  1. integrate(f, 3, 7)

请注意,你不能简单地写:

  1. integrate(x + 10, 3, 7)

原因有两个。第一,x的作用域不清楚;第二,这将把x + 10的值而不是函数f传给积分。

事实上,数学上{\rm d}x的秘密作用就是说“以x为自变量、结果是x+10的那个函数。”

3.9.2 与Java 8的Lambda联系起来

前面说过,Java 8的表示法(double x) -> x + 10(一个Lambda表达式)恰恰就是为此设计的,因此你可以写:

  1. integrate((double x) -> x + 10, 3, 7)

或者

  1. integrate((double x) -> f(x), 3, 7)

或者,用前面说的方法引用,只要写:

  1. integrate(C::f, 3, 7)

这里C是包含静态方法f的一个类。理念就是把f背后的代码传给integrate方法。

现在你可能在想如何写integrate本身了。我们还假设f是一个线性函数(直线)。你可能会写成类似数学的形式:

  1. public double integrate((double -> double) f, double a, double b) { ←---- 错误的Java代码!(函数的写法不能像数学里那样。)
  2. return (f(a) + f(b)) * (b - a) / 2.0
  3. }

不过,由于Lambda表达式只能用于接受函数式接口的地方(这里就是DoubleFunction4),所以你必须得写成这个样子:

4使用DoubleFunctionFunction更高效,因为它避免了结果的装箱操作。

  1. public double integrate(DoubleFunction<Double> f, double a, double b) {
  2. return (f.apply(a) + f.apply(b)) * (b - a) / 2.0;
  3. }

或者用DoubleUnaryOperator,这样也可以避免对结果进行装箱:

  1. public double integrate(DoubleUnaryOperator f, double a, double b) {
  2. return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
  3. }

顺便提一句,有点可惜的是你必须写f.apply(a),而不是像数学里面写f(a),但Java无法摆脱“一切都是对象”的思想——它不能让函数完全独立!