Lambda -- 02概念

目录

Lambda表达式语法

行为值 <=> 函数式接口的实例对象

行为参数化

类型检查、推导、限制

语法糖:简易版本

语法糖的语法糖:方法引用

数学中的函数思想

函数调用链:柯里化思想


Lambda表达式语法

Lambda表达式的一般形式:( params )-> { statements; }   ( params )-> statement

( ) 方法的入参描述
-> lambda表达式的语法特征符,见到这个符号就表示是lambda表达式
{ } 方法主体,其内部是方法的内容(也即具体的业务逻辑和返回值)

以下是lambda表达式的重要特征:

  • 可选的入参类型声明:不需要声明参数类型,编译器可以统一判断识别实参的类型。
  • 可选的圆括号:当只有一个方法入参时无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果方法主体内只包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个语句则编译器会自动返回值;当使用大括号时且想要返回值时则需要明确给出return关键字。
/**
* Lambda 表达式实例
*/

//无参,返回值为 5  
() -> 5

//无参,无返回值
() -> System.out.println(123)  
  
//单一入参,入参无类型,是否需要返回运算结果要看抽象方法的定义,本表达式是支持返回值的  
x -> 2 * x  
  
//两个入参,入参无类型,是否需要返回运算结果要看抽象方法的定义,本表达式是支持返回值的  
(x, y) -> x – y  
  
//两个入参,入参有类型,是否需要返回运算结果要看抽象方法的定义,本表达式是支持返回值的  
(int x, int y) -> x + y  
  
//单一入参,入参有类型,无返回值 
(String s) -> System.out.println(s)

//单一入参,入参有类型,有返回值
(String s) -> { 
    System.out.println(s);
    return s.substring(1);
}

行为值 <=> 函数式接口的实例对象

jdk8之前的编程手段,java中只有8大基本类型(6个数值类型,1个布尔类型,1个字符类型),Class类类型,Type泛型(泛型,泛型变量,泛型数组,泛型上下界)。java中所有的类/接口/方法/方法入参/方法返回值/属性等定义时,一定是8大基础类型或Class或Type中的一种,脱离不了这个范围。那么,语法形如:() -> {} 的lambda表达式到底是个什么类型呢?难道jdk8引入了lambda表达式又配套的引入了一种新的类型嘛?答:不是的,lambda表达式不过是语法糖而已,简写版的描述:一个方法的入参,业务逻辑和返回值,lambda表达式的类型是类类型的。请看下方小示例:

Predicate<String> p = ( String s ) -> { return s.equals("abc"); }

  • lambda表达式:( String s ) -> { return s.equals("abc"); }
  • s 方法入参
  • s.equals("abc"); 方法的主体业务逻辑
  • return s.equals("abc"); 方法的返回值
  • p 实例对象
  • Predicate 类类型

Predicate

小小总结:lambda表达式,也叫 行为值,也叫 函数式接口的实例对象。

行为参数化

 “行为参数化”,什么意思呢,就是你把行为可以当成一个《值》一样来传递给方法了,见上方行为值的说明和图示。这个《值》更贴切的叫法,应该叫《行为值》,行为值要描述清楚,行为(/行为也就是函数,也就是方法)的入参是什么,行为的具体逻辑是什么,行为有什么样的返回值

所以,行为参数化,更具体点来说,应该叫做“行为值参数化”。至此,我们会发现,虽然jdk8引入了lambda表达式,也可以当成方法入参来直接传递,但是其本质仍然没脱离jdk的传统,Java编程传统,编写的代码都是有明确的类型的,类有明确的类型;泛型有明确的类型;类里面的数据成员(/属性)有明确的类型;类里面的方法成员有明确的入参类型,返回值类型;

举例:Collections类的sort(List<T> list, Comparator<? super T> c) 方法的示例

Collections.sort( List<Integer> list, (a,b) -> a-b ); //编译ok
Collections.sort( List<String> list, (a,b) -> a.indexOf("x") - b.indexOf("x") ); //编译ok

(a,b) -> a-b 和 (a,b) -> a.indexOf("x")-b.indexOf("x") 表达式是不同的,但都符合Comparator函数式接口的表达,我们针对这种现象可以说:lambda表达式(也即行为值),是可以当成入参的,进而有了 "行为参数化" 的概念。

类型检查、推导、限制

