lambda表达式和方法引用
文章引用自:https://blog.csdn.net/tengdazhang770960436/article/details/99956111
一、函数式接口
1、定义
一个接口中有且只有一个抽象方法,不包括 equals这类在object中已经定义的方,为了明确表示一个接口是函数式接口,防止别人在接口中添加其他抽象方法,我们可以给接口定义的时候添加一个添加一个@FunctionalInterface注解。
示例如下:
package com.bsx.test.lambda; import java.io.Serializable; @FunctionalInterface public interface IGetter<T> extends Serializable { Object get(T source); }
2、SerializedLambda
这一部分内容比较多,请参考另一篇文章使用SerializedLambda代替字符串硬编码
二、lambda
1、概述
λ表达式有三部分组成:1.参数列表,2.箭头(->),3.一个表达式或语句块,其中表达式是指的是一句代码,语句块是用大括号"{}"包起来的一系列代码,而λ本身必须是函数接口才能使用λ表达式。lambda 语法本质上是一个匿名方法是【语法糖】,由编译器推断并帮助你转换包装为常规代码。说白了lambad表达式就是把函数定义从原来的标准定义方式给简化了,这是因为编译器可以根据表达式内容来推断入参、出参。因此使用lambda可以使用更少的代码来实现相同功能。
2、示例
// 1. 不需要参数,返回值为 5 () -> 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s) // 6.循环打印数组 String[] atp = {"Rafael Nadal", "Novak Djokovic"}; List<String> players = Arrays.asList(atp); // 以前的循环方式 for (String player : players) { System.out.print(player + "; "); } // 使用 lambda 表达式以及函数操作(functional operation) players.forEach((player) -> System.out.print(player + "; ")); String[] players = {"Rafael Nadal", "Novak Djokovic}; // 7.排序 // 1.使用匿名内部类根据 name 排序 players Arrays.sort(players, new Comparator<String>() { @Override public int compare(String s1, String s2) { return (s1.compareTo(s2)); } }); // 2.使用 lambda expression 排序 players Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2)); Arrays.sort(players, sortByName); // 3.也可以采用如下形式: Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));
三、方法引用
1、概述
在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下:
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));
在Java8中,我们可以直接通过方法引用来简写lambda表达式中已经存在的方法。
Arrays.sort(stringsArray, String::compareToIgnoreCase);
这种特性就叫做方法引用(Method Reference)。
2、定义
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。
注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"。
3、示例
首先定义一个包含了各种类型方法的类:
package com.bsx.test.lambda; /** * @Description: 定义一个包含了各种类型方法的类 * @author: ztd * @date 2019/8/19 上午11:30 */ public class DoubleColon { public static void printStr(String str) { System.out.println("printStr : " + str); } public void toUpper() { System.out.println("toUpper: " + this.toString()); } public void toLower(String str) { System.out.println("toLower: " + str); } public int toInt(String str) { System.out.println("toInt: " + str); return 1; } public void printInteger(Integer i) { System.out.println("printInteger: " + i); } }
写一个测试类
package com.bsx.test.lambdatest; import com.bsx.test.entity.Person; import com.bsx.test.lambda.DoubleColon; import org.junit.Test; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; /** * @Description: * @author: ztd * @date 2019/8/19 上午11:37 */ public class DoubleColonTest { @Test public void testColon() { // =========静态方法========== // 静态方法因为jvm已有对象,直接接收入参。 Consumer<String> printStrConsumer = DoubleColon::printStr; printStrConsumer.accept("printStrConsumer"); // =========非静态方法========== // 方法参数个数=函数式接口参数个数,通过【new 类的实例::方法名】引用 // 使用的时候,直接传入需要的参数即可 Consumer<Integer> toPrintConsumer = new DoubleColon()::printInteger; toPrintConsumer.accept(123); // 方法参数个数=函数式接口参数个数-1,通过【类的实例::方法名】引用 // 使用的时候,传入的第一个参数是类的实例,后面是方法的参数 Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon()); BiConsumer<DoubleColon, String> toLowerConsumer = DoubleColon::toLower; DoubleColon doubleColon = new DoubleColon(); toLowerConsumer.accept(doubleColon, "toLowerConsumer"); BiFunction<DoubleColon, String, Integer> toIntFunction = DoubleColon::toInt; int i = toIntFunction.apply(new DoubleColon(), "toInt"); System.out.println(i); } }
你已经看到测试类里面针对不同类型的方法,方法引用的定义方式并不一样,使用方式也不一样,我们的困惑就在于为什么要这么定义,为什么这么使用?这样我们才能在使用jdk1.8里面的各种Function和stream的时候变得随心所欲。
4、为什么这么定义这么使用
首先我们需要明确的一点是,函数式接口也是接口,只是它里面只有一个抽象方法,在使用的时候跟其他的接口并没有本质区别,区别只在于使用的方式更简洁。要实现它同样需要按照普通接口的规范去使用,比如要保证实现方法和接口的输入输出参数完全对应。
4.1静态方法引用
这个很容易理解,静态方法因为jvm已有对象,直接接收入参函数的定义跟接口完全一致。
// 定义 public static void printStr(String str) { System.out.println("printStr : " + str); } // 使用 Consumer<String> printStrConsumer = DoubleColon::printStr; printStrConsumer.accept("printStrConsumer");
4.2非静态方法
// 函数定义 public void printInteger(Integer i) { System.out.println("printInteger: " + i); } public void toUpper() { System.out.println("toUpper: " + this.toString()); } // 函数使用 Consumer<Integer> toPrintConsumer = new DoubleColon()::printInteger; toPrintConsumer.accept(123); Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon());
非静态方法的函数引用定义和使用就有点让人很困惑了,明明函数式接口里面是2参数,可是非静态方法里面是1个参数,这个是怎么实现的呢?
其实这个不一致是有要求的,就是函数式接口的参数个数-非静态方法参数个数=0或者1,我这里叫它为参数差,这个参数差取值范围不能变,如果大于1或者小于0都会报错。
接下来我们分别来讨论0和1的情况。
参数差=0:方法参数个数相等,那么直接通过一个类的实例来调用这个方法即可,因此方法引用就是【实例::方法名】,使用的时候也是直接传递所需要的参数即可。
参数差=1:说明非静态方法少一个参数,这是就不能保证接口参数和实现方法参一一对应了,这很明显有问题。这时候我们通过【类::方法名】来定义(这种定义方式也是jdk的规定,记住就好)。因为这个方法不是静态方法,定义的时候也没有给它传递类的实例,所以我们需要在使用的时候给这个方法传递一个宿主(类的实例),这个宿主永远是接口的第一个参数,我称为宿主优先原则,因此就会出现下面的这种定义和使用的方式:
// 方法DoubleColon::toUpper没有参数,因此Consumer的参数就是DoubleColon的一个实例 // 因此定义的时候Consumer的泛型类型就是DoubleColon // 使用的时候只需要传递一个DoubleColon的实例即可 Consumer<DoubleColon> toUpperConsumer = DoubleColon::toUpper; toUpperConsumer.accept(new DoubleColon());
四、结合示例
@FunctionalInterface interface Test { public void run(String str); } public class blog { public static void main(String[] args) { Test t1 = new Test() { @Override public void run(String str) { System.out.println("匿名内部类创建实现类对象" + str); } }; Test t2 = (str) -> { System.out.println("lambda创建实现类对象" + str); }; } }
由于上面的t2实现类中只传入一个参数所以小括号可以取消,方法体只有一条语句,大括号可以取消
写成如下形式:
Test t = str-> System.out.println(str);
1、静态方法引用
格式 :
类名::方法名
注意事项:
被引用的方法参数列表和函数式接口中抽象方法的参数一致!!
接口的抽象方法没有返回值,引用的方法可以有返回值也可以没有
接口的抽象方法有返回值,引用的方法必须有相同类型的返回值!!
interface Test { public void run(String str); } public class blog { public static void main(String[] args) { printString("静态方法引用",System.out::println); // 实质代码: printString("静态方法引用", (str) -> System.out.println(str)); } public static void printString(String str, Test t) { t.run(str); } }
这个代码中,利用lambda表达式创建了Test接口的实现类对象,
重写run()方法,正好方法体是静态方法printString方法
稍微修改一下,将函数式接口抽象方法作为一个有返回值的方法,那么引用方法也得有同样类型的返回值
interface Test { public int run(String str); } public class blog { public static void main(String[] args) { // 实质代码: getInteger("123", (str) -> Integer.parseInt(str)); int i = getInteger("123", Integer::parseInt); System.out.println(i); //输出:123 } public static int getInteger(String str, Test t) { return t.run(str); } }
两个例子中,分别调用System.out类的println静态方法,Integer类的parseInt静态方法
由于满足抽象参数列表与引用参数列表相同,所以可以写成静态方法引用的格式
2、对象方法引用
格式:
对象名::非静态方法名
注意事项与静态方法引用完全一致
@FunctionalInterface interface Test { public String run(String str); } class Person { public String goWalking(String str) { return string.concat(" 引用方法"); } } public class blog { public static void main(String[] args) { //实质代码: Test t1 = (str) -> new Person().goWalking(str); //实质代码: System.out.println(t1.run("对象")); Test t2 = new Person()::goWalking; System.out.println(t2.run("对象")); //输出:对象 引用方法 } }
类中有一个方法goWalking()方法体是test实现类对象需要的方法体
且方法列表参数一致,返回值类型相同
则可以利用lambda创建test的实现类对象,然后重写的抽象方法体就是调用Person对象的goWalking方法
符合对象引用方法的所有要求,则可以写成t2的样式
3、构造方法引用
格式:
类名::new
注意事项:
被引用的类必须存在一个构造方法与函数式接口的抽象方法参数列表一致
interface Test { public Person1 run(String str); } class Person { String name; public Person(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } } public class blog { public static void main(String[] args) { //实质代码: Test t1 = (string) -> new Person(str); //实质代码: Person p = t1.run("张三"); Test t2 = Person::new; Person p2 = t2.run("李四"); System.out.println(p2); //输出:Person{name='李四'} } }
由于函数式接口test中抽象方法,返回值是Person对象,且参数列表与Person类中的构造方法相同
则可以通过创建函数式接口的实现类对象,方法体通过调用类中的构造方法创建对象
使用了构造方法引用写成了代码中t2的形式
4、数组构造方法引用
格式:
数据类型[ ]::new
interface Test { public String[] run(int length); } public class blog { public static void main(String[] args) { //实质代码: Test t1 = (length) -> new String[length]; Test t2 = String[]::new; String[] arr = t2.run(5); } }
5、特定类型的方法引用
格式:
类名::非静态方法
public class blog { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); Collections.addAll(list,"d1wdaddq","ASDINAOSDN","aa","AA"); //实质代码: Collections.sort(list,(string1,string2)->string1.compareToIgnoreCase(string2)); Collections.sort(list,String::compareToIgnoreCase); System.out.println(list); } }
特定类型方法引用,在Comparator函数式接口的抽象方法中传入的参数有两个,
可是compareToIgnoreCase()方法参数只有一个,第一个传入的参数作调用对象
这就满足了特定类型的方法引用,所以可以简化成类名::非静态方法的形式
6、类中方法调用父类或本类方法引用
格式:
this::方法名
super::方法名
interface Test { public void itMethod(); } class father { public void buy() { System.out.println("买东西"); } } class son extends father { public void buy() { System.out.println("买糖"); } public void test() { // 实质代码: Test t = () -> buy(); Test t = this::buy; t.itMethod(); // 实质代码: Test t2 = ()->super.buy(); Test t2 = super::buy; t2.itMethod(); } } public class blog { public static void main(String[] args) { son s = new son(); s.test(); //输出: 买糖 买东西 } }
在有继承关系的类中,若方法想调用本类或父类的成员方法
在函数式接口抽象方法与成员方法参数列表相同,且返回值类型相同的情况下
也可以使用this和super的方法引用来简写原本的lambda代码
参考文章:
https://blog.csdn.net/weixin_42022555/article/details/81943263
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架