万字详解Java的Lambda表达式

一、概述

如果你是一个对lambda表达式很熟悉的老鸟,那么你可以跳过“一”。

如果你想把lambda表达式搞明白,那么建议你从“一”开始。

下面我们从一个小例子由浅入深地带你了解 Java 的 Lambda 表达式。

二、一个例子

我们从一个小例子由浅入深地讲解 Java Lambda 表达式,我们先准备一个接口和两个类。

首先,我们创建一个接口,接口中有一个抽象方法。

package com.dake.service;

public interface Printable {
    void print();
}

其次,创建一个Cat类,并实现Printable接口。

package com.dake.entity;

import com.dake.service.Printable;

public class Cat implements Printable {

    @Override
    public void print() {
        System.out.println("喵");
    }
}

最后,我们创建一个类,并添加一个main方法。

package com.dake.main;

import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {}
}

现在准备工作完毕,我们开始我们的 Java Lambda 表达式之旅。

package com.dake.main;

import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        Printable printable = new Cat();
        printable.print();
    }
}

这是一段很简单的代码,运行之后会在控制台打印一个“喵”。

现在我们在Lambdas类中添加一个静态方法,并改变调用接口方式。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        Printable printable = new Cat();
        printThing(printable);
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

 这样运行之后,依然会打印一个“喵”。

这里我们创建了一个Cat对象,然后将Cat对象实例作为参数传递到printThing方法中进行调用。

最终printThing方法执行的其实就是Cat对象中的print方法,现在我们把这个方法作为参数传入到printThing中。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        printThing(
            public void print() {
                System.out.println("喵");
            }
        );
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

这是很明显代码会报错。

如果去除 public void print,然后在()后面添加一个->这样的箭头,此时会发现代码不报错了。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        printThing(
            () -> {
                System.out.println("喵");
            }
        );
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

运行这段代码还是会打印一个“喵”。

这其实就是Lambda表达式。 

这里我们忽略了修饰符,去除了返回类型,也不需要方法名和参数类型,在()右边加了一个“->”,这样就成了一个Lambda表达式。

我们看上面的代码,IDE已经给出了提示,就是花括弧,我们可以进一步优化。

我们知道,这个花括弧这一部分代码本来是print方法的方法体,可以称作是语句Lambda。我们将方法体的花括弧去掉之后,代码依然正确,此时就成了表达式Lambda。因为我们的Lambda中只有一句代码,只是一个表达式而已。

此时代码变成了这样:

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        printThing(() -> System.out.println("喵"));
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

我们从上面知道,printThing方法中传入的原本是Cat类的print方法,通过我们一番简化操作之后,代码依然正常运行,这其实就是Lambda表达式的真谛:传递方法

我们知道,一般地,Java方法中只能传递接口、类(抽象类、普通类——对象)、变量等,此时我们做到了在Java方法中传递方法,这就是Lambda表达式。

此时,我们需要做一个简单的总结:

Lambda表达式的本质就是方法传递,其实传递的就是一个方法,这个方法像任何其他东西(接口、类(抽象类、普通类——对象)、变量)一样,我们可以将它转化为对象、当做一个变量并作为参数传递到方法中,只是我们去除了方法的修饰符、返回值类型、方法名,最后在方法的括弧右边加上Lambda表达式的标志“->”。

既然传递的是一个方法实现,我们可以将它转化为对象、当做一个变量,那么就可以赋值给这个方法对应的类的实例、类的实例的接口、抽象类。

这个方法实现,只能是接口的方法实现,不能是抽象类的方法实现。至于为什么,这里我们先卖个关子。

此时代码可以写成这样并运行,我们依然可以打印出一个“喵”。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		Printable printable = () -> System.out.println("喵");
        printThing(printable);
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

此时我们可以将Cat类注释掉,或者删除掉,因为我们其实已经将这个Cat类的print方法传入到了printThing方法中,并最终形成了我们的Lambda表达式。

总结下来就是一句话:

通过Lambda表达式,我们实现了原本需要实现类去实现的功能。

这是Lambda表达式最重要的功能。

现在我们这个Lambda表达式传递的是一个无参、无返回值的print方法,下面我们在Printable接口的print方法中增加一个参数,此时我们的printThing方法会报错,是因为我们这个Lambda表达式本身就是Printable接口方法的实现,但是我们没有传递参数。

package com.dake.service;

public interface Printable {
    void print(String suffix);
}

我们在Printable接口的print方法中添加了一个String类型的参数suffix,但是我们调用printThing方法时报错了,原因我们在上面分析过了。

