当前位置: 首页 > news >正文

方法引用和lambda表达式的奥妙

方法引用替代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);// 方法引用,使用thisMyInterface2 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-Demo2100
prefix-Demo2100
prefix-Demo2Parent100
prefix-Demo2Parent100
prefix-Demo2Grandpa100

注意:要求借用方法和函数式借口的抽象方法形参类型相互匹配才行。返回值要求是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么。

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;}@Overridepublic 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. 通过上下文找到函数式接口和里面的抽象方法
  2. 找到方法引用借用的方法

1. 类名::静态方法

例子:

package com.yimeng.mydemo2;public class Demo1 {public static void main(String[] args) {new Demo1().f();}private void f() {// 方法引用MyInterface1 myInterface1 = Demo1::Demo1Sum;System.out.println(myInterface1.sum(1,2));// lambda表达式
//        MyInterface1 myInterface2 = (a,b)->Demo1Sum(a,b);// 这个也行的。Demo1Sum(a,b)这个相当于是下面Demo1.Demo1Sum(a,b)的省略写法,因为这里使用Demo1Sum方法的地方和Demo1Sum方法在一个类里面嘛。所以可以省略。
//        MyInterface1 myInterface2 = (a,b)->Demo1.Demo1Sum(a,b);// 这个是lambda表达式的省略写法MyInterface1 myInterface2 = (int a,Integer b)->{return Demo1.Demo1Sum(a,b);};// 这个是lambda表达式的完整写法。System.out.println(myInterface2.sum(1,2));// 匿名内部类MyInterface1 myInterface3 = new MyInterface1() {@Overridepublic long sum(int a, Integer b) {return Demo1.Demo1Sum(a,b);}};System.out.println(myInterface3.sum(1,2));// 方法引用MyInterface11 myInterface11 = Demo1::Demo1Sum;
//        System.out.println(myInterface11.sum(1,2));// 不行,因为抽象方法 void sum(Integer a,Integer b);没有返回值。myInterface11.sum(1,2);// lambda表达式MyInterface11 myInterface12 = (Integer a,Integer b)->{Demo1.Demo1Sum(a,b);};myInterface12.sum(1,2);// 匿名内部类MyInterface11 myInterface13 = new MyInterface11() {@Overridepublic void sum(Integer a, Integer b) {Demo1.Demo1Sum(a,b);}};myInterface13.sum(1,2);}public static int Demo1Sum(Integer a,Integer b){return a+b;}// 如果下面这个注释打开,那么方法引用的就应该是下面这个方法了,而不是int Demo1Sum(Integer a,Integer b)了。因为有多个匹配的重载方法,那么就会调用最匹配的重载方法(重载方法的知识点)。
//    public static int Demo1Sum(int a,Integer b){
//        return a+b;
//    }
}
interface MyInterface1{long sum(int a,Integer b);
}interface MyInterface11{void sum(Integer a,Integer b);
}

执行结果:

3
3
3

找这个Demo2::Demo2Sum借用的方法:

上面的例子展示了方法引用——>lambda表达式——>匿名内部类的过程,看到把方法引用还原为完整的匿名内部类了,我们可以从匿名内部类看到方法引用具体的省略的细节。

这里看到细节就可以解释为什么函数式接口的抽象方法的形参是(int a,Integer b),返回值是long,可以使用int Demo2Sum(Integer a,Integer b)这个方法作为方法引用的方法了。虽然他们类型没有完全匹配,但是却没有问题。因为上面myInterface1的方法引用相当于是上面myInterface3匿名内部类的写法。long sum(int a, Integer b)中int的a可以传给int Demo2Sum(Integer a,Integer b)中Integer的a,因为自动装箱。int Demo2Sum(Integer a,Integer b)中int的返回值,可以作为long sum(int a,Integer b)的返回值,因为java类型自动转换,自动向上扩展。所以没有问题。

所以,结论是:抽象方法的如果传给被借用方法的形参是可以的,那么就行,如果被借用方法的返回值可以传给抽象方法的返回值就行。没有要求抽象方法和借用方法的形参、返回值完全一样。

我们也看到,抽象方法返回值是void,那么借用方法的返回值可以是任何东西。因为抽象方法是void的时候,使用方法引用,那么方法引用的效果相当于调用了借用的方法,但是不return借用方法的返回值。所以,你借用方法返回值是任何东西都没有关系。这就是对上面说的“返回值是,如果抽象方法如果有返回,那么借用的方法的返回类型就要能被抽象方法返回值所接收就行,如果抽象方法没有返回值,那么随便借用方法返回什么”的解释。

2. 实例::成员方法

package com.yimeng.mydemo2;public class Demo2 extends Demo2Parent {public static void main(String[] args) {Demo2 demo2 = new Demo2();demo2.f();}public void f(){Demo2 demo2 = new Demo2();// 方法引用MyInterface2 myInterface2 = demo2::say;myInterface2.printInt(100);// 方法引用,使用this(和上面的demo2::say效果一样,但是上面这种做法需要多创建一个Demo2对象,下面这种不需要再创建一个Demo2对象了。相当于是直接用main中创建的Demo2对象,this嘛,就是指当前对象,即指调用这个f()方法的对象。)MyInterface2 myInterface21 = this::say;myInterface21.printInt(100);// 转换为lambda表达式和匿名内部类。MyInterface2 myInterface211 = (Integer i)->{this.say(i);// lambda表达式和方法引用中使用的this就直接指向外部类};myInterface211.printInt(100);MyInterface2 myInterface2111 = new MyInterface2() {@Overridepublic void printInt(Integer i) {Demo2.this.say(i);// 匿名内部类中的this是指向匿名内部类的,所以要用Demo2.this才能指向外部类}};myInterface2111.printInt(100);// 方法引用,使用super借用父类的方法MyInterface2 myInterface22 = super::sayParent;myInterface22.printInt(100);MyInterface2 myInterface221 = (Integer i)->{super.sayParent(i);};myInterface221.printInt(100);MyInterface2 myInterface2211 = new MyInterface2() {@Overridepublic void printInt(Integer i) {Demo2.super.sayParent(i);// 和上面的this一样,指向内部类。}};myInterface2211.printInt(100);// 方法引用,使用super借用祖先类的方法MyInterface2 myInterface23 = super::sayGrandpa;myInterface23.printInt(100);MyInterface2 myInterface231 = (Integer i)->{super.sayGrandpa(i);};myInterface231.printInt(100);MyInterface2 myInterface2311 =new MyInterface2() {@Overridepublic void printInt(Integer i) {Demo2.super.sayGrandpa(i);// 和上面的this一样,指向内部类。}};myInterface2311.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-Demo2100
prefix-Demo2100
prefix-Demo2100
prefix-Demo2100
prefix-Demo2Parent100
prefix-Demo2Parent100
prefix-Demo2Parent100
prefix-Demo2Grandpa100
prefix-Demo2Grandpa100
prefix-Demo2Grandpa100

找这个demo2::say;借用的方法:先看demo2::say,看到是“实例::XX”,所以就确定是找一个方法名为say的成员方法了。然后看上下文确定函数式接口和抽象方法,我们找到这个抽象方法的形参,用这个形参去对比demo2中say的成员方法,找到一个say成员方法的形参能和void printInt(Integer i);匹配的,就是那个方法了,如果有多个匹配的,那么就找最匹配的。所以找到了void say(Object o)。

注意:匿名内部类中的this是指向匿名内部类的(指向最内层的类),所以要用“外部类.this”才能指向外部类。但是对于lambda表达式和方法引用中使用的this就直接指向外部类了(准确的说是指向lambda表达式和方法引用所在的最内层类)。原因可能是:编译器先去处理this,把this替换为对应的对象引用,然后再去把lambda表达式和方法引用转为匿名内部类的。super也是同理,在匿名内部类内super是指最内层类的,需要用“外部类.super”指向外部类,但是lambda表达式和方法引用中的super是指向外部类的(准确的说是指向lambda表达式和方法引用所在的最内层类)。

3. 类名::成员方法

package com.yimeng.mydemo2;public class Demo3 {public static void main(String[] args) {new Demo3().f();}private void f() {// 抽象方法除第一个参数外,无参MyInterface3 myInterface3 = MyClass3::test;String say = myInterface3.say(new MyClass3());System.out.println(say);// 改为lambda表达式MyInterface3 myInterface31 = (myClass3) -> myClass3.test();String say1 = myInterface31.say(new MyClass3());System.out.println(say1);// 改为匿名内部类MyInterface3 myInterface32 = new MyInterface3() {@Overridepublic String say(MyClass3 myClass3) {return myClass3.test();}};String say2 = myInterface32.say(new MyClass3());System.out.println(say2);// 抽象方法除第一个参数外,还有参数MyInterface33 myInterface33 = MyClass33::test3;String hello = myInterface33.say(new MyClass33(), "hello", 1);System.out.println(hello);// 改为lambda表达式MyInterface33 myInterface333 = (myClass33, s, i) -> myClass33.test3(s, i);String hello1 = myInterface333.say(new MyClass33(), "hello", 1);System.out.println(hello1);// 改为匿名内部类MyInterface33 myInterface334 = new MyInterface33() {@Overridepublic String say(MyClass33 myClass33, String s, int i) {return myClass33.test3(s, i);}};String hello2 = myInterface334.say(new MyClass33(), "hello", 1);System.out.println(hello2);}
}interface MyInterface3 {String say(MyClass3 myClass3);
}class MyClass3 {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
MyClass3的test方法被借用
say
MyClass3的test方法被借用
say
MyClass33的test3方法被借用hello1
hello
MyClass33的test3方法被借用hello1
hello
MyClass33的test3方法被借用hello1
hello

找MyClass3::test借用的方法:先看MyClass3的test方法,找到成员方法或者静态方法都行。然后我们看上下文推断出函数式接口,并找到抽象方法。看抽象方法的参数,如果是第一个参数和方法引用借用的类不一样,那么,我们应该找静态方法,并且找的方法和抽象方法的形参匹配才行。如果抽象方法的第一个参数类型和方法引用借用的类是一样的,那么有两种可能,可能找静态方法,也可能找成员方法,找静态方法要求找静态方法的形参与抽象方法的形参匹配的静态方法,找成员方法,就找方法形参和抽象方法除第一个参数外的参数匹配的成员方法。如果找静态方法找到了某个可能借用的方法,找成员方法也找到了某个可能借用的方法,那么这种情况下,idea会提示报错的,因为编译器通过方法引用去匹配借用的方法的时候遇到了语义不明确的问题了。

注意:“类名::成员方法”可能和“类名::静态方法”形成语义不明确

比如:

package com.yimeng.mydemo;public class MyDemo {public static void main(String[] args) {new MyDemo().f();}private void f() {
//        MyDemoInterface myDemoInterface = MyDemoClass::test;// 注释打开会语义不明确// 可能会语义不明确的情况下,使用lambda表达式来做或者匿名内部类来做就行了。减少省略就行了。// 使用lambda表达式// 使用成员方法MyDemoInterface myDemoInterface1 = (myDemoClass, s, i) -> myDemoClass.test(s, i);// 使用静态方法MyDemoInterface myDemoInterface2 = (myDemoClass, s, i) -> MyDemoClass.test(myDemoClass, s, i);// 使用匿名内部类// 使用成员方法MyDemoInterface myDemoInterface11 = new MyDemoInterface() {@Overridepublic String say(MyDemoClass myDemoClass, String s, int i) {return MyDemoClass.test(myDemoClass, s, i);}};// 使用静态方法MyDemoInterface myDemoInterface22 = new MyDemoInterface() {@Overridepublic String say(MyDemoClass myDemoClass, String s, int i) {return myDemoClass.test(s, i);}};}
}
interface MyDemoInterface {String say(MyDemoClass myDemoClass, String s, int i);
}
class MyDemoClass {public String test(String s, int i) {System.out.println("MyDemoClass的test(String s, int i)方法被借用" + s + i);return "hello";}public static String test(MyDemoClass myDemoClass,String s, int i) {System.out.println("MyDemoClass的test(MyDemoClass myDemoClass,String s, int i)方法被借用" + s + i);return "hello";}
}

如果把上面的注释打开会语义不明确。

原因:我们通过MyDemoClass::test知道是找MyDemoClass类的test方法,我们找到了两个方法String test(String s, int i)和String test(MyDemoClass myDemoClass,String s, int i)。我们看函数式接口的抽象方法的形参来选择这些重载方法。选择的时候,会看抽象方法的第一个参数是不是方法引用的要借用的方法的类。如果不是,那么只会找静态方法,并且找的静态方法参数要和抽象方法匹配。如果是(即,抽象方法第一个参数类型和要借用的方法所在的类一样),那么可能找参数匹配的静态方法,也可能找成员方法,找成员方法的时候要求成员方法的形参要和抽象方法除第一个参数外的其他参数组成参数列表匹配。上面的例子里面,属于是抽象方法的第一个参数是我们要借用方法的类,所以可能找成员方法,也可能找静态方法。结果发现,找参数列表匹配成员方法,找到了String test(String s, int i),找参数列表和抽象方法除第一个形参外的其他参数组成的参数列表匹配的静态方法,找到了String test(MyDemoClass myDemoClass,String s, int i),这样就导致了编译器不知道借用哪一个方法了,所以就语义不明确了。

这种可能会产生模糊的情况,最好还是用lambda表达式或者匿名内部类来写,这样省略的东西就少了,不用编译器自己推断了,可以避免任何混淆或可能的错误。

4. 类名::new

这个方法引用还原起来很简单,就是找函数式表达式的抽象方法,知道形参。然后找引用的类的构造方法,找到形参和抽象方法形参匹配的构造方法,方法引用就是调用这个构造方法。为什么是找形参和抽象方法形参匹配的构造方法,你看方法引用还原为匿名内部类的代码就知道了。只要你抽象方法的参数可以全部放到构造方法中进行调用就算匹配了。

package com.yimeng.mydemo2;public class Demo4 {public static void main(String[] args) {// 方法引用MyInferface4 myInferface4= Student::new;myInferface4.createInstance("张三");// lambda表达式MyInferface4 myInferface41= (String name)->new Student(name);myInferface41.createInstance("李四");// 匿名内部类MyInferface4 myInferface42= new MyInferface4() {@Overridepublic Student createInstance(String name) {return new Student(name);}};myInferface42.createInstance("王五");}
}
interface MyInferface4{Student 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;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}

结果:

构造方法执行
构造方法执行
构造方法执行

5. 类型[]::new

这个方法引用还原起来也是很简单的,这个就拿来纯记忆就行了,就认为“类型[]::new”是等价与抽象方法的实现是“【return】new 类型[抽象方法形参对应的变量];”就行了,至于有没有return,就单纯看抽象方法是不是void就行。

package com.yimeng.mydemo2;import java.util.Arrays;public class Demo5 {public static void main(String[] args) {// 方法引用// 引用类型MyInferface5 myInferface5 = String[]::new;String[] arr = myInferface5.f(2);System.out.println(Arrays.toString(arr));// lambda表达式MyInferface5 myInferface52 = (i) -> new String[i];System.out.println(Arrays.toString(myInferface52.f(3)));// 匿名内部类MyInferface5 myInferface53 = new MyInferface5() {@Overridepublic String[] f(Integer i) {return new String[i];}};System.out.println(Arrays.toString(myInferface53.f(3)));}
}interface MyInferface5 {String[] f(Integer i);
}

结果:

[null, null]
[null, null, null]
[null, null, null]

回顾一下创建数组的知识点

创建数组的几种方式:

摘录自:【Java-数组】Java数组的创建的3种方法6种写法_java创建数组的正确语句-CSDN博客

一维数组

动态创建(4种写法)

第1种:

int a[] = new int[5];//创建长度为5的一维数组等待赋值,初始值为0

第2种:

int[] a1 = new int[5];//创建长度为5的一维数组等待赋值,初始值为0

第3种:

int b[] = new int[] {1,2,3};//声明并创建内存空间且赋值

第4种:

int[] b1 = new  int[] {1,2,3};//声明并创建内存空间且赋值

静态创建(2种写法)

第1种:

int c[] = {1,2,3};//声明并创建内存空间,直接赋值

第2种:

int[] c1 = {1,2,3};//声明并创建内存空间,直接赋值

二维数组

动态创建(4种写法)

第1种:

int[][] arr1 = new int[3][3];//声明并创建内存空间,等待赋值,初始值为0

第2种:

int arr2[][] = new int[3][3];//声明并创建内存空间,等待赋值,初始值为0

第3种:

int[][] arr1 = new int[][] {{1,2,3},{4,5,6},{7,8,9}};//声明并创建内存空间且赋值

第4种:

int arr2[][] = new int[][] {{1,2,3},{4,5,6},{7,8,9}};//声明并创建内

静态创建(2种写法)

第1种:

int[][] arr1 = {{1,2,3},{4,5,6},{7,8,9}};//声明并创建内存空间,直接赋值

第2种:

int arr2[][] = {{1,2,3},{4,5,6},{7,8,9}};//声明并创建内存空间,直接赋值

总结

怎么用方法引用

  1. 抽象方法的实现方法效果是直接借用某个类的静态方法,需要满足的条件:静态方法的形参与函数式接口的抽象方法形参匹配,那么可以直接用“类名::静态方法”。注意语义不明确问题,容易和“抽象方法第一个参数的类名::成员方法”冲突。(需要完全一样吗?答:不需要完全一样。是不是只要抽象方法的返回值能接受借用方法的返回值,并且抽象方法的形参能传给借用方法的形参就行了?答,是的)
  2. 抽象方法的实现方法效果是直接借用某个实例的成员方法,需要满足的条件是:成员方法的形参和返回值与函数式接口的抽象方法匹配,那么可以直接用“实例::成员方法”(这里还包括“this::方法名”、“super::方法名”)
  3. 抽象方法的实现方法效果是调用抽象方法第一个形参的成员方法,需要满足的条件是:存在函数式接口的上下文,要调用的方法形参和抽象方法除第一个形参外的其他形参组成的参数列表匹配,那么可以使用“抽象方法第一个参数的类名::成员方法”。注意语义不明确问题,容易和“类名::静态方法”冲突。
  4. 抽象方法的实现方法想要的效果时调用某个类的构造方法,需要满足的条件是:存在函数式接口的上下文,抽象方法需要的形参和某个构造方法的形参匹配,那么可以用“类名::new”。
  5. 抽象方法的实现方法想要的效果是创建数组,需要满足的条件是:存在函数式接口的上下文,抽象方法形参是int或者Integer,那么可以用使用”类型[]::new“。创建出来的数组对象的长度是调用抽象方法时传的实参确定的,创建出来的数组中元素的值是初始化的值。

怎么看方法引用

先看上下文,然后推断出抽象方法,再去看方法引用所找的方法。

  1. 如果是“实例::XXX”就找这个实例变量的类中找成员方法。如果是“this::XXX”就找本类中的成员方法,当然哈,父类祖先类继承下来的成员方法也可以找到。如果是“super::XXX”就找父类和父类继承(祖先类)的成员方法。静态方法不会找。
  2. 如果是”类名::new“,那么就找这个类名的构造方法,构造方法的形参要和抽象方法的形参兼容。
  3. 如果是”类型[]::new“,那么调用函数式接口的抽象方法就是返回一个你指定类型的数组,调用的实参决定你创建数组元素的长度,创建后数组中元素的值都是初始化的值。
  4. 如果是”类名::XX“,就要看看抽象方法的形参第一个参数是不是方法引用的类了。
    • 如果不是这个类型,那么就去找这个类名中XX方法名对应的所有静态方法,然后找到形参和抽象方法兼容的方法就是要借用的方法了。(父类和祖先类中的方法要不要找?答:会找)
    • 如果抽象第一个参数和方法引用的类一样,那么要找这个类名中对应的所有静态方法(父类和祖先类的静态方法也会找),如果静态方法的形参和抽象方法的形参兼容,那么就找到了,作为备选方法A。但是还是要继续,因为这种情况还可能是借用成员方法。我们继续找成员方法,要找的形参和抽象方法除第一个形参外的其他形参组成的参数列表都兼容的成员方法,作为备选方法B。如果A和B都找到了,那么编译不会过,因为语义不明确。

例子(看到可以继承父类的静态方法的):

package com.yimeng.mydemo;public class Demox1 extends Demox{public static void main(String[] args) {new Demox1().f();}private void f() {int i = Demox1.xSum(2, 4);System.out.println(i);MyInterface1 myInterface1 = Demox1::xSum;System.out.println(myInterface1.sum(1,2));MyInterface1 myInterface2 = Demox1::ySum;System.out.println(myInterface2.sum(4,5));}
}
interface MyInterfacex1{long sum(int a,Integer b);
}
class Demox extends Demoy{public static int xSum(Integer a,Integer b){return a+b;}
}class Demoy{public static int ySum(Integer a,Integer b){return a+b;}
}

结果:

6
3
9

补充:函数式接口

函数式接口:

  1. 如果一个接口有且仅有一个抽象方法(非抽象方法无论有多少个都行),那么该接口就是一个函数式接口。
  2. 如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口。效果是什么呢?比如我们在加上@FunctionalInterface注解的接口中写了两个抽象方法,程序编译就会报错的,相当于给我们程序员的一个提醒,一般你加了这个注解,就想要借这个接口来使用lambda表达式或者方法引用,要使用方法引用或者lambda表达式,就要求接口一定只有一个抽象方法,所以我们确定某个地方一定借这个接口来使用lambda表达式或者方法引用的话,我们在这个接口上面加上这个注解,那么我们就必须在接口中写一个抽象方法了,这样就不会有问题了,相当于给自己加上一个检查机制。没有加这个注解的接口有且仅有一个抽象方法的话,那么这个接口也是函数式接口。只是加上这个注解编译器就会自动进行检测,相当于你告诉编译器某个接口要符合函数式接口的定义,如果不符合就提前告知你,是把错误前置的一种做法。
  3. 函数式接口一般都是为lambda表达式的使用而存在的。当然哈,你不想用lambda表达式也可以写函数式接口哈。

@FunctionalInterface的例子:

package com.yimeng;// 函数式接口只能有一个抽象方法
@FunctionalInterface
public interface FuncTest {void accept(Object o);
}

如果在函数式接口中写两个以上的方法,编译会报错:

一文带你搞懂Lambda表达式_java_02

我们来自己写一个实际的例子了解下函数式接口的使用:

package com.yimeng;// 函数式接口只能有一个抽象方法
// 1.写一个对传入参数进行操作的函数式接口
@FunctionalInterface
public interface FuncTest {Integer operation(Integer x);
}
class Main{public static void main(String[] args) {// 3.借用函数式接口写lambda表达式System.out.println(operate(1, (x) -> x + x));  // 输出 2}// 2.写一个方法,将函数式接口作为参数private static Integer operate(Integer a, FuncTest funcTest) {return funcTest.operation(a);}
}

Java8内置的函数式接口

常见的4种函数式接口

我们想使用lambda表达式或者方法引用就需要一个函数式接口,所以上面的很多例子中,我们都自己创建了函数式接口,然后在利用这个函数式接口构建出上下文环境,再使用lambda表达式。但是其实,java内其实已经给我们定义了一些比较常见比较通用的函数式接口了,你可以直接用的,避免了你重新定义了。

四种常用的函数式接口:

  • Consumer :消费型接口

    方法:void accept(T t);

    Consumer<String> consumer = x -> System.out.println(x);// 内部类处理是打印实参
    consumer.accept("Hello");  // 打印Hello
    
  • Supplier :供给型接口

    方法:T get();

    Supplier<MyTest> supplier =  () -> new MyTest();// 内部类处理是创建MyTest对象并返回
    System.out.println(supplier.get());  // 打印com.yimeng.MyTest@7cca494b
    
  • Function<T, R> :函数型接口

    方法:R apply(T t);

    Function<String, Integer> function = x -> x.length();// 内部处理是拿到字符串的长度
    System.out.println(function.apply("Hello"));  // 打印5
    
  • Predicate :断言型接口

    方法:boolean test(T t);

    Predicate<String> predicate = x -> x.isEmpty();// 内部处理是判断是否为空
    System.out.println(predicate.test("Hello"));  // 打印false
    

例子:

package com.yimeng;import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;public class MyTest {public static void main(String[] args) {// 1. 消费型接口,方法:void accept(T t);// 适合:实际调用的时候传一个参数,然后交给内部处理,结果不返回的情况。// lambda表达式Consumer<String> consumer = x -> System.out.println(x);// 内部类处理是打印实参consumer.accept("Hello");  // 打印Hello// 方法引用Consumer<String> consumer2 = System.out::println;consumer2.accept("Hello");  // 打印Hello// 2. 供给型接口,方法:T get();// 适合:实际调用的时候不需要传参数,然后内部处理后,直接返回处理结果的情况。// lambda表达式Supplier<MyTest> supplier =  () -> new MyTest();// 内部类处理是创建MyTest对象并返回System.out.println(supplier.get());  // 打印com.yimeng.MyTest@7cca494b// 方法引用Supplier<MyTest> supplier2 = MyTest::new;System.out.println(supplier2.get());  // 打印com.yimeng.MyTest@7699a589// 3. 函数型接口,方法:R apply(T t);// 适合:实际调用的时候传一个参数,然后交给内部处理,直接返回处理结果的情况。// lambda表达式Function<String, Integer> function = x -> x.length();// 内部处理是拿到字符串的长度System.out.println(function.apply("Hello"));  // 打印5// 方法引用Function<String, Integer> function2 = String::length;System.out.println(function2.apply("Hello"));  // 打印5// 4. 断言型接口,方法:boolean test(T t);// 适合:实际调用的时候传一个参数,然后交给内部处理,结果返回boolean类型的情况。// lambda表达式Predicate<String> predicate = x -> x.isEmpty();// 内部处理是判断是否为空System.out.println(predicate.test("Hello"));  // 打印false// 方法引用Predicate<String> predicate2 = String::isEmpty;System.out.println(predicate2.test("Hello"));  // 打印false}
}

执行结果:

Hello
Hello
com.yimeng.MyTest@7cca494b
com.yimeng.MyTest@7699a589
5
5
false
false

如果上面四个不够用,那么可以使用下面Java.util.function其他一些函数式接口。

其他函数式接口

Java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:

一文带你搞懂Lambda表达式_编程语言_03

怎么用起来lambda表达式和方法引用

从我们开发者来思考怎么把lambda表达式用起来。

比如这样一个案例:我们开发的时候经常会操作数据库,遇到批量更新或者批量插入的情况,我们如果在for循环里面一个个插入或者更新,这样的效率很低下,原因是IO次数太多了。所以我们想做到把要批量执行的sql一次给到数据库,然后批量执行,这样就减少了IO。做法也很简单,就是利用JDBC的flushStatements方法来完成,我们把插入或者更新语句放到一个缓存里面去,到一定数量的时候,批量拿到的数据库去执行就行了。但是这里的问题是,如果我们每一个想批量插入或者更新的语句都这样写,操作很麻烦,所以,我们想写一个通用的方法。但是通用的方法,不同mapper要执行的方法是不同的,执行的逻辑是不同的,那么怎么来做到通用性呢?其实这里就可以用函数式接口和lambda表达式来解决。

我们定义了一个方法batchUpdateOrInsert,第一个参数是要操作的数据List,使用泛型可以更加具有通用性。第二个参数是,一个函数式接口,里面定义一个方法,方法的作用是插入或者更新单个数据。具体怎么操作,看这个接口的实现就行了,反正我们只要指定这个抽象方法的效果是插入或者更新单个数据就行,这个方法需要一个参数,参数是要更新或者插入的单个数据对象,抽象方法的返回值是插入和更新影响的行数。

具体的实现,我们可以用lambda表达式或者方法引用来写的。

package com.yimeng;import java.util.Arrays;
import java.util.List;public class MybatisBatchUtils {/*** 每次处理1000条。*/private static final int BATCH_SIZE = 1000;/*** 批量处理修改或者插入** @param data 需要批量被查找和插入的数据* @param processDatabase 自定义处理逻辑* @return int 影响的总行数*/public <T> int batchUpdateOrInsert(List<T> data,ProcessDatabase<T> processDatabase) {int i = 1;System.out.println("开启事务,并拿到SqlSession");try {// 拿到数据的总数int size = data.size();// 分批处理for (T element : data) {// 想要借用一个参数的方法。processDatabase.insertOrUpdate(element);if ((i % BATCH_SIZE == 0) || i == size) {// 可以减少数据库的IO操作System.out.println("批量刷新缓存,一次执行到数据库");}i++;}System.out.println("提交事务");} catch (Exception e) {System.out.println("回滚事务");} finally {System.out.println("关闭SqlSession");}return i - 1;}
}// 执行
class AccessingTheDB{public static void main(String[] args) {MybatisBatchUtils mybatisBatchUtils=new MybatisBatchUtils();UserMapper userMapper=new UserMapper();List<String> userList= Arrays.asList("user1","user2","user3");mybatisBatchUtils.batchUpdateOrInsert(userList,(user)->userMapper.insertUser(user));mybatisBatchUtils.batchUpdateOrInsert(userList, userMapper::updateUser);}
}
// 具体的操作
class UserMapper{public int insertUser(String user){System.out.println("insert user: " + user);return 1;}public int updateUser(String user){System.out.println("update user: " + user);return 1;}
}
// 操作数据库
interface ProcessDatabase<T>{// 插入或者更新数据库,参数是要插入或者更新的数据对象,返回值是影响的行数int insertOrUpdate(T data);
}

执行结果:

开启事务,并拿到SqlSession
insert user: user1
insert user: user2
insert user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession
开启事务,并拿到SqlSession
update user: user1
update user: user2
update user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession

上面这里的函数式接口可以使用java内置的函数式接口来替代。

我们找一找有没有存在只有一个形参的,返回值是int的抽象方法的函数式接口:

image-20241214112454623

找到了,我们现在进行改造:

package com.yimeng;import java.util.Arrays;
import java.util.List;
import java.util.function.ToIntFunction;public class MybatisBatchUtils {/*** 每次处理1000条。*/private static final int BATCH_SIZE = 1000;/*** 批量处理修改或者插入** @param data 需要批量被查找和插入的数据* @param processDatabase 自定义处理逻辑* @return int 影响的总行数*/
//    public <T> int batchUpdateOrInsert(List<T> data,ProcessDatabase<T> processDatabase) {public <T> int batchUpdateOrInsert(List<T> data, ToIntFunction<T> processDatabase) {int i = 1;System.out.println("开启事务,并拿到SqlSession");try {// 拿到数据的总数int size = data.size();// 分批处理for (T element : data) {// 想要借用一个参数的方法。
//                processDatabase.insertOrUpdate(element);processDatabase.applyAsInt(element);if ((i % BATCH_SIZE == 0) || i == size) {// 可以减少数据库的IO操作System.out.println("批量刷新缓存,一次执行到数据库");}i++;}System.out.println("提交事务");} catch (Exception e) {System.out.println("回滚事务");} finally {System.out.println("关闭SqlSession");}return i - 1;}
}// 执行
class AccessingTheDB{public static void main(String[] args) {MybatisBatchUtils mybatisBatchUtils=new MybatisBatchUtils();UserMapper userMapper=new UserMapper();List<String> userList= Arrays.asList("user1","user2","user3");mybatisBatchUtils.batchUpdateOrInsert(userList,(user)->userMapper.insertUser(user));mybatisBatchUtils.batchUpdateOrInsert(userList, userMapper::updateUser);}
}
// 具体的操作
class UserMapper{public int insertUser(String user){System.out.println("insert user: " + user);return 1;}public int updateUser(String user){System.out.println("update user: " + user);return 1;}
}
 操作数据库
//interface ProcessDatabase<T>{
//    // 插入或者更新数据库,参数是要插入或者更新的数据对象,返回值是影响的行数
//    int insertOrUpdate(T data);
//}

执行结果:

开启事务,并拿到SqlSession
insert user: user1
insert user: user2
insert user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession
开启事务,并拿到SqlSession
update user: user1
update user: user2
update user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession

看到效果一样!

抽象方法的设计还是比较多样的,看你想怎么用吧。

比如你也可以这样:

package com.yimeng;import java.util.Arrays;
import java.util.List;public class MybatisBatchUtils {/*** 每次处理1000条。*/private static final int BATCH_SIZE = 1000;/*** 批量处理修改或者插入** @param data 需要批量被查找和插入的数据* @param mapper 执行的mapper* @param processDatabase 自定义处理逻辑* @return int 影响的总行数*/public <T,U> int batchUpdateOrInsert(List<T> data,U mapper,ProcessDatabase<U,T> processDatabase) {int i = 1;System.out.println("开启事务,并拿到SqlSession");try {// 拿到数据的总数int size = data.size();// 分批处理for (T element : data) {// 想要借用一个参数的方法。processDatabase.handle(mapper,element);if ((i % BATCH_SIZE == 0) || i == size) {// 可以减少数据库的IO操作System.out.println("批量刷新缓存,一次执行到数据库");}i++;}System.out.println("提交事务");} catch (Exception e) {System.out.println("回滚事务");} finally {System.out.println("关闭SqlSession");}return i - 1;}
}// 执行
class AccessingTheDB{public static void main(String[] args) {MybatisBatchUtils mybatisBatchUtils=new MybatisBatchUtils();UserMapper userMapper=new UserMapper();List<String> userList= Arrays.asList("user1","user2","user3");mybatisBatchUtils.batchUpdateOrInsert(userList,userMapper,(u,user)->u.insertUser(user));mybatisBatchUtils.batchUpdateOrInsert(userList,userMapper,(u,user)->u.updateUser(user));}
}
class UserMapper{public int insertUser(String user){System.out.println("insert user: " + user);return 1;}public int updateUser(String user){System.out.println("update user: " + user);return 1;}
}
interface ProcessDatabase<T,U>{// 调用mapper的方法,date作为参数,返回值是影响行数int handle(T date,U mapper);
}

执行结果:

开启事务,并拿到SqlSession
insert user: user1
insert user: user2
insert user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession
开启事务,并拿到SqlSession
update user: user1
update user: user2
update user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession

使用内置的函数式接口:

image-20241213235415076

package com.yimeng;import java.util.Arrays;
import java.util.List;
import java.util.function.ToIntBiFunction;public class MybatisBatchUtils {/*** 每次处理1000条。*/private static final int BATCH_SIZE = 1000;/*** 批量处理修改或者插入** @param data 需要批量被查找和插入的数据* @param mapper 执行的mapper* @param processDatabase 自定义处理逻辑* @return int 影响的总行数*/
//    public <T,U> int batchUpdateOrInsert(List<T> data,U mapper,ProcessDatabase<U,T> processDatabase) {public <T,U> int batchUpdateOrInsert(List<T> data, U mapper, ToIntBiFunction<U,T> processDatabase) {int i = 1;System.out.println("开启事务,并拿到SqlSession");try {// 拿到数据的总数int size = data.size();// 分批处理for (T element : data) {// 想要借用一个参数的方法。
//                processDatabase.handle(mapper,element);processDatabase.applyAsInt(mapper,element);if ((i % BATCH_SIZE == 0) || i == size) {// 可以减少数据库的IO操作System.out.println("批量刷新缓存,一次执行到数据库");}i++;}System.out.println("提交事务");} catch (Exception e) {System.out.println("回滚事务");} finally {System.out.println("关闭SqlSession");}return i - 1;}
}// 执行
class AccessingTheDB{public static void main(String[] args) {MybatisBatchUtils mybatisBatchUtils=new MybatisBatchUtils();UserMapper userMapper=new UserMapper();List<String> userList= Arrays.asList("user1","user2","user3");mybatisBatchUtils.batchUpdateOrInsert(userList,userMapper,(u,user)->u.insertUser(user));mybatisBatchUtils.batchUpdateOrInsert(userList,userMapper,(u,user)->u.updateUser(user));}
}
class UserMapper{public int insertUser(String user){System.out.println("insert user: " + user);return 1;}public int updateUser(String user){System.out.println("update user: " + user);return 1;}
}
//interface ProcessDatabase<T,U>{
//    // 调用mapper的方法,date作为参数,返回值是影响行数
//    int handle(T date,U mapper);
//}

执行结果:

开启事务,并拿到SqlSession
insert user: user1
insert user: user2
insert user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession
开启事务,并拿到SqlSession
update user: user1
update user: user2
update user: user3
批量刷新缓存,一次执行到数据库
提交事务
关闭SqlSession

看到使用函数式接口的好处其实主要是,在定义通用方法的时候会用。效果就是,我们知道要做的操作是什么功能,操作要接收的参数和返回值是什么,暂时不用管具体是怎么实现的,我们写通用方法具体逻辑的时候就当这个抽象方法已经按我们想的方式来实现好了,我们直接按照我们想的这个抽象方法的功能来用这个抽象方法把通用方法的逻辑写好就行了。然后就是使用阶段了,使用时我们实际去实现这个抽象方法,就按我们具体的逻辑去写就行,想怎么填充就怎么填充,有很大的灵活性,但是注意,你实现这个抽象方法的时候,要按照我们当时设计这个抽象方法的功能的模样去实现,比如需要的参数是什么,返回值是什么,方法的功能是什么都是已经想象好的,你的实现不能脱离当时设计通用方法时对这个抽象方法的设想。

下面再来做一个例子:

平时业务中,我们经常有创建对象,并给对象赋值的操作。一般,我们都类似User user=new User();user.setName(“张三”);user.setAge(12);这样操作。但是这样比较麻烦,我们就想能不能写一个工具类,使用链式编程,并且可以使用lambda表达式来创建对象。

代码:

package com.yimeng;import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;public class Builder<T> {private final Supplier<T> instantiator;private List<Consumer<T>> modifiers = new ArrayList<>();public Builder(Supplier<T> instantiator) {this.instantiator = instantiator;}public static <T> Builder<T> of(Supplier<T> instantiator) {return new Builder<>(instantiator);}public <P1> Builder<T> with(Consumer1<T, P1> consumer, P1 p1) {Consumer<T> c = instance -> consumer.accept(instance, p1);modifiers.add(c);return this;}public <P1, P2> Builder<T> with(Consumer2<T, P1, P2> consumer, P1 p1, P2 p2) {Consumer<T> c = instance -> consumer.accept(instance, p1, p2);modifiers.add(c);return this;}public <P1, P2, P3> Builder<T> with(Consumer3<T, P1, P2, P3> consumer, P1 p1, P2 p2, P3 p3) {Consumer<T> c = instance -> consumer.accept(instance, p1, p2, p3);modifiers.add(c);return this;}public T build() {T value = instantiator.get();modifiers.forEach(modifier -> modifier.accept(value));modifiers.clear();return value;}/*** 1 参数 Consumer*/@FunctionalInterfacepublic interface Consumer1<T, P1> {void accept(T t, P1 p1);}/*** 2 参数 Consumer*/@FunctionalInterfacepublic interface Consumer2<T, P1, P2> {void accept(T t, P1 p1, P2 p2);}/*** 3 参数 Consumer*/@FunctionalInterfacepublic interface Consumer3<T, P1, P2, P3> {void accept(T t, P1 p1, P2 p2, P3 p3);}
}
class User{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;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +'}';}
}
class MyMain{public static void main(String[] args) {// 使用老的做法User zhangsan=new User();zhangsan.setName("张三");zhangsan.setAge(12);System.out.println(zhangsan);// User{name='张三', age=12}// lambda表达式的做法User lisi = Builder.of(() -> new User()).with((u, name) -> u.setName(name), "李四").with((u, age) -> u.setAge(age), 12).build();System.out.println(lisi);// User{name='李四', age=12}// 方法引用User wangwu = Builder.of(User::new).with(User::setName, "王五").with(User::setAge, 12).build();System.out.println(wangwu);// User{name='王五', age=12}User zhaoliu = new Builder<User>(User::new).with(User::setName, "赵六").with(User::setAge, 12).build();System.out.println(zhaoliu);// User{name='赵六', age=12}}
}

结果:

User{name='张三', age=12}
User{name='李四', age=12}
User{name='王五', age=12}
User{name='赵六', age=12}

分析下面这个语句执行为什么能返回一个User,并且返回时还设置了对应的属性值:

User lisi = Builder.of(() -> new User()).with((u, name) -> u.setName(name), "李四").with((u, age) -> u.setAge(age), 12).build();
System.out.println(lisi);// User{name='李四', age=12}
  1. Builder.of(() -> new User())是调用了of方法,of方法又调用了new Builder<>(instantiator),创建Builder对象。
  2. 创建对象的时候,把这个() -> new User()实例给instantiator成员变量了。通过分析这个lambda表达式知道,我们之后要是调用instantiator的抽象方法get()【Supplier的抽象方法是T get();】,就会创建就是相当于执行了new User();
  3. .with((u, name) -> u.setName(name), “李四”)。因为Builder.of(() -> new User())返回的是Builder对象,所以可以调用with方法。调用with方法,看到调用的第一个参数是lambda表达式,所以知道,(u, name) -> u.setName(name)是一个Consumer1类型的对象,并且调用这个对象的accept方法【void accept(T t, P1 p1);是Consumer1的抽象方法】就是相当于是执行“accept第一个参数的setName(accept第二个参数)”。with方法第二个参数是字符串“李四”,所以p1是“李四”。所以consumer.accept(instance, p1)的效果是,相当于调用“instance.setName(“李四”)”。所以Consumer c = instance -> consumer.accept(instance, p1);就是说,之后调用c的accept方法【void accept(T t);是Consumer的抽象方法】,就是调用“accept传的参数的setName(“李四”)”。with中最后把c放到了List<Consumer> modifiers中。
  4. .with((u, age) -> u.setAge(age), 12)也是和上面的3点类似,不分析了。相当于是之后调用c的accept方法【void accept(T t);是Consumer的抽象方法】,就是调用“accept传的参数的setAge(12)”。区别是,3点中,创建的c对象是modifiers的第一个元素,而4点中创建的c对象【3创建的c对象和4创建的c对象不是一个对象哈,只是局部变量叫c而已】是modifiers的第二个元素。
  5. 最后执行builder方法,先执行T value = instantiator.get();返回的是一个T,根据1点,我们知道这里调用instantiator.get();相当于是执行了new User(),所以返回的是User对象。然后执行modifiers.forEach(modifier -> modifier.accept(value));即,把modifiers中的对象按顺序来执行他们的accept方法,并且把User对象作为参数传进去,因为执行的时候是看实际的对象的,所以执行第一个元素的accept方法,效果是给value.setName(“李四”)【看3点】,调用第二个元素的accept方法,效果是给value.setAge(12)【看4点】。这样后,那么这个value值就被设置了对应的属性的值了。然后对modifiers进行clear。最后返回这个value。这样我们拿到的User就是设置好属性后的结果了。

http://www.mrgr.cn/news/80154.html

相关文章:

  • 细说Flash存储芯片W25Q128FW和W25Q16BV
  • 帆软的无数据展示方案
  • HIK 相机 设置缓存节点进行取流
  • 图计算之科普:BSP计算模型、Pregel计算模型、
  • 文件上传之基本介绍
  • 语言模型(序列模型)
  • 电感2222
  • #渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍01
  • freeswitch(开启支持MCU视频会议,使用mod_av模块)
  • 设计一个基础JWT的多开发语言分布式电商系统
  • Python课设-谁为影狂-豆瓣数据【数据获取与预处理课设】
  • 前端(五)css属性
  • C++知识整理day5容器——string容器
  • SQL server学习03-创建和管理数据表
  • 【arm】程序跑飞,SWD端口不可用修复(N32G435CBL7)
  • 40 list类 模拟实现
  • C#使用实体类Entity Framework Core操作mysql入门:从数据库反向生成模型2 处理连接字符串
  • 利用ESP-01S中继实现STM32F103C8T6与MQTT服务器的串口双向通信
  • Linux:Git
  • canal安装使用
  • [C++]多态
  • ORB-SLAM3源码学习:G2oTypes.cc: void EdgeInertial::computeError 计算预积分残差
  • 代码随想录算法训练营第一天 | 数组理论基础,977.有序数组平方结果再排序
  • 玛哈特精密矫平机:制造业的隐秘
  • C语言数组和字符串笔记
  • 51c大模型~合集89