方法引用替代Lambda表达式
什么情况可以使用方法引用替代lambda表达式?
下面代码中两处使用了lambda表达式,一个是filter内,一个是forEach内。其中,forEach内的lambda表达式可以被方法引用替代,但是filter内的lambda表达式不能被方法引用替代。
package com.yimeng;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream().filter(e -> e % 2 == 0).forEach(e -> System.out.println(e));
}
}
使用方法引用取代的做法:
package com.yimeng;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream().filter(e -> e % 2 == 0).forEach(System.out::println);
}
}
为什么filter内的lambda表达式不能使用方法引用取代,但是forEach内的lambda表达式可以被方法引用取代呢?
这个需要看lambda表达式里面的内容了。
在上面案例中,filter内的lambda表达式是写一个对流中元素进行处理的逻辑。而forEach内的lambda表达式只是做了一个传递参数的动作,他相当于是借用了已经存在的方法去执行具体的逻辑,至于逻辑是什么样的,得看System.out对象的println方法了。
所以看出区别了吧!一个是自己在lambda表达式的方法体里面去处理逻辑,一个是传递参数,使用已经有的方法去处理逻辑。就是说,你想要做的事情已经有了一个解决方案了,你直接用那个解决方案来处理就行了(方法就是解决方案)。
总结:如果是直接传递参数到其他方法中,使用其他方法去处理的,那么可以使用方法引用来替代lambda表达式。(这里还要注意一点,就是能替代为方法引用的lambda表达式,一定是单纯的调用其他方法来处理哈,一定是只有一句语句,就是仅仅做参数传递的lambda表达式才能简化为方法引用。)
我们知道lambda表达式是满足某些条件(满足存在上下文并且上下文推断出一个接口,并且接口是一个函数式接口)的匿名内部类的一种简写。
而方法引用其实就是对满足某些条件的lambda表达式再次进行简写的一种写法。注意:匿名内部类、lambda表达式、方法引用都是一个对象,是一个实例哦。
满足某些条件的lambda表达式,那么这里说的满足某些条件是指什么?
什么条件下,我们可以使用方法引用替代lambda表达式?
1. 上下文能推断出函数式接口
首先,能使用方法引用的地方一定能使用lambda表达式,因为方法引用是在特定条件下lambda表达式的简写嘛!
某个位置要能写lambda表达式,能写lambda表达式,所以这个能写方法引用的地方通过上下文环境肯定能推断出函数式接口。(lambda表达式中的知识点)
比如,我们要写内容的地方是在某个方法里面,方法的形参是函数式接口,那么这个地方就能写lambda表达式。比如赋值操作,赋值号的左边是一个函数式接口,那么你赋值号的右边就可以写lambda表达式。
所以要能写方法引用第一个条件就是,要有能推断出函数式接口的上下文。
2. 需要完成的函数式接口的抽象方法可以通过直接借用的存在方法来达到目标
然后,因为不是所有的lambda表达式都可以用方法引用来替代的,只有在函数式接口抽象方法实现可以是单纯调用其他已经存在的方法就可以完成当前需要的功能的情况下,才可以使用方法引用来替代lambda表达式。所以要某个地方要能使用方法引用,那么必须是这个地方的上下文推断出来的抽象方法的实现,可以是单纯地调用某个存在的方法就可以完成我们需求的。(注意,相当于是纯调用才行,并且只能是一句语句)
好,我们想象一下,如果现在要你补充完一段代码逻辑,你发现这段代码的上下文是一个函数式接口,并且你也找到了能借用的方法。那么你要怎么借用呢?
怎么使用方法引用去借用已经存在的方法
1. 如果要借用的方法是一个静态方法
“类名::静态方法”
使用==“类名::静态方法”==就行。(注意,不能用实例::静态方法
)
对函数式接口的要求:抽象方法的形参能传给被借用方法,抽象方法的返回值能接纳借用方法的返回值,或者抽象方法的返回值是void。
例子:
package com.yimeng.mydemo;
public class Demo1 {
public static void main(String[] args) {
new Demo1().f();
}
private void f() {
/* 问题:MyInterface1 myInterface1 = ??;这里的??
*
* 看题目知道当前存在可以推断出函数式接口的上下文。并且知道要实现的方法长这样:long sum(int a,Integer b);
* 知道这个接口的抽象方法想要实现的功能是把a和b相加,然后返回相加后的结果。
* 看到Demo1中有一个静态方法demo1Sum(Integer a,Integer b)和我们想实现的功能是一样的,我们直接调用那个方法就能实现我们要的功能,所以我们可以用方法引用来完成。
* 怎么用呢?要方法引用一个静态方法,那么就要用“类名::静态方法名”
**/
MyInterface1 myInterface1 = Demo1::demo1Sum;
// 注意,方法引用和lambda表达式、匿名内部类一样,整体就是一个函数式接口的实现类实例,所以可以直接调用方法。方法的功能就看实现的方法体。这里是接口引用,是借用,相当于是实现的方法体中就一句语句,语句的效果是:直接调用Demo1类中的静态方法demo1Sum给返回(这里有返回值,所以会返回)
System.out.println(myInterface1.sum(1,2));
// MyInterface1 myInterface2 = new Demo1()::demo1Sum;// 不行
}
public static int demo1Sum(Integer a,Integer b){
return a+b;
}
}
interface MyInterface1{
// long sum(int a,String b);// 这个不行,因为类型没有匹配(不需要完全匹配,能兼容就行)
long sum(int a,Integer b);// 借用方法和抽象方法能匹配就行
}
结果:
3
注意:没有要求函数式接口的抽象方法和要借用的方法完全一模一样(没有要求形参类型一样,返回值要求是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么)。要满足什么条件呢?看下面方法引用推出lambda表达式、匿名内部类的笔记就能理解了。
注意:函数式接口的抽象方法和要借用的方法参数列表要匹配才行。(没有说完全匹配,必须要参数列表的顺序一样、参数列表的类型可以兼容。返回值是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么)
注意:写完方法引用后,要看看这个方法引用的类名,是不是和函数式接口中抽象方法的第一个形参一样,并且这个类中也能找到一个成员方法,成员方法的形参和抽象方法除第一个形参外的形参列表匹配,如果能找到,那么会出现语义不明确。
2. 如果要借用的方法是一个成员方法
“实例::成员方法”
使用==“实例::成员方法”==就行。注意:this和super也是实例,只有成员方法可以用this和super,使用成员方法就必须要创建对象,那么这个成员方法执行的时候,这个this和super就是指你调用这个成员方法的对象。
对函数式接口的要求:抽象方法的形参能传给被借用方法,抽象方法的返回值能接纳借用方法的返回值,或者抽象方法的返回值是void。
例子:
package com.yimeng.mydemo;
public class Demo2 extends Demo2Parent{
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
demo2.f();
}
public void f(){
/* 问题:MyInterface2 myInterface2 = ??;这里的??可以怎么填,要求它的抽象方法可以实现把形参打印出来,并且带上前缀:“prefix类名-:”。
*
* 看题目知道当前存在可以推断出函数式接口的上下文。并且知道要实现的方法长这样:void printInt(Integer i);
* 知道这个接口的抽象方法想要实现的功能是:把形参打印出来,并且带上前缀:“prefix类名-:”。
* 看到Demo2、Demo2的父类、Demo2的祖先类中都有成员方法和我们想实现的功能是一样的。我们直接调用对应方法就能实现我们要的功能,所以我们可以用方法引用来完成。
* 怎么用呢?要借用一个成员方法,那么就要用“实例::成员方法名”,如果要借用的方法和使用方法引用的方法在一个类里面,并且都在成员方法里面(静态方法里面不能用this),那么可以用“this::成员方法名”代替“实例::成员方法名”。如果要借用的方法在他的父类或者祖先类中,那么可以用“super::成员方法名”代替“实例::成员方法名”。
**/
Demo2 demo2 = new Demo2();
// 方法引用
MyInterface2 myInterface2 = demo2::say;
myInterface2.printInt(100);
// 方法引用,使用this
MyInterface2 myInterface21 = this::say;
myInterface21.printInt(100);
// 方法引用,使用super借用父类的方法
MyInterface2 myInterface22 = super::sayParent;
myInterface22.printInt(100);
// 方法引用,使用this也借用父类的方法。因为子类会继承父类的方法
MyInterface2 myInterface222 = this::sayParent;
myInterface222.printInt(100);
// 方法引用,使用super借用祖先类的方法
MyInterface2 myInterface23 = super::sayGrandpa;
myInterface23.printInt(100);
}
public void say(Object o){
System.out.println("prefix-Demo2:"+o);
}
}
interface MyInterface2{
void printInt(Integer i);
// int printInt(Integer i);// 不行,因为返回值不匹配
}
class Demo2Parent extends Demo2Grandpa{
public void sayParent(Object o){
System.out.println("prefix-Demo2Parent:"+o);
}
}
class Demo2Grandpa{
public void sayGrandpa(Object o){
System.out.println("prefix-Demo2Grandpa:"+o);
}
}
结果:
prefix-Demo2:100
prefix-Demo2:100
prefix-Demo2Parent:100
prefix-Demo2Parent:100
prefix-Demo2Grandpa:100
注意:要求借用方法和函数式借口的抽象方法形参类型相互匹配才行。返回值要求是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么。
3. 如果要借用的方法是函数式接口抽象方法第一个参数的成员方法
“类名::成员方法”
使用==“类名::成员方法”==就行。
对函数式接口的要求:抽象方法的第一个形参类型和被被借用方法所在类一样,抽象方法除第一个形参外的其他参数能传给被借用方法的形参,抽象方法的返回值能接纳借用方法的返回值,或者抽象方法的返回值是void。(只要你抽象方法的第一个形参类型中能找到被借用方法,这里找的方法,不仅包括第一个形参类型本类中的方法,也包括第一个形参类型中从他父类中继承到的方法哈)
注意:要借用方法的参数要和函数式接口的抽象方法除第一个参数外的参数匹配(如果抽象方法除了第一个参数外没有其他参数,那么借用方法的参数就是无参的)。
例子:
package com.yimeng.mydemo;
public class Demo3 {
public static void main(String[] args) {
new Demo3().f();
}
private void f() {
/* 问题1:MyInterface3 myInterface3 = ??;这里的??可以怎么填,要求它的抽象方法可以实现打印“MyClass3的test方法被借用”并返回“say”字符串
* 问题2:MyInterface33 myInterface33 = ??;这里的??可以怎么填,要求它的抽象方法可以实现打印“MyClass33的test3方法被借用+传进去的字符串+传进去的整数”并返回“hello”字符串
*
* 看题目1知道当前环境可以推断出函数式接口的上下文是MyInterface3。并且知道要实现的方法长这样:String say(MyClass3 myClass3);
* 看题目2知道当前环境可以推断出函数式接口的上下文是MyInterface33。并且知道要实现的方法长这样:String say(MyClass33 myClass33, String s, int i);
* 知道这个MyInterface3接口的抽象方法想要实现的功能是:打印“MyClass3的test方法被借用”并返回“say”字符串。
* 看到MyInterface3接口的抽象方法say第一个参数myClass3中有成员方法和我们MyInterface3抽象方法想实现的功能是一样的,并且要借用方法的形参和抽象方法除第一个形参外的参数是匹配的,返回值也是匹配的。所以我们可以用方法引用来完成。
* 知道这个MyInterface33接口的抽象方法想要实现的功能是:打印“MyClass33的test3方法被借用+传进去的字符串+传进去的整数”并返回“hello”字符串
* 看到MyInterface33接口的抽象方法say第一个参数myClass33中有成员方法和我们MyInterface33抽象方法想实现的功能是一样的,并且要借用方法的形参和抽象方法除第一个形参外的参数是匹配的,返回值也是匹配的。所以我们可以用方法引用来完成。
* 怎么用呢?要借用抽象方法第一个参数的成员方法,那么就要用“抽象方法第一个参数类名::成员方法名”。
**/
// 抽象方法除第一个参数外,无参
MyInterface3 myInterface3 = MyClass3::test;
String say = myInterface3.say(new MyClass3());
System.out.println(say);
// 抽象方法除第一个参数外,还有参数
MyInterface33 myInterface33 = MyClass33::test3;
String hello = myInterface33.say(new MyClass33(), "hello", 1);
System.out.println(hello);
}
}
interface MyInterface3 {
String say(MyClass3 myClass3);
}
class MyClass3 {
// 下面这个注释打开就会和String test()冲突。因为MyInterface3 myInterface3 = MyClass3::test;可以通过“类名::静态方法名”找到String test(MyClass3 myClass3),也可以通过“类名::成员方法名”找到String test(),这样就语义不明确了。
// public static String test(MyClass3 myClass3) {
// System.out.println("MyClass3的test方法被借用");
// return "say";
// }
public String test() {
System.out.println("MyClass3的test方法被借用");
return "say";
}
}
interface MyInterface33 {
String say(MyClass33 myClass33, String s, int i);
}
class MyClass33 {
public String test3(String s, int i) {
System.out.println("MyClass33的test3方法被借用" + s + i);
return "hello";
}
}
结果:
MyClass3的test方法被借用
say
MyClass33的test3方法被借用hello1
hello
注意:写完方法引用后,要看看这个方法引用是不是,也能找到方法引用的类中,有没有一个静态方法,方法的形参和抽象方法形参匹配,如果能找到,那么会出现语义不明确。
4. 特别的,如果要借用的方法是一个构造方法
“类名::new”
使用==“类名::构造方法”==就行。
对函数式接口的要求:抽象方法的形参能传给被借用构造方法,抽象方法的返回值能接纳借用这个构造方法创建的对象,或者抽象方法的返回值是void。
注意:函数式接口的形参和要借用的构造方法的形参匹配。返回值要求是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么。
例子:
package com.yimeng.mydemo;
public class Demo4 {
public static void main(String[] args) {
/* 问题:MyInferface4 myInferface4 = ??;这里的??可以怎么填,要求它的抽象方法效果是创建Student对象并且把Student对象返回,并且要创建对象的时候把name也给赋值上。
*
* 看题目知道当前存在可以推断出函数式接口的上下文。并且知道要实现的方法长这样:Student createInstance(String name);
* 知道这个接口的抽象方法想要实现的功能是:是创建Student对象并且把Student对象返回,并且要创建对象的时候把name也给赋值上。
* 看到Student的Student(String name)构造方法,和我们想实现的功能是一样的。我们直接调用这个方法就能实现我们要的功能,所以我们可以用方法引用来完成。
* 怎么用呢?要借用一个构造方法,那么就要用“类名::new”。
**/
MyInferface4 myInferface4= Student::new;
myInferface4.createInstance("张三");
}
}
interface MyInferface4{
Student createInstance(String name);// 可以,相当于是调用了构造方法,并且把创建的对象给返回。
// void createInstance(String name);// 可以,相当于是单纯的调用构造方法进行执行,没有返回值。
// Object createInstance(String name);// 可以,Student是Object的子类。
// String createInstance(String name);// 不可以,其他的返回值就行不行了。
}
class Student {
private String name;
private int age;
public Student(String name) {
System.out.println("构造方法执行");
this.name = name;
}
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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
结果:
构造方法执行
5. 特别的,如果要借用数组的创建
“类型[]::new”【引用类型和基本类型都行】
注意:创建数组的这种函数式接口中的抽象方法形参只能是Integer或者int,其他都不行。long和Object都不行。
对函数式接口的要求:抽象方法的形参为int或者Integer。返回值是lambda表达式想创建的数组类型。
package com.yimeng.mydemo;
import java.util.Arrays;
public class Demo5 {
/* 问题:MyInferface5 myInferface5 = ??;这里的??可以怎么填,要求它的抽象方法效果是创建一个数组对象,创建数组元素的个数取决于是抽象方法参数的值。
*
* 看题目知道当前存在可以推断出函数式接口的上下文。并且知道要实现的方法长这样:String[] f(Integer i);
* 知道这个接口的抽象方法想要实现的功能是:创建一个数组对象,创建数组的个数取决于是抽象方法参数的值。
* 这种情况就直接用“类型[]::new”来实现即可。
**/
public static void main(String[] args) {
// 方法引用
// 引用类型
MyInferface5 myInferface5 = String[]::new;
String[] arr = myInferface5.f(2);
System.out.println(Arrays.toString(arr));
// 基本类型也行
MyInferface51 myInferface51 = int[]::new;
int[] arr2 = myInferface51.f(3);
System.out.println(Arrays.toString(arr2));
}
}
interface MyInferface5 {
String[] f(Integer i);
// String[] f(Object i);// 不行
}
interface MyInferface51 {
int[] f(int i);
// int[] f(long i);// 不行
}
结果:
[null, null]
[0, 0, 0]
注意:看到执行的结果是创建数组,并且把数组中的值设值为初始化的值。即,这种的调用创建数组的长度取决于传的实参,创建出来的数组里面的元素都是初始值。
怎么通过方法引用推断出lambda表达式和匿名内部类
其实很简单,需要下面两步:
- 通过上下文找到函数式接口和里面的抽象方法
- 找到方法引用借用的方法
1. 类名::静态方法
例子:
package com.yimeng.mydemo2;
public class Demo1 {
public static void main(String[] args) {
new Demo1().f();
}
private void f() {
// 方法引用