Java lambda expression
Lambda表达式
匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。Lambda表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。
Java教程是为JDK 8编写的。本页描述的示例和实践没有利用后续版本中引入的改进。
Lambda表达式
匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。Lambda表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。
上一节“ 匿名类 ”向您展示了如何在不给它命名的情况下实现基类。虽然这通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过分和繁琐。Lambda表达式允许您更紧凑地表达单方法类的实例。
Lambda表达式的理想用例
假设您正在创建社交网络应用程序。您希望创建一项功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。下表详细描述了此用例:
领域 | 描述 |
---|---|
名称 | 对选定的成员执行操作 |
主要演员 | 管理员 |
前提条件 | 管理员已登录系统。 |
后置条件 | 仅对符合指定条件的成员执行操作。 |
主要成功案例 |
|
扩展 |
1A。管理员可以选择在指定要执行的操作之前或选择“ 提交”按钮之前预览符合指定条件的成员。 |
发生频率 | 白天很多次。 |
假设此社交网络应用程序的成员由以下Person
类表示 :
公共类人{ public enum Sex { 男,女 } 字符串名称; LocalDate生日; 性别; 字符串emailAddress; public int getAge(){ // ... } public void printPerson(){ // ... } }
假设您的社交网络应用程序的成员存储在一个List<Person>
实例中。方法我介绍一种,其他的可以查看其官网。
创建搜索匹配一个特征的成员的方法
一种简单的方法是创建几种方法; 每种方法都会搜索与一个特征匹配的成员,例如性别或年龄。以下方法打印超过指定年龄的成员:
public static void printPersonsOlderThan(List <Person> roster,int age){ for(Person p:roster){ if(p.getAge()> = age){ p.printPerson(); } } }
注意:A List
是有序的 Collection
。甲集合是一个对象,该组中的多个元素到单个单元中。集合用于存储,检索,操作和传递聚合数据。有关集合的更多信息,请参阅 集合跟踪。
这种方法可能会使您的应用程序变得脆弱,这是由于引入了更新(例如更新的数据类型)导致应用程序无法工作的可能性。假设您升级应用程序并更改Person
类的结构,使其包含不同的成员变量; 也许该类记录和测量年龄与不同的数据类型或算法。您必须重写大量API以适应此更改。此外,这种方法是不必要的限制; 例如,如果您想要打印年龄小于某个年龄的成员,该怎么办?
Lambda表达式的语法
lambda表达式包含以下内容:
-
括号中用逗号分隔的形式参数列表。该
CheckPerson.test
方法包含一个参数,p
表示Person
该类的实例 。注意:您可以省略lambda表达式中参数的数据类型。此外,如果只有一个参数,则可以省略括号。例如,以下lambda表达式也是有效的:
p - > p.getGender()== Person.Sex.MALE && p.getAge()> = 18 && p.getAge()<= 25
-
箭头标记,
->
-
一个主体,由单个表达式或语句块组成。此示例使用以下表达式:
p.getGender()== Person.Sex.MALE && p.getAge()> = 18 && p.getAge()<= 25
如果指定单个表达式,则Java运行时将计算表达式,然后返回其值。或者,您可以使用return语句:
p - > { return p.getGender()== Person.Sex.MALE && p.getAge()> = 18 && p.getAge()<= 25; }
return语句不是表达式; 在lambda表达式中,必须用braces(
{}
)括起语句。但是,您不必在大括号中包含void方法调用。例如,以下是有效的lambda表达式:电子邮件 - > System.out.println(电子邮件)
请注意,lambda表达式看起来很像方法声明; 您可以将lambda表达式视为匿名方法 - 没有名称的方法。
以下示例 Calculator
是一个lambda表达式的示例,它采用多个形式参数:
公共类计算器{ interface IntegerMath { int operation(int a,int b); } public int operateBinary(int a,int b,IntegerMath op){ return op.operation(a,b); } public static void main(String ... args){ 计算器myApp = new Calculator(); IntegerMath add =(a,b) - > a + b; IntegerMath减法=(a,b) - > a - b; System.out.println(“40 + 2 =”+ myApp.operateBinary(40,2,另外)); System.out.println(“20 - 10 =”+ myApp.operateBinary(20,10,减法)); } }
该方法operateBinary
对两个整数操作数执行数学运算。操作本身由实例指定IntegerMath
。的例子中定义了lambda表达式两个操作,addition
和subtraction
。该示例打印以下内容:
40 + 2 = 42 20 - 10 = 10
访问封闭范围的局部变量
像本地和匿名类一样,lambda表达式可以 捕获变量 ; 它们对封闭范围的局部变量具有相同的访问权限。但是,与本地和匿名类不同,lambda表达式没有任何阴影问题(有关更多信息,请参阅 阴影)。Lambda表达式是词法范围的。这意味着它们不会从超类型继承任何名称或引入新级别的范围。lambda表达式中的声明与封闭环境中的声明一样被解释。以下示例 LambdaScopeTest
演示了这一点:
import java.util.function.Consumer; 公共类LambdaScopeTest { public int x = 0; class FirstLevel { public int x = 1; void methodInFirstLevel(int x){ //以下语句导致编译器生成 //错误“从lambda表达式引用的局部变量 //必须是最终的或有效的最终“在声明A中: // // x = 99; 消费者<整数> myConsumer =(y) - > { System.out.println(“x =”+ x); //声明A. System.out.println(“y =”+ y); System.out.println(“this.x =”+ this.x); System.out.println(“LambdaScopeTest.this.x =”+ LambdaScopeTest.this.x); }; myConsumer.accept(X); } } public static void main(String ... args){ LambdaScopeTest st = new LambdaScopeTest(); LambdaScopeTest.FirstLevel fl = st.new FirstLevel(); fl.methodInFirstLevel(23); } }
此示例生成以下输出:
x = 23 y = 23 this.x = 1 LambdaScopeTest.this.x = 0
如果在lambda表达式的声明中替换参数x
代替,则编译器会生成错误:y
myConsumer
消费者<整数> myConsumer =(x) - > { // ... }
编译器生成错误“变量x已在方法methodInFirstLevel(int)中定义”,因为lambda表达式不会引入新的作用域级别。因此,您可以直接访问封闭范围的字段,方法和局部变量。例如,lambda表达式直接访问x
方法的参数methodInFirstLevel
。要访问封闭类中的变量,请使用关键字this
。在此示例中,this.x
引用成员变量FirstLevel.x
。
但是,与本地和匿名类一样,lambda表达式只能访问最终或有效最终的封闭块的局部变量和参数。例如,假设您在methodInFirstLevel
定义语句后立即添加以下赋值语句:
void methodInFirstLevel(int x){ x = 99; // ... }
由于这个赋值语句,变量FirstLevel.x
不再是有效的最终结果。因此,Java编译器生成类似于“从lambda表达式引用的局部变量必须是final或者final final”的错误消息,其中lambda表达式myConsumer
尝试访问FirstLevel.x
变量:
System.out.println(“x =”+ x);
目标打字
你如何确定lambda表达式的类型?回想一下lambda表达式,它选择了男性和年龄在18到25岁之间的成员:
p - > p.getGender()== Person.Sex.MALE && p.getAge()> = 18 && p.getAge()<= 25
这个lambda表达式用于以下两种方法:
-
public static void printPersons(List<Person> roster, CheckPerson tester)
在方法3:在局部类指定搜索条件码 -
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
在方法6:Lambda表达式使用标准的功能接口
当Java运行时调用该方法时printPersons
,它期望数据类型为CheckPerson
,因此lambda表达式属于此类型。但是,当Java运行时调用该方法时printPersonsWithPredicate
,它期望数据类型为Predicate<Person>
,因此lambda表达式属于此类型。这些方法所期望的数据类型称为目标类型。要确定lambda表达式的类型,Java编译器将使用发现lambda表达式的上下文或情境的目标类型。因此,您只能在Java编译器可以确定目标类型的情况下使用lambda表达式:
-
变量声明
-
分配
-
退货声明
-
数组初始化器
-
方法或构造函数参数
-
Lambda表达体
-
条件表达式,
?:
-
转换表达式
目标类型和方法参数
对于方法参数,Java编译器使用另外两种语言特性确定目标类型:重载解析和类型参数推断。
考虑以下两个功能接口( java.lang.Runnable
和 java.util.concurrent.Callable<V>
):
public interface Runnable { void run(); } 公共接口Callable <V> { V call(); }
该方法Runnable.run
不返回值,而是返回值Callable<V>.call
。
假设您已invoke
按如下方式重载方法(有关重载方法的详细信息,请参阅 定义方法):
void invoke(Runnable r){ r.run(); } <T> T invoke(Callable <T> c){ return c.call(); }
将在以下语句中调用哪个方法?
String s = invoke(() - >“done”);
该方法invoke(Callable<T>)
将被调用因为该方法返回的值; 方法 invoke(Runnable)
没有。在这种情况下,lambda表达式的类型() -> "done"
是Callable<T>
。
序列化
如果lambda表达式的目标类型及其捕获的参数是可序列化的,则可以 序列化它。但是,与 内部类一样,强烈建议不要对lambda表达式进行序列化。
各种方法参考
有四种方法参考:
类 | 例 |
---|---|
参考静态方法 | ContainingClass::staticMethodName |
引用特定对象的实例方法 | containingObject::instanceMethodName |
引用特定类型的任意对象的实例方法 | ContainingType::methodName |
引用构造函数 | ClassName::new |
参考静态方法
方法引用Person::compareByAge
是对静态方法的引用。
引用特定对象的实例方法
以下是对特定对象的实例方法的引用示例:
class ComparisonProvider { public int compareByName(Person a,Person b){ return a.getName()。compareTo(b.getName()); } public int compareByAge(Person a,Person b){ return a.getBirthday()。compareTo(b.getBirthday()); } } ComparisonProvider myComparisonProvider = new ComparisonProvider(); Arrays.sort(rosterAsArray,myComparisonProvider :: compareByName);
方法引用myComparisonProvider::compareByName
调用compareByName
作为对象一部分的方法myComparisonProvider
。JRE推断出方法类型参数,在本例中是(Person, Person)
。
对特定类型的任意对象的实例方法的引用
以下是对特定类型的任意对象的实例方法的引用示例:
String [] stringArray = {“芭芭拉”,“詹姆斯”,“玛丽”,“约翰”, “Patricia”,“Robert”,“Michael”,“Linda”}; Arrays.sort(stringArray,String :: compareToIgnoreCase);
方法引用的等效lambda表达式String::compareToIgnoreCase
将具有形式参数列表(String a, String b)
,其中a
和b
是用于更好地描述此示例的任意名称。方法引用将调用该方法a.compareToIgnoreCase(b)
。
参考构造函数
您可以使用名称以与静态方法相同的方式引用构造函数new
。以下方法将元素从一个集合复制到另一个集合:
public static <T,SOURCE扩展Collection <T>,DEST扩展Collection <T >> DEST transferElements( SOURCE sourceCollection, 供应商<DEST> collectionFactory){ DEST result = collectionFactory.get(); for(T t:sourceCollection){ result.add(T); } 返回结果; }
功能接口Supplier
包含一个get
不带参数并返回对象的方法。因此,您可以transferElements
使用lambda表达式调用该方法,如下所示:
设置<Person> rosterSetLambda = transferElements(roster,() - > {return new HashSet <>();});
您可以使用构造函数引用代替lambda表达式,如下所示:
设置<Person> rosterSet = transferElements(roster,HashSet :: new);
Java编译器推断您要创建HashSet
包含类型元素的集合Person
。或者,您可以按如下方式指定:
设置<Person> rosterSet = transferElements(名册,HashSet <Person> :: new);