Lambda: 代码传递

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重新赋值,导致了编译异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值