1.行为参数化
阴影部分面积可通过计算 ∫ sinxdx 在(0, Π) 上的积分获得,体现到代码上就是:
integrate(F(x), 0, pi);
integrate(-cos(x), 0, pi);
或者,我们要算下cosine的积分,这个时候就需要传递:
integrate(sin(x), 0, pi);
当然,我们现在还不能直接传递原函数F(x),我们可以通过
方法调用:
public static double integrateSine(double a,double b){
return (-Math.cos(b)) - (-Math.cos(a));
}
public static double integrateCosine(double a,double b){
return (Math.sin(b)) - (Math.sin(a));
}
public static void main(String[] args) {
double s = integrateSine(0,Math.PI);
System.out.println(s);
}
多态:
public interface PrimitiveFunction{
double getY(double x);
}
public static class SinPrimitiveFunction implements PrimitiveFunction{
@Override
public double getY(double x) {
return -Math.cos(x);
}
}
public static class CosPrimitiveFunction implements PrimitiveFunction{
@Override
public double getY(double x) {
return Math.sin(x);
}
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
double s = integrate(new SinPrimitiveFunction(), 0, Math.PI);
System.out.println(s);
}
匿名类:
public interface PrimitiveFunction{
double getY(double x);
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
double s = integrate(new PrimitiveFunction() {
@Override
public double getY(double x) {
return -Math.cos(x);
}
}, 0, Math.PI);
System.out.println(s);
}
逐步优化我们的代码。
但是,这样还是很繁琐,我们要改动的仅仅是一行(或多行)代码。
如果,代码能够像参数一样传递(类似上面的伪代码),那就太好了。
Java8之前,是不行的,只有对象才能传递,代码怎么能直接传递呢。
Java8之后,我们要改变这种思想了,Lambda表达式让我们可以更好的实现行为参数化。
2.函数式接口
在学习Lambda之前,我们先来看一个概念——函数式接口
一言蔽之,仅有一个抽象方法的接口就是函数式接口(有多个非抽象方法是什么鬼?这里暂且不表,后面会讲到),可标注@FunctionalInterface用以标识及编译器检查
大家熟知的Runnable,Callable都是函数式接口,后面还有Java8提供的大量函数式接口。
当然我们也可以创建自己的函数式接口上文中的PrimitiveFunction就是一个函数式接口
3.Lambda表达式
理解了函数式接口,我们再来看看代码可以修改为:
public interface PrimitiveFunction{
double getY(double x);
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
double s = integrate((double x)->-Math.cos(x), 0, Math.PI);
System.out.println(s);
}
这是一次重大的突破,我们没有写冗余的内容,仅仅是把 -Math.cos(x) 这段代码通过Lambda表达式传递到了方法中(至少看上去是这样,实际上是为函数式接口生成了一个实例)
4.类型检查和推断
Lambda表达式是怎么运作的呢?这里就要用到类型检查。
通过Lambda的上下文(接口方法的参数和返回值)定义了目标类型,当我们传入的Lambda表达式符合目标类型时,才能编译通过。
例如上面代码中:
根据 integrate(PrimitiveFunction pf, double a,double b) 找到目标类型为 PrimitiveFunction。
PrimitiveFunction 的抽象方法为 double getY(double x) ,意味着接收一个double,同时返回一个double,这里可以说函数描述符为 double -> double。
Lambda表达式 (double x) -> -Math.cos(x) ,同样是接收一个double ,返回一个double ,与函数描述符一致,检查无误。
Java编译器不仅能通过上下文检查类型,同时可以推断类型。
例如上面代码中:
可以推断出需要的Lambda类型为 double -> double ,那我们就无效额外的定义类型了,代码可以精简为:
public interface PrimitiveFunction{
double getY(double x);
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
double s = integrate((x) -> -Math.cos(x),0, Math.PI);
System.out.println(s);
}
甚至当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略:
public interface PrimitiveFunction{
double getY(double x);
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
double s = integrate(x -> -Math.cos(x), 0, Math.PI);
System.out.println(s);
}
5.常见函数式接口
到这里我们已经可以在代码中应用Lambda表达式了,但是每次都要建个函数式接口(这里是PrimitiveFunction ),太麻烦!
Java8早就想到了这点,在 java.util.function 包下,为我们提供了常用的函数式接口(Predicate, Comsumer, Function等)
至此,我们代码可以修改为:
public static double integrate(Function<Double,Double> pf, double a,double b){
return pf.apply(b) - pf.apply(a);
}
public static void main(String[] args) {
double s = integrate(x -> -Math.cos(x), 0, Math.PI);
System.out.println(s);
}
同时,Java8也考虑到了装箱拆箱的性能损耗,我们可以替换为更优的函数式接口DoubleFunction:
public static double integrate(DoubleFunction<Double> pf, double a,double b){
return pf.apply(b) - pf.apply(a);
}
public static void main(String[] args) {
double s = integrate(x -> -Math.cos(x), 0, Math.PI);
System.out.println(s);
}
6.方法引用
还能更简吗?方法引用可以帮你实现!
不管是 -Math.cos(x) 也好,Math.sin(x) 也好,我们本质是在传递方法,如果一个方法是已有的,那我们是不是直接标明方法就好了。
这里有个语法糖( 目标引用 :: 方法名 ),帮我们做了这一实现
所以 Math.sin(x) 就等价于 Math::sin (注意sin方法是不带括号的,这里只是指明方法,并没有实际调用):
public static double integrate(DoubleFunction<Double> pf, double a,double b){
return pf.apply(b) - pf.apply(a);
}
public static void main(String[] args) {
double s = integrate(Math::sin, 0, Math.PI);
System.out.println(s);
}
哪些地方可以使用方法引用呢?
静态方法引用(Integer :: parseInt)
实例方法引用(String :: length)
现有对象实例方法引用(s :: length)
构造函数引用(User :: new)
7.默认方法
前面讲到一个接口可以有多个非抽象方法 ,这也是Java8的新特性。
Java8以前,想要扩展一个已有的接口是十分困难的,这会涉及到所有实现类对扩展方法的实现。
现在,我们扩展接口的同时,只需提供一个默认方法,就不会影响到实现类。List,Function都有这类的扩展。
8.复合lambda
为方便使用,许多函数式接口都提供了默认方法,来允许你进行复合运算。
例如:
Predicate 有 and 方法,可以让两个Predicate做与运算,复合为一个更复杂的表达式。
Function 有andThen 方法,可以在执行完一个Function之后,将输出作为输入,再执行另一个Function(对Stream的处理经常使用到)。
通过andThen将计算的原函数再取绝对值(当然这里没有数学意义,仅作方法使用demo):
public static double integrate(Function<Double,Double> pf, double a,double b){
return pf.apply(b) - pf.apply(a);
}
public static void main(String[] args) {
Function<Double,Double> pf = x -> -Math.cos(x);
double s = integrate(pf.andThen(Math::abs), 0, Math.PI);
System.out.println(s);
}
同时,我们也可以编写自己的函数式接口和默认方法。
下面的代码中展示了在函数式接口PrimitiveFunction中,添加默认方法abs(),实现复合lambda的案例:
public interface PrimitiveFunction{
double getY(double x);
default PrimitiveFunction abs() {
return x-> Math.abs(getY(x));
}
}
public static double integrate(PrimitiveFunction pf, double a,double b){
return pf.getY(b) - pf.getY(a);
}
public static void main(String[] args) {
PrimitiveFunction pf = x -> -Math.cos(x);
double s = integrate(pf.abs(), 0, Math.PI);
System.out.println(s);
}
9.void兼容和变量捕获
void兼容
如果一个Lambda主体是一个语句表达式,它就可以和返回void的函数描述符兼容,这样做可以让无反参的接口更方便的使用Lambda:
List<String> sList = new ArrayList<>();
Predicate<String> p = c->sList.add(c);
Consumer<String> c = sList::add;
上面的写法都是合法的,尽管 sList::add 返回了一个boolean,而Consumer 的函数描述符是 String -> void
变量捕获
Lambda表达式不仅可以使用主体参数,也可以使用其外层作用域中定义的变量(类似匿名类),这种行为被称为捕获Lambda。
在上面代码块中:
Predicate<String> p = s -> sList.add(s);
sList就是被捕获的外层变量。但是,对于局部变量的捕获,必须是final或者等效final的。下图中,对sList重新赋值,导致了编译异常。