首页 >> 大全

万字详解Java的Lambda表达式

2023-11-10 大全 23 作者:考证青年

一、概述

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

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

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

二、一个例子

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

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

package com.dake.service;public interface Printable {void print();
}

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

package com.dake.entity;import com.dake.service.Printable;public class Cat implements Printable {@Overridepublic 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 表达式之旅。

package com.dake.main;import com.dake.service.Printable;public class Lambdas {public static void main(String[] args) {Printable printable = new Cat();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 = new Cat();printThing(printable);}static void printThing(Printable printable) {printable.print();}
}

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

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

最终方法执行的其实就是Cat对象中的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(public void print() {System.out.println("喵");});}static void printThing(Printable printable) {printable.print();}
}

这是很明显代码会报错。

如果去除 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();}
}

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

这其实就是表达式。

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

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

我们知道,这个花括弧这一部分代码本来是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();}
}

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

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

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

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

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

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

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

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方法传入到了方法中,并最终形成了我们的表达式。

总结下来就是一句话:

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

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

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

package com.dake.service;public interface Printable {void print(String suffix);
}

我们在接口的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方法,但是没有传参,肯定是错误的。我们给它加一个参数。

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方法的实现上讲参数传递给了表达式,也就是(s),这个s就是我们接收的感叹号,但是我们在方法体中,也就是在下面的代码中没有使用它。

System.out.println("喵");

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

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

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

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

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是什么呢?

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

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

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

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("!");}
}

代码运行同样没有问题。

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

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

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

package com.dake.service;public interface Printable {String print(String suffix);
}

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

报错信息说了:

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

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

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方法返回类型是,但是我们这里使用表达式来实现接口的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("!");}
}

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

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

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

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

package com.dake.service;public interface Printable {String print(String prefix, String suffix);
}

代码立马会编译报错:

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

很明显,我们接口中是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("泰迪", "!");}
}

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

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

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);}
}

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

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);}
}

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

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

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

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

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

package java.lang;import java.lang.annotation.*;/*** An informative annotation type used to indicate that an interface* type declaration is intended to be a functional interface 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* not 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.** 

Note that instances of functional interfaces can be created with* lambda expressions, method references, or constructor references.**

If a type is annotated with this annotation type, compilers are* required to generate an error message unless:**

    *
  • The type is an interface type and not an annotation type, enum, or class.*
  • The annotated type satisfies the requirements of a functional interface.*
**

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 {}

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

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

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

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

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

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

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

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

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

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

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

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

这种接口也被称作Sam接口,因为它们拥有一个抽象方法( ),简称sam。

这种方法可以包含其他类型的方法,比如静态方法或默认方法。

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

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

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

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

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

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

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

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

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

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

文章参考:

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

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了