这很好办,我们知道这个Lambda表达式中的()其实就是原本正常方法的括弧,如果我们要再这个Lambda表达式中添加一个参数的话,那么就只能在括弧中添加。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		Printable printable = (s) -> System.out.println("喵");
        printThing(printable);
    }

    static void printThing(Printable printable) {
        printable.print();
    }
}

此时我们的printThing方法也报错了。

这个很明显,我们使用了Printable接口的print方法,但是没有传参,肯定是错误的。我们给它加一个参数。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		Printable printable = (s) -> System.out.println("喵");
        printThing(printable);
    }

    static void printThing(Printable printable) {
        printable.print("!");
    }
}

我们执行代码,依然会打印出一个“喵”。

这时就会有小伙伴会说了,你print方法传递了一个中文的感叹号,但是为什么打印结果没有显示出来呢?

原因很简单,我们在print方法的实现上讲参数传递给了Lambda表达式,也就是(s),这个s就是我们接收的感叹号,但是我们在方法体中,也就是在下面的代码中没有使用它。

System.out.println("喵");

这其实就是方法体,只是去除了花括弧,变成了一条Java语句,我们上面说这种Lambda为表达式Lambda。

我们都知道在Java中,方法中传递一个参数,但是我们可以不使用它,当然也可以使用它,那么在Lambda表达式中也一样。

所以,最终打印出的“喵”并没有感叹号。

下面我们将参数加入进来,放在打印方法中,改造一下代码。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		Printable printable = (s) -> System.out.println("喵" + s);
        printThing(printable);
    }

    static void printThing(Printable printable) {
        printable.print("!");
    }
}

再打印。

我们看到在“喵”后面多了一个中文的感叹号,这就是在打印代码的时候拼接上去的。

Java怎么知道这个s是什么呢?

我们上面说了,Lambda表达式就是方法实现,那么Java编译器自然知道接口或者抽象类中定义的参数类型,那么接口的实现上,也就是我们这里的Lambda表达式上,也必然是同样的参数类型。

所以这里的s必然是我们之前修改的接口中方法的参数类型,也就是String类型。

既然Lambda表达式可以转化为对象或者变量,那么自然可以传递给printThing方法中,代码修改如下:

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		printThing((s) -> System.out.println("喵" + s));
    }

    static void printThing(Printable printable) {
        printable.print("!");
    }
}

代码运行同样没有问题。

 

Lambda表达式中, 如果是单个参数,可以省略括弧,但是如果没有任何参数或者多个参数,那么括弧不可以省略。

所以上面的例子中的s中的括弧都可以省略。

上面我们测试的时候的Printable接口中的print方法是没有返回值的,也就是void的,现在我们把接口改造一下返回String。

package com.dake.service;

public interface Printable {
    String print(String suffix);
}

此时的Lambdas中的代码会编译报错:

报错信息说了:

 lambda 表达式中存在错误返回类型: void 无法转换为 String

我们知道,->后面的代码其实就是方法体,只是因为我们这个方法体只有一句,所以我们对它进行了优化,而实际上真正的代码应该是这样的:

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		printThing(s -> {
            System.out.println("喵" + s);
        });
    }

    static void printThing(Printable printable) {
        printable.print("!");
    }
}

它们是等价的,但是我们看报错信息:

 因为在接口中的print方法返回类型是string,但是我们这里使用Lambda表达式来实现接口的print方法,方法体只有一个打印的方法,却没有返回值,自然会报错。

我们在上面说过,这种带花括弧的是语句Lambda,而去除花括弧,只有一个表达式的Lambda被称作表达式Lambda。

根据上面说的,它们两者其实是等价的,那表达式Lambda同样会报错,只是报错的信息不一样而已,但是本质是一样的。

所以,我们得出一个结论:

Lambda表达式中是否有返回值,以及返回什么类型,都是和接口中定义的一样的。

因为还是那句话,Lambda表达式就是方法实现

那么上面的报错,我们怎么改造呢?

有两种方式,一是针对表达式Lambda,一是针对语句Lambda。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		printThing(s -> "喵" + s);

        printThing(s -> {
            System.out.println("喵" + s);
            return "喵" + s;
        });
    }

    static void printThing(Printable printable) {
        printable.print("!");
    }
}

以上两种方式都可以,只不过一个没有打印,一个有打印而已。运行结果,自然只打印一个。

那我们怎么知道表达式Lambda写成那样就可以了呢?还是那句话,因为我们接口中定义的返回类型是String,一条语句的这种表达式Lambda只是没有return关键字而已。 而且这个关键字,我们不能加,加上return会报错。

