赞
踩
如果你是一个对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接口也是一个函数式接口。
通过注释,我们可以得出如下结论:
一个接口可以不使用FunctionalInterface注解,依然可以成为函数式接口,我们依然可以使用对应的Lambda表达式。但是如果我们添加了这个注解,就只能有一个抽象方法了。如果我们再加一个抽象方法,则编译器会报错:
在接口xxx中找到多个非重写 abstract 方法
这种接口也被称作Sam接口,因为它们拥有一个抽象方法(single abstract method),简称sam。
这种方法可以包含其他类型的方法,比如静态方法或默认方法。
我们上面提到我们卖了一个关子:
只能是接口的方法实现,不能是抽象类的方法实现。
现在我们来解开这个关子。
首先通过函数式接口这个说法以及注解的名字(FunctionalInterface)我们就知道,这只能是一个作用在接口上的注解,所以Lambda也必然只能是接口的方法实现。
如果我们作用在抽象类上会报错:
所以,我们又得出一个结论:
函数式接口只能是接口,不能是抽象类, FunctionalInterface注解也就只能作用在接口上。
至此,关于Lambda表达式也演示差不多了,通过代码一步一步地讲解到最后的注解。到现在基本上一些简单的Lambda表达式应该也可以看懂了,我们自己也可以写一些函数式接口并应用在Lambda表达式上。
我们也是时候做一个关于第一部分的总结了。
文章参考:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。