Lambda表达式的目标类型检查推导的过程:

A:搜寻函数式接口
B:搜寻函数式接口的唯一抽象方法
C:推导lambda表达式是否符合抽象方法的签名(方法入参,方法返回值)

Lambda表达式的目标类型推导有以下三种方式:
1:从赋值的上下文来推导
2:从调用方法(入参)的上下文来推导
3:从类型转换的上下文来推导


让我们依据推导过程,演示下推导的三种方式,不同角度的上下文环境中,怎么推导出来lambda表达式的类型的,

1:从赋值的上下文来推导目标类型

从赋值的上下文来推导目标类型

举例:Runnable r = ()-> System.out.println("abc")

A:搜寻函数式接口 接口 Runnable
B:搜寻函数式接口的唯一抽象方法

抽象方法 public void run();

C:是否匹配抽象方法的签名

方法入参

()

方法返回值

System.out.println("abc")

以上的从赋值的上下文来推导目标类型的示例:

Runnable r = ()-> System.out.println("abc") //编译OK

Object r = ()-> System.out.println("abc") //编译failure,Object不是一个函数式接口!


2:从调用方法(入参)的上下文来推导

从调用方法(入参)的上下文来推导

举例:Collections.sort( List<Integer> list, (a,b) -> a-b ); // Lambda表达式为 (a,b) -> a-b

A:搜寻函数式接口

Collections.sort()方法的声明:

sort(List<T> list, Comparator<? super T> c);

sort()方法的第二个入参为Comparator<T>类型
Comparator<T>类型,是函数式接口
B:搜寻函数式接口的唯一抽象方法

Comparator函数式接口中的唯一抽象方法:

int compare(T o1, T o2);

C:是否匹配抽象方法的签名 方法入参 T o1, T o2

T明确为Integer类型,入参a,b都是Integer类型

方法返回值 int a-b的结果为int类型(int可自动拆装箱)

以上的从调用方法(入参)的上下文来推导目标类型的示例:

Collections.sort( List<Integer> list, (a,b) -> a-b ); //编译ok
Collections.sort( List<Integer> list, (a,b) -> a.indexOf("x") - b.indexOf("x") ); //编译failure,入参a,b推导为Integer类型,Integer类的定义中无indexOf方法。


3:从类型转换的上下文来推导

interface MyPredicate{ //我们自己定义的函数式接口
    boolean test(Character d);
}


public class TestUser
{
    static boolean test(MyPredicate p){ //多态:重载的同名方法test
          return p.test('a');
    }
    static boolean test(Predicate<Character> p){ //多态:重载的同名方法test
          Predicate<Character> p0 = p.negate();
          return p0.test('a');
    }
 
    public static void main(String[] args){
        /** System.out.println( test( (d)->{return true;} ) );
         *
         * 该句无法编译:
         * TestUser.java:32: 错误: 对test的引用不明确
         * test(MyPredicate) 和 test(Predicate<Character>) 都匹配 (d)->{return true;}
         */

         System.out.println( test((MyPredicate)d->true) ); //输出true
         System.out.println( test((Predicate<Character>)d->true) ); //输出false
    }
}

test((MyPredicate)d->true) d -> true

目标类型:MyPredicate函数式接口

test((Predicate<Character>)d->true) d -> true 目标类型:Predicate函数式接口

对于局部变量访问的一些限制:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);//lambda表达式访问局部变量portNumber,局部变量必须是final的,或 事实上是最终final的,此句编译ok

----------------------------------------------------------------------------------------------------

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);//此句编译failure,无法编译。因为portNumber局部变量不是事实上最终final的,下方这条语句对它进行了二次赋值。
portNumber = 31337;


语法糖:简易版本

如果以传统的编程手段,《接口-->实现类》的手法 或者 《匿名内部类》的手法,都是可以实现接口的具体业务逻辑的。但是传统的编程手段,显得太low,尤其是一次性使用的具体业务逻辑实现时,显得死模板代码的存在更是臃肿,由此产生了可省略死模板代码的编程手段,Java引入了Lambda表达式,它就是函数式接口的具体业务逻辑的实现。其可以更优雅的方式(所谓的更优雅就是指,可以省略掉传统编程手段中的死模板代码)来实现具体业务逻辑。所以相对于传统编程手段的臃肿和繁琐,我们把同样实现了具体业务逻辑的Lambda表达式称之为“语法糖”,由此可知,语法糖是相对于传统臃肿编程手段而言的。