所以,表达式Lambda如果有返回值,那么表达式只需要给出接口中定义的类型就可以了,可以是一个字符串、其他基本数据类型、一个对象实例(比如,new Cat())等,而且不能写return。

现在我们可以在接口上再加上一个参数。

package com.dake.service;

public interface Printable {
    String print(String prefix, String suffix);
}

代码立马会编译报错:

如果我们按住Ctrl加鼠标左键,代码立马跳转到了printThing方法中:

 很明显,我们接口中是2个参数,但是在这里使用print方法时是一个参数,所以自然会报错,我们加一个。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
		printThing(s -> "喵" + s);

        printThing(s -> {
            System.out.println("喵" + s);
            return "喵" + s;
        });
    }

    static void printThing(Printable printable) {
        printable.print("泰迪", "!");
    }
}

上面的2个方法也报错了:

错误原因一样的,我们同样加上一个参数。

 

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        printThing((p, s) -> p + "喵" + s);
        printThing((p, s) -> {
            return p + "喵" + s;
        });
    }

    static void printThing(Printable printable) {
        printable.print("泰迪", "!");
    }
}

此时我们执行代码,不会有任何东西打印。

我们在printThing方法中可以接收接口的返回值,并打印。因为Lambda表达式就是接口的实现,在printThing方法接收方法实现后返回的结果,自然可以打印方法执行后的结果。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        printThing((p, s) -> p + "1喵" + s);
        printThing((p, s) -> {
            return p + "2喵" + s;
        });
    }

    static void printThing(Printable printable) {
        String taidi = printable.print("泰迪", "!");
        System.out.println(taidi);
    }
}

上面的例子中,我们给出过一段将Lambda表达式赋值给接口的代码,但是那里的代码是表达式Lambda,如果是语句Lambda呢?下面分别给出这两种写法。

package com.dake.main;

import com.dake.entity.Cat;
import com.dake.service.Printable;

public class Lambdas {

    public static void main(String[] args) {
        Printable printable = (p, s) -> p + "喵" + s;

        Printable printable1 = (p, s) -> {
            System.out.println(p + "喵" + s);
            return p + "喵" + s;
        };
    }

    static void printThing(Printable printable) {
        String taidi = printable.print("泰迪", "!");
        System.out.println(taidi);
    }
}

我们可以看到,第一个表达式Lambda是不需要return,只需要给出接口方法对应的返回值即可,也就是一个字符串。而第二个语句Lambda需要一个return,并且在右花括弧后面有一个分号结尾,代表着这个方法体的结束,而且这个分号是不能少的,不然会报错。

此时如果执行方法,将不会有任何东西打印,因为这就好比我们实现了一个接口的方法,最终返回,但是没有地方调用,当然不会打印任何东西。

到了这里,关于Lambda表达式其实我们已经通过代码的方式由浅入深地讲解得差不多了。但是我们上面演示的只是如何使用,我们不能只是知其然,还要知其所以然。

上面我们演示的将Lambda表达式赋值给接口,这种接口其实就是函数式接口,只不过我们定义的Printable方法没有加关于函数式接口的注解而已,只是一个普通接口。

关于函数式接口有一个注解:

package java.lang;

import java.lang.annotation.*;

/**
 * An informative annotation type used to indicate that an interface
 * type declaration is intended to be a <i>functional interface</i> as
 * defined by the Java Language Specification.
 *
 * Conceptually, a functional interface has exactly one abstract
 * method.  Since {@linkplain java.lang.reflect.Method#isDefault()
 * default methods} have an implementation, they are not abstract.  If
 * an interface declares an abstract method overriding one of the
 * public methods of {@code java.lang.Object}, that also does
 * <em>not</em> count toward the interface's abstract method count
 * since any implementation of the interface will have an
 * implementation from {@code java.lang.Object} or elsewhere.
 *
 * <p>Note that instances of functional interfaces can be created with
 * lambda expressions, method references, or constructor references.
 *
 * <p>If a type is annotated with this annotation type, compilers are
 * required to generate an error message unless:
 *
 * <ul>
 * <li> The type is an interface type and not an annotation type, enum, or class.
 * <li> The annotated type satisfies the requirements of a functional interface.
 * </ul>
 *
 * <p>However, the compiler will treat any interface meeting the
 * definition of a functional interface as a functional interface
 * regardless of whether or not a {@code FunctionalInterface}
 * annotation is present on the interface declaration.
 *
 * @jls 4.3.2 The Class Object
 * @jls 9.8 Functional Interfaces
 * @jls 9.4.3 Interface Method Body
 * @jls 9.6.4.9 @FunctionalInterface
 * @since 1.8
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

通过上面代码我们可以知道FunctionalInterface注解是JDK1.8才有的。注解上面给出了文档注释:

一种信息注释类型,用于指示接口类型声明是Java语言规范定义的函数接口从概念上讲,函数接口只有一个抽象方法。由于默认方法有一个实现,所以它们不是抽象的。如果一个接口声明了一个抽象方法来覆盖java.lang.Object的一个公共方法,那么这也不计入该接口的抽象方法计数,因为该接口的任何实现都将具有来自java.lang.Oobject或其他地方的实现。

请注意,函数接口的实例可以使用lambda表达式、方法引用或构造函数引用来创建。

如果使用此注释类型对类型进行注释,则编译器需要生成错误消息,除非:

类型是接口类型,而不是注释类型、枚举或类。

带注释的类型满足功能接口的要求。

但是,无论接口声明上是否存在FunctionalInterface注释,编译器都会将符合函数接口定义的任何接口视为函数接口

上面标红的最后一个语句,正是我们上面说的,我们的Printable接口也是一个函数式接口。

通过注释,我们可以得出如下结论:

  1. 函数式接口只有一个抽象方法
  2. 默认方法不是抽象方法
  3. 重写Object中的方法不计入抽象方法
  4. 函数式接口的实例可以是Lambda表达式、方法引用或构造函数引用
  5. 无论接口上是否有FunctionalInterface注解,只要符合函数式接口定义的接口就是函数式接口

一个接口可以不使用FunctionalInterface注解,依然可以成为函数式接口,我们依然可以使用对应的Lambda表达式。但是如果我们添加了这个注解,就只能有一个抽象方法了。如果我们再加一个抽象方法,则编译器会报错:

在接口xxx中找到多个非重写 abstract 方法

 这种接口也被称作Sam接口,因为它们拥有一个抽象方法(single abstract method),简称sam。
这种方法可以包含其他类型的方法,比如静态方法或默认方法。

我们上面提到我们卖了一个关子:

只能是接口的方法实现,不能是抽象类的方法实现。

现在我们来解开这个关子。

首先通过函数式接口这个说法以及注解的名字(FunctionalInterface)我们就知道,这只能是一个作用在接口上的注解,所以Lambda也必然只能是接口的方法实现。

如果我们作用在抽象类上会报错:

 

 

所以,我们又得出一个结论:

函数式接口只能是接口,不能是抽象类, FunctionalInterface注解也就只能作用在接口上。

至此,关于Lambda表达式也演示差不多了,通过代码一步一步地讲解到最后的注解。到现在基本上一些简单的Lambda表达式应该也可以看懂了,我们自己也可以写一些函数式接口并应用在Lambda表达式上。

我们也是时候做一个关于第一部分的总结了。

  1. Lambda表达式就是一个接口的方法实现。去除修饰符、返回类型、方法名以及参数类型,在参数列表(也就是括弧)的右边加上一个“->”,就成了Lambda表达式。
  2. Lambda的()中就是接口的参数列表,不需要参数类型,只要参数名即可。
  3. Lambda的花括弧就是接口的方法实现。
  4. Lambda中是否有返回值,返回什么类型,都是和接口中定义的方法一样的。
  5. 只有一句代码的Lambda可以去除花括弧,如果有返回类型,不需要return关键字,只需要给出接口返回类型对应的字符串或其他数据类型或对象实例。
  6. 方法体最后的花括弧不可以省略。
  7. 只有一句代码而被去除花括弧的Lambda称作表达式Lambda。
  8. 有花括弧的Lambda称作语句Lambda。
  9. 整个Lambda的本质是一个方法传递,可以转化为任何对象,比如接口、类、变量等,自然可以赋值给对应的对象。
  10. Lambda表达式实现了原本需要在接口实现类中实现的方法。
  11. 如果Lambda的参数是单个参数,括弧可以省略;如果是无参或者多个参数,则不可以省略。
  12. Lambda实现的接口就是函数式接口。
  13. 函数式接口只有一个抽象方法。
  14. 默认方法不是抽象方法。
  15. 重写Object中的方法不计入抽象方法。
  16. 函数式接口的实例可以是Lambda表达式、方法引用或构造函数引用。
  17. 无论接口上是否有FunctionalInterface注解,只要符合函数式接口定义的接口就是函数式接口。
  18. 函数式接口只能是接口,不能是抽象类。
  19. FunctionalInterface注解只能作用在接口上,不能作用在抽象类上。
  20. 函数式接口也被称作Sam接口,因为它们只拥有一个抽象方法(single abstract method),简称sam。

文章参考:

国外大佬教你学习编程必须要会的Lamda表达式

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北冥牧之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值