实验5:定义任意的复合函数。
理解:一个策略选择融为另外一个策略选择的一部分。桥接和装饰模式。
数学中的函数y = f(x),其中 f 表示从x到y的映射。函数的映射方式无穷无尽,如
f(x) = x+1
f(x)= x *x
f(x) = (x+1)* (x+1)
首先,用函数接口F定义一种简单的映射,即把映射f抽象为Java方法,对参数x进行某种操作(简化起见,f设为一元函数,参数为int)并返回一个int。函数接口F的实现类定义进行何种具体的操作,F的实现类无穷多,例程4-10中编写的一个独立的子类如Y1描述了f(x)= x *x。提供Y1便于不熟悉lambda表达式读者,用Y1这种传统的代码增加依靠感。
函数接口F是抽象策略,而Demo与F构成策略模式,Demo的求值函数eval(F, int) 将函数接口F作为形参。
package chap4.bridgeP.bridgeP2;
@FunctionalInterface
public interface F {//IntUnaryOperator
public int f(int x) ;
}//Y1: return x *x;
import static yqj2065.util.Print.*;
public class Test{ //FContext
//参数化F
public static int eval(F y, int x) { //函数y作用于x一次
return y.f(x);
}
public static void test() {
F y = x -> x + 1;
int r = eval(y, 2); pln(r);// r =y.f(2);
int r2 = eval(x -> x + 1, 2); pln(r2); //F对象直接作为实参
int r3 = eval(x -> x*x, y.f(2)); pln(r3);
int r4 = eval(x -> x*x, eval(x -> x + 1, 2)); pln(r4);
}
}
复合函数(composite function)的求值过程,同样采取上面语句的执行方式,将一个函数的计算结果用于另外一个函数的计算中。即先将2参与f(x) = x+1计算得到结果3,再将结果3参与f(x) = x*x计算。复合函数的求值过程并非先把(x+1)* (x+1)转化成x*x+2x+1再对参数求值。
复合函数则是经过多次映射的函数,例如函数z = g(f(x))。如果f(x) = x+1,g(x) = x*x,则g(f(x)) = f(x)* f(x) =( x+1)* ( x+1)。定义复合函数,需要将多个函数先复合成为一个最终的F对象,再用于计算某个参数x的值。
定义复合函数h(g(f(x))),最简单的方式是按照从内到外的顺序来进行,首先定义函数f(x)是什么,然后再使用f(x)去定义g(f(x))。
public static void compsite(){
F y = x -> x + 1; //函数 y = x+1
F comp = x -> y.f(x) * y.f(x);//z=y*y
int r =comp.f(2); pln(r);
}
这段代码中程序员手工将多个策略选择串接起来(将两个函数进行复合)。可以在F接口中定义一个默认方法用于函数复合。
public default F com(F y) {
return (int x) -> this.f(y.f(x));
}
于是,
F y = x -> x + 1; //函数 y = x+1
F z = x -> x * x;
F u = x -> x * x + 2 * x;
F composeF = u.com(z.com(y));
pln(composeF.f(2));
在F的基础上,可以定义更高级的函数接口G。
F定义了函数映射int→int,而G定义了函数的复合映射F→F。
G的抽象方法和F的默认方法com(F )看起来是一样的,实质上不同。F的默认方法,表示一个F的对象将用方法体与参数对象复合;而G的抽象方法,用于创建数学表示g(f(x))中的g对象,对于z=g(y),不管y具体是什么,z最终是一个x的函数即F的对象,即它对参数y进行复合后得到的是一个F的对象。
package bridge.compositeFunction;
public interface G {
public F com (F y);
}
class Z1 implements G{
@Override public F com (F y){
return (x) -> y.f(x) * y.f(x);
}
}
1. 通过G进行函数复合
使用函数接口G定义复合函数,其流程是:可以先定义一个G,再定义一个F,然后调用G的方法com(F)将两次策略选择串接起来。这就是所谓从外到内的复合。如
G z = new Z1();
F y = x -> x + 1;//现在知道y是什么了
F comp = z. com (y);
通常,G的实现类不会被编写成独立的类,因此上面的定义复合函数流程,有如下的变体:
(1)G z声明语句的变化。通常使用lambda表达式创建G的实现类对象。
G z = y -> {
return (x) -> y.f(x) * y.f(x);//z = y * y
};//或者
G z = y -> x -> y.f(x) * y.f(x);
外层的λ表达式,定义了对于任意的函数y的复合方式y*y,其返回值为内层的λ表达式,即(x) -> y.f(x) * y.f(x)。
package higherOrderFunction;
import static util.Print.*;
public class MathDemo {
public static void main(String[] a) {
G z = y -> x -> y.f(x) * y.f(x);
F y = x -> x + 1;
F f3 = z.com(y);
pln(f3.f(2));
}
}
可以定义一系列g1(y)、g2(y)、g3(y),按照希望的顺序对它们进行复合。最后都需要一个F对象如y作为落脚点。可以在接口F中预定义一个命名常量如恒等函数F.identity作为固定的F,
public interface F{
public int f(int x);
public default F com(F y) {
return (int x) -> this.f(y.f(x));
}
F identity = x->x;
}
//Test
public static void testGn(){
G h = y -> x -> y.f(x) * y.f(x) + 2 * y.f(x);//
G g = y -> x -> y.f(x) * y.f(x);
G z = y -> x -> y.f(x)+1;
F comp = h.com(g.com(z.com(F.identity)));
comp = z.com(g.com(h.com(F.identity)));
pln(comp.f(2));
}
理解其计算过程,非常重要的一点是,λ表达式只是一个引用,正如创建一个String对象"hello"并不会执行"hello"..startsWith("h")一样,λ表达式的函数体并不会执行。
为了观察执行过程,建议在代码中增加打印语句:
G z = y -> {
pln("z:" + y);
return (x) -> {
pln("in z: x=" + x);
int i = y.f(x);
pln("in z: i=" + i);
return i * i;
};
};
另外,可以使用通用函数接口替代F、G。
//import java.util.function.*;
public static void main(String[] a) {
// G z = y -> x -> y.f(x) * y.f(x);
Function g3 = y -> x -> y.applyAsInt(x) * y.applyAsInt(x);
IntUnaryOperator op =x -> x + 1;
// F y = x -> x + 1;
pln(g3.apply(op).applyAsInt(2));
}
或者:
public static void main(String[] a) {
Function f = x -> x + 1;
Function g = x -> x * x;
pln(g.compose(f).apply(2));
}