举例:

    MyPredicate p = new MyPredicateImpl(){ //【本质】
        public boolean test(Dish dish){
            return dish.getCalories() > 400;
        }
    }

和下面这一句是一个意思:

    MyPredicate p = (dish) -> { return dish.getCalories() > 400; } ;


语法糖的语法糖:方法引用

lambda表达式是传统编程手段的语法糖,那如果lambda表达式本身就比较复杂,lambda它本身能以更优雅的姿态来抒写嘛?答:是可以的。lambda表达式更进一步的简易抒写的编程手法,就是《方法引用》。先看小示例:

List<Integer> lst = Arrays.asList(666,456,789,123);        
Collections.sort(lst,(a,b)->a-b);
lst.stream().forEach(System.out::println); //输出 123 456 666 789

method reference

数学中的函数思想

数学中的标准函数如下定义:

f(x) = x + 10 ; //给出一个整型数值x,最终的答案是x+10,且此函数是无副作用的。所谓的无副作用指的是:无论给出的整型数值x是多少,其最终答案都是x+10,不会因为整型数值x的不同,而影响运算逻辑x+10

那么jdk8以来,引入的lambda表达式,就是表现函数式编程的。让我们看看lambda的长相,会发现lambda表达式,就是引用了数学中的函数的抒写的手法。

(x) -> { return x + 10; }

函数调用链:柯里化思想

函数柯里化基本是在做这么一件事情:只传递给f函数一部分参数来调用它,让它返回另一个g函数去处理剩下的参数。用公式表示就是我们要做的事情其实是

f(a,b,c,d) => f(a){ return g(b,c,d){ a+b+c+d; } }

fn(a,b,c,d) => fn(a,b){ return g(c,d){ a+b+c+d; } }

fn(a,b,c,d) => fn(a,b,c){ return g(d){ a+b+c+d; } }

这也是我们常说的闭包函数的体现,所谓闭包函数,就是f函数主体的内部有一个定义了的g函数,然后返回此g函数。所以说,柯里化思想体现到具体编程手法上面,就是闭包函数的编写。

举例:

函数:接受两个入参的加法 函数:只接受一个入参的加法 Java类

function add(a, b) {

       return a + b;

}

function f(a) {

       return g(b) { //闭包函数

             return a + b;

       }

}

class A{

     private int a ;

     public A(int a){

            this.a = a;

     }

     public int add(int b){

            return a + b ;

     }

}

运用:add(2,3) ; //输出5

运用:var add = f(2) ;

           add(3) ; //输出5

运用:A obj = new A(2) ;

           obj.add(3) ; //输出5

由举例,不难发现,function f(a) 只接受一个参数的函数,仍然能实现加法。之所以能实现,是因为f(a)的执行,返回闭包函数g(b),闭包函数g(b)是能访问宿主函数f(a)作用域内的一切变量的,宿主函数f(a)的作用域是指:f(a)的入参,f(a)主体内部定义的局部变量,它们的生命周期都是局限在f(a)内部的。

之所以表格举例中,还列举出第三列Java类,是方便大家理解。我们可以认为f(a)就是类的初始化new A(2),g(b)就是调用类的方法obj.add(3),类中的私有成员int a 当然是能被类中的实例方法add(int b)来访问的。


那柯里化思想带给我们的好处或者说,它的使用场景在哪些方面呢?

柯里化思想,就是要不停的编写函数的调用链(函数内部还有函数),来逐步的精简函数的入参个数,使得最外层的函数的入参足够精简。了解了柯里化思想,有助于我们理解jdk中的引入的java.util.function包,这个包是函数包(也就是唯一抽象方法的函数式接口的包),绝大部分的函数式接口中都有默认方法,并且该默认方法执行后,返回的是函数。随便举个例子说明下:

public interface Predicate<T> { //函数式接口

    boolean test(T t); //唯一的抽象方法

    //与
    default Predicate<T> and(Predicate<? super T> other) {
        return (t) -> test(t) && other.test(t); //返回的是lambda表达式,也即另一个函数
    }
}

猜你喜欢

转载自blog.csdn.net/mmlz00/article/details/86676021
今日推荐