您的位置:首页 > 编程语言 > Java开发

Java函数式编程之最细致的lambda表达式讲解

2019-07-11 22:39 453 查看
版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons

在这里给大家分享一下我从开始接触lambda表达式的故事:

2019年大一春季学期,教授突然开始讲Agda这门新语言,想必各位也是在此初识Agda(Agda是一个依赖类型的函数式编程语言),面对这门陌生的语言,有着许多奇奇怪怪的语言规则和无比抽象的表达方式,刚刚从半学期学习面向对象编程的Java苦海中逃离,又要掉入函数式编程的黑洞中,顿时让大家束手无策。经过了一个学期的学习和讨论,终于学有所获。Lambda的初次见面就是在学习Agda时,在此我将用Java语言来讲述我学习Lambda的来龙去脉,如有不足,希望在评论区指出,相互学习相互借鉴。

文章目录

  • 二.Lambda Calculus(Lambda演算)
  • 三.Lambda表达式定义与语法
  • 四.如何用Lambda表达式简化代码
  • 五.Lambda后传之Java8新特性【方法引用】与【构造器引用】
  • 一.必备知识:函数接口与匿名类

    函数式接口(Functional Interface)与匿名类(Anonymous Class),其中函数式接口是Java8的新特性,两者都是学习Lambda表达式的必备知识。

    1.1什么是函数式接口

    函数式接口:就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,会有

    @FunctionalInterface
    注解。

    例如:

    @FunctionalInterface
    public interface Function
    {
    void run();
    }

    里面可以有一个抽象方法,但是如果有两个或以上就会报错,但是如果加上一个Object类型的public方法呢?

    @FunctionalInterface
    public interface Function
    {
    void run();
    
    @Override
    boolean equals(Object obj);
    }

    编译器也没有报错,因此,函数式接口,有且仅有一个抽象方法,Object的public方法除外。

    1.2什么是匿名类

    匿名类,顾名思义,就是没有名称的类,没有名称也就是其他地方就不能引用,不能实例化,只用一次,当然也就不能有构造器。
    格式:

    new 父类(){子类内容……}

    其中,“父类”是子类需要继承或者实现外部的类或者接口,并且匿名类可以继承父类的方法,也可以重写父类的方法。
    例如:
    定义了一个Function接口

    public interface Function{
    int apply(int arg)
    }

    可以用匿名类来实现apply()方法

    class Test{
    public static void main(String[] args){
    
    new Function(){   //必须通过接口或者父类来实现匿名类
    @Override
    public int apply(int arg){
    return arg*3;
    }
    };
    }

    如果没有匿名类,就还需要一个实现类的class来实现Function接口,还有测试类:

    class FunctionTest implements Function{
    @Override
    public int apply(int arg){
    return arg*3;
    }
    }
    
    class Test{
    public static void main(String[] args){
    
    Function f = new FunctionTest();//创建接口对象,必须通过接口FunctionTest实现类实现
    int result = f.apply(1);
    System.out.println(result);
    
    }
    }

    二.Lambda Calculus(Lambda演算)

    前序:

    之前在网上看了许多关于lambda的相关文章,基本上都是直奔主题,从Lambda表达式的定义开始的,但是想要真正了解Lambda的来历,应当从Lambda Calculus(Lambda演算)说起。
    Lambda Calculus是所有函数语言(如Haskell、OCaml、Scala等)的基础。然而,lambda演算现在是许多非函数性语言(如Java、C++、Python等)的一部分。原因是,定义匿名函数有时很方便,就像匿名类很方便一样,通常在回调函数的定义中也是如此。

    正文:

    我们理解函数的一种方法是把它看成是一个从输入到输出的映射。 例如,给输入的x加1,我们可以写成:x↦x+1x↦x+1x↦x+1

    方程也可以有多个变量,比如一个方程可以计算输入值的平均值:(x,y)↦(x+y)2(x,y)↦\frac{(x+y)}{2}(x,y)↦2(x+y)​

    方程也可以拿其他的方程当作参数,比如argmax function,其返回参数的值,其结果为最大值(argmax):f↦xsuchthat∀x′.f(x′)≤f(x)f↦x such that ∀x′.f(x′)≤f(x)f↦xsuchthat∀x′.f(x′)≤f(x)

    所有这些都需要统一形式化,并且以一种清晰可计算的方式,以一种机械的方式执行。

    丘奇的绝妙想法是定义一个简单的函数,只包含变量、函数定义和函数应用:

    • 函数应用:
      f x
      or
      f (x)
    • 函数定义:
      \x -> F
      (F是Lambda项),就是由变量、函数应用程序和函数定义组成的表达式。

    例子:

    (\x -> x + 1) 7              //相当于:f(x)=x+1,其中x=7;
    = 7 + 1
    = 8
    
    (\x y -> x + y) 7 8         //相当于:多参函数f(x,y)=x + y
    = (\y -> 7 + y) 8     //不理解多参函数的同学可以查看下方主页文章:【在Java中的函数】
    = 7 + 8
    = 15
    
    (\x -> x + 1) ((\x -> x + 1) 2)   //先演算(\x -> x + 1) 2
    = (\x -> x + 1) (2 + 1)
    = (\x -> x + 1) 3
    = 3 + 1
    = 4

    【在Java中的函数】https://blog.csdn.net/weixin_44551646/article/details/95369228

    如果对Lambda Calculus感兴趣的同学,更多详细资料—>主页文章【Lambda Calculus】

    三.Lambda表达式定义与语法

    • 定义
      我们看了Lambda演算的过程,就对Lambda有了基本的数学概念上的理解。
      Lambda Calculus就是定义了一个简单的函数,只包含变量、函数定义和函数的应用,那么Lambda表达式就是与Lambda Calculus一样,可以理解为表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。

    • 语法
      (1)参数列表(参数可以写类型,也可以不写,JVM可类型推断
      (2)箭头➡️
      (3)函数主体(可以是单语句,也可以是代码块)
      (4)返回类型(➡️后的内容)

    • 例题:
      根据Lambda Calculus和Lambda表达式定义与语法,我们先猜想一下以下5个代码分别代表什么意思:

    1. () -> 5
    
    2. (x) -> 2 * x 或者 x-> 2 * x
    
    3. (x, y) -> x – y
    
    4. (int x, int y) -> x + y
    
    5. (String x) -> System.out.print(x)
    
    6. ()->{
    for(int i=0; i<10; i++) {
    System.out.println("Thread"+i);
    }
    }

    答案:

    1. 不需要参数,返回值为 5
    2. 接收一个参数(数字类型),返回其2倍的值
    3. 接受2个参数(数字),返回他们的差值
    4. 接收2个int型整数,返回他们的和
    5. 接受一个 string 对象,并在控制台打印,不返回任何值
    6. 不需要参数,并执行大括号里的内容,不返回任何值
    lambda一共有这几种情况:
    1.无参数,无返回值
    2.无参数,有返回值
    3.有一个参数,无返回值(括号可省略)
    4.有两个或两个以上的参数,有返回值,并且函数体是语句块
    5.若只有一条语句,return和大括号都可省略

    这就是Lambda表达式,正如Lambda Calculus一样,只不过与Java方法相结合,Lambda表达式实质就是可传递的匿名函数的一种方式:它没有名称,但它有参数列表(括号内的为参数)、函数主体(大括号内的内容)、返回类型(箭头后的内容)。

    四.如何用Lambda表达式简化代码

    前文中,我们将Lambda演绎与Lambda表达式相结合,也了解了基础知识点,但是到底如何使用Lambda表达式来简化我们的代码呢?具体应该在什么样的场景下使用Lambda表达式?

    4.1 Lambda在【函数式接口】中的应用

    1⃣️:带有返回值的Lambda使用:
    以上述代码为例,如果我们在Java中不用Lambda,只用匿名内部类则代码如下;

    public inter
    8000
    face Function{
    int apply(int arg);
    }
    
    class Test{
    public static void main(String[] args){
    
    new Function(){   //必须通过接口(Function)或者父类来实现匿名内部类
    @Override
    public int apply(int arg){
    return arg*arg;
    }
    };
    
    }

    Lambda是Java8新引入的,使用了Lambda并不会改变定义Function接口的方式,但是它会让实现变得非常简单:

    class Test{
    public static void main(String[] args){
    
    Function square = arg -> arg * arg;
    
    }

    注意⚠️:

    1. Lambda推导必须要有类型,Function square其中Function为接口名,square为对象名
    2. Lambda表达式中的第一个arg为参数,就是apply()方法的参数。
    3. 当只有一个参数时可以省略,即
      (arg) -> arg * arg;
      中的括号可以省略。
    4. 方法体只有一句话,所以return,大括号都可以省略。
    5. Lambda语句的参数列表里的数据类型可以省略不写,JVM会自动推断。

    2⃣️没有返回值的Lambda使用:

    没有用lambda之前:

    public class Test{
    public static void main(String args[]){
    Runnable r = new Runnable(){
    public void run(){
    System.out.println("Lambda");
    }
    };
    r.run();
    }
    }

    使用Lambda后:

    public class Test2{
    public static void main(String args[]){
    Runnable r = ()->System.out.println("Lambda");
    r.run();
    }
    }

    注意⚠️:

    1. 由于run()方法中没有参数,所以可以用空括号表示,但是括号不能省略。

    3⃣️自定义函数式接口

    public interface ILike
    {
    void lambda();
    }
    
    public class Like implements ILike
    {
    @Override
    public void lambda()
    {
    System.out.println("I Like Lambda!");
    
    }
    }
    
    class Test{
    public static void main(String[] args ) {
    //一般实现方法
    ILike like = new Like();  //接口指向实现类
    like.lambda();
    
    //匿名内部类实现
    ILike like2=new ILike() {
    @Override
    public void lambda()
    {
    System.out.println("I Like Lambda2!");
    }
    };
    like2.lambda();
    
    //lambda实现
    //ILike like3 :lambda推导必须存在类型,ILike类型的like3
    ILike like3 = ()-> System.out.println("I Like Lambda3!");
    like3.lambda();
    
    }
    }

    3.2 Lambda在【线程】中的应用

    Lambda表达式可以简化Runnable线程,线程必须是使用一次的简单线程。

    下面是使用了匿名内部类,实现Runnable接口的线程:

    public class Test1
    {
    public static void main(String[] args)
    {
    new Thread(new Runnable(){
    @Override
    public void run()
    {
    System.out.println("匿名内部类+Runnable实现线程");
    }).start();
    }
    }
    }

    下面是使用lambda之后的线程:
    1⃣️:函数主体为单语句Lambda表达式

    public class Test1
    {
    public static void main(String[] args)
    {
    new Thread(()->System.out.println(“Lambda+Runnable实现线程”)).start();
    }
    }

    2⃣️:函数主体为代码块的Lambda表达式

    new Thread(()->{
    for(int i=0; i<10; i++) {
    System.out.println("Thread"+i);
    }
    }).start();

    3.3 Lambda在【比较器】中的应用

    Collections.sort(list1, (o1,o2) -> {
    if(((Student) o1).getName()==((Student) o1).getName()) {
    return ((Student) o1).getName().compareTo(((Student) o1).getName());
    }else {
    return Integer.compare(((Student)o1).getAge(), ((Student)o2).getAge());
    }
    });

    3.5 Lambda在【集合】中的应用

    首先定义一个student类

    public class Student {
    
    private String name;
    private int age;
    
    public String getName()
    {
    return name;
    }
    
    public void setName(String name)
    {
    this.name = name;
    }
    
    public int getAge()
    {
    return age;
    }
    
    public void setAge(int age)
    {
    this.age = age;
    }
    
    public Student(){}
    
    public Student(String name, int age)
    {
    super();
    this.name = name;
    this.age = age;
    }
    @Override
    public String toString()
    {
    return "Student [name=" + name + ", age=" + age + "]";
    }
    }

    1⃣️lambda+forEach循环遍历集合:

    在测试类中,我们分别用普通的 forEach循环和lambda+forEach循环

    import java.util.ArrayList;
    
    class Test{
    
    public static void main(String []args) {
    
    ArrayList<Student> list1 = new ArrayList<>();
    list1.add(new Student("lili",001));
    list1.add(new Student("haha",002));
    list1.add(new Student("wawa",003));
    
    //foreach循环
    for(Student stu : list1) {
    System.out.println(stu);
    }
    System.out.println("-----");
    
    //lambda+foreach循环
    list1.forEach(stu -> System.out.println(stu));
    }
    }

    Output:

    Student [name=lili, age=1]
    Student [name=haha, age=2]
    Student [name=wawa, age=3]
    -----
    Student [name=lili, age=1]
    Student [name=haha, age=2]
    Student [name=wawa, age=3]

    2⃣️在Stream()中使用Lambda表达式:

    五.Lambda后传之Java8新特性【方法引用】与【构造器引用】

    5.1.方法引用

    如果lambda表达式中有方法已经实现了,就可以使用【方法引用】。也可以理解为【方法引用】是Lambda表达式的另外一种表现形式。

    三种表达形式

    1.对象::实例方法名

    2.类::静态方法名

    3.类::实例方法名

    注释:

    实例方法:属于对象的方法,由对象来调用。
    静态方法:使用static修饰(静态方法),属于整个类的,不是属于某个实例的,只能处理static域或调用static方法;

    例子1⃣️:

    对象::实例方法名

    //consumer函数就是对所输入的参数进行操作,这里是打印出所输入的参数hhh
    Consumer<String> con = x -> System.out.println(x);
    con.accept("hhh");

    因为Lambda体中已经有println()方法完成了我们想要的功能,因此可以用方法引用的方式表达
    Lambda体中

    System.out.println(x);
    其实是
    PrintStream out = System.out;
    out.println();
    两步操作完成的,因此
    println()
    方法是一个实例方法,对象名为
    out

    应用【方法引用】后:

    PrintStream out = System.out;
    Consumer<String> con1 = out::println();
    con1.accept("hhh");

    最终简化版:

    Consumer<String> con2 =System.out::println();
    con2.accept("hhh");

    注意⚠️:要实现的接口中的抽象方法的参数列表和返回值类型,必须要与调用的方法中的参数列表和返回值类型保持一致。
    上述例子中:接口中的抽象方法为

    void accept(T t);
    调用的方法为
    public void println(String x)

    再比如,我有一个Student类,定义了name和age两个私有属性,有get和set方法。想要获取stu的年龄,通过supplier函数

    Student stu = new Student("xiaowang",20);
    Supplier<Integer> sup = ()->sup.getAge();
    Integer num = sup.get();
    System.out.println(num);

    可以简化为:

    Student stu = new Student("xiaowang",20);
    Supplier<Integer> sup =stu::getAge;
    Integer num = sup.get();
    System.out.println(num);

    例子2⃣️:

    类::静态方法名

    Comparator<Integer> cam1 = (x, y) -> Integer.compare(x, y);
    System.out.println(cam1.compare(3, 2));

    因为函数接口中抽象方法:

    int compare(T o1, T o2);
    与调用方法:
    public static int compare(int x, int y)
    返回值类型与参数类型一致,并且调用方法为静态方法,所以应用【方法引用】之后为:

    Comparator<Integer> com1 = Integer::compare;
    System.out.println(com1.compare(1, 2));

    例子3⃣️:

    类::实例方法名

    BiPredicate是Predicate的子类,其区别是,BiPredicate可以传入两个参数进行比较。

    @FunctionalInterface
    public interface BiPredicate<T, U> {
    
    /**
    * Evaluates this predicate on the given arguments.
    *
    * @param t the first input argument
    * @param u the second input argument
    * @return {@code true} if the input arguments match the predicate,
    * otherwise {@code false}
    */
    boolean test(T t, U u);
    }

    比如相比较两个字符串

    BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    System.out.println(bp.test("a", "b"));

    使用【方法引用】后:

    BiPredicate<String, String> bp1 = String::equals;
    System.out.println(bp1.test("a", "b"));

    注意⚠️:equals为实例方法,为什么不能用

    对象::实例方法名
    的方式,因为当lambda的参数列表中第一个参数为方法的调用者,第二个参数为实例方法的参数时,可以通过
    类::实例方法名
    使用【方法引用】

    5.2构造器引用

    格式:

    类名::new

    Student类:
    其中有一个无参构造器,一个含有一个参数的有参构造器,和一个含有两个参数的有参构造器。

    public class Student {
    
    private String name;
    private int age;
    
    public Student(){}
    
    public Student(int age)
    {
    super();
    this.age = age;
    }
    
    public Student(String name, int age)
    {
    super();
    this.name = name;
    this.age = age;
    }
    
    @Override
    public String toString()
    {
    return "Student [name=" + name + ", age=" + age + "]";
    }
    }

    1⃣️:没有用构造器引用之前,无参构造

    public class Main {
    public static void main(String[] args) {
    //没有用构造器引用之前,无参构造
    Supplier<Student> sup = () -> new Student();
    Student stu1 = sup.get();
    System.out.println(stu1);
    }

    Output:

    Student [name=null, age=0]

    因为用的无参构造器,没有传入参数,所以name为null,age为0;

    1⃣️:使用构造器引用之后,无参构造

    public class Main {
    public static void main(String[] args) {
    //用造器引用之后,无参构造
    Supplier<Student> sup = Student::new;
    Student stu1 = sup.get();
    System.out.println(stu1);
    }

    2⃣️:没有用构造器引用之前,一个参数的有参构造

    class Test{
    public static void main(String []args) {
    Function<Integer, Student> fun = (age) -> new Student(age);
    Student stu1 = fun.apply(18);
    System.out.println(stu1);
    }
    }

    Output:

    Student [name=null, age=18]

    2⃣️:使用构造器引用之后,一个参数的有参构造

    class Test{
    public static void main(String []args) {
    Function<Integer, Student> fun = Student::new;
    Student stu1 = fun.apply(18);
    System.out.println(stu1);
    }
    }

    3⃣️:未使用构造器引用之前,两个参数的有参构造

    class Test{
    public static void main(String []args) {
    BiFunction<String,Integer, Student> fun = (name,age) ->new Student(name,age);
    Student stu1 = fun.apply("xiaowang",18);
    System.out.println(stu1);
    }
    }

    Output:

    Student [name=xiaowang, age=18]

    3⃣️:使用构造器引用之后,两个参数的有参构造

    class Test{
    public static void main(String []args) {
    BiFunction<String,Integer,Student> fun = Student::new;
    Student stu1 = fun.apply("xiaowang",18);
    System.out.println(stu1);
    }
    }

    三种构造引用:

    Supplier<Student> sup = Student::new;
    Function<Integer, Student> fun = Student::new;
    BiFunction<String,Integer, Student> fun = Student::new;

    注意⚠️:

    • 构造引用中,等号后面统一格式
      类名::new
    • 构造引用中,创建对象时我们需要传入的参数都在接口中

    比如,我们看一下BiFunction接口的源码:

    @FunctionalInterface
    public interface BiFunction<T, U, R> {
    
    /**
    * Applies this function to the given arguments.
    *
    * @param t the first function argument
    * @param u the second function argument
    * @return the function result
    */
    R apply(T t, U u);
    
    }
    • 在BiFunction<T, U, R>接口中,我们需要三个参数T,U,R,与上文例子对应,T就是String,U就是Integer,R就是Student。
    • 在接口的
      R apply(T t, U u);
      方法中,我们可以接受两个分别参数,分别为T类型的tU类型的u,并且返回一个参数R。与上文例子对应,t就是String类型的name,U就是Integer类型的age,R就是Student。

    当然我们也可以自定义接口

    @FunctionalInterface
    interface MyInterface<S>{
    public S show(String n,int a);
    }
    • 在MyInterface< S>接口中,我们需要一个参数S,在下文中S就是Student。
    • 在接口的
      S show(String n,int a)
      方法中,我们可以接受两个参数n和a,并返回参数S。其中n就是name,a就是age,S就是Student。
    public class Main {
    public static void main(String[] args) {
    MyInterface<Student> m = Student::new;
    Student stu = m.show("xiaowang",20);
    System.out.println(stu);
    }
    }

    推荐阅读:

    1. 【四种内部类讲解】:https://blog.csdn.net/sinat_37003267/article/details/80058544
    2. 【深入学习Java8中的函数式接口】:https://www.geek-share.com/detail/2712138948.html
    3. 【在Java中的函数】https://blog.csdn.net/weixin_44551646/article/details/95369228
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: