匿名类存在的问题是: 如果匿名类的实现非常简单,例如仅包含一个方法的接口,则匿名类的语法可能看起来很笨拙且不清楚。在这些情况下,您通常 new一个匿名内部类对象
作为参数传递给方法,例如,当某人单击按钮时应采取什么措施。 Lambda表达式
能实现这样的需求,它可以更紧凑更简洁的表达单方法类的实例。
本篇文章从以下几点介绍一下Lambda表达式:
Lambda表达式用例
假设您正在开发一个社交网络程序。您想新增一个功能,使管理员可以对满足特定条件的社交网络用户执行任何类型的操作,例如发送消息。
用户用以下Person类表示:
public class Person { public enum Sex { MALE, FEMALE } String name; LocalDate birthday; Sex gender; String emailAddress; public int getAge() { // ... } public void printPerson() { // ... } }
并且用户存储在一个 List<Person>
实例中。
我们先从最笨的实现开始,然后使用本地和匿名类对该方法进行改进,最后再使用lambda表达式以一种高效而简洁的方式实现。
一种简单的实现是:创建几个方法,每个方法都会搜索和一个特征(例如性别或年龄)相匹配的成员。以下方法将打印出超过指定年龄的成员:
public static void printPersonsOlderThan(List<Person> roster, int age) { for (Person p : roster) { if (p.getAge() >= age) { p.printPerson(); } } }
这样做可以满足业务需求,但是他的可扩展性非常差,并且每个特征的一种搜索需要写一个方法,很麻烦。可以考虑以下几个问题:
以下方法比前面的 printPersonsOlderThan
方法更通用,它会打印指定年龄范围内的成员:
public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } }
考虑以下几个问题:
printPersonsOlderThan
下面的方法打印和指定的搜索条件匹配的用户:
public static void printPersons(List<Person> roster, CheckPerson tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
这个方法会遍历List中的Person对象,通过CheckPerson检查每个Person,如果满足搜索条件,就会输出Person信息。
要指定搜索条件,实现以下 CheckPerson接口:
interface CheckPerson { boolean test(Person p); }
下面的类实现了CheckPerson接口并且实现了接口中的test的方法,这个方法筛选男性且年龄在18至25岁之间的用户。
class CheckPersonEligibleForSelectiveService implements CheckPerson { public boolean test(Person p) { return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } }
使用CheckPerson
printPersons( roster, new CheckPersonEligibleForSelectiveService());
以下方法传入的第二个参数是一个匿名类,该类筛选男性且年龄在18至25岁之间的用户:
printPersons( roster, new CheckPerson() { public boolean test(Person p) { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; } } );
这种方法减少了代码量,因为您不必为每个搜索条件创建一个类。但是,考虑到 CheckPerson
接口仅包含一个方法,并且匿名类的代码相当庞大,在这种情况下,您可以使用 lambda表达式
代替匿名类。
CheckPerson
接口是一个 functional interface
。 functional interface
是仅包含一个抽象方法的接口 。( functional interface
可能包含一个或多个 默认方法
或 静态方法
。)由于 functional interface
仅包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。因此,可以使用 lambda表达式
(而不是使用匿名类表达式)。
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
可以使用 functional interface
来代替 CheckPerson
,这可以进一步减少所需的代码量。
interface CheckPerson { boolean test(Person p); }
CheckPerson
一个非常简单的接口,是一个 functional interface
。因为JDK已经提供了一些通用的 functional interface
,可以在 java.util.function
包中找到它们。所以,我们可以直接使用这些 functional interface
,如果能够满足我们的需求,我们没必要再定义这样的接口。
例如,可以使用 Predicate<T>
接口代替 CheckPerson
。该接口包含方法 boolean test(T t)
:
interface Predicate<T> { boolean test(T t); }
此接口仅包含一个参数类型T。当使用实际参数声明或实例化泛型类型时,您将拥有一个参数化类型。例如,参数化类型Predicate<Person>如下:
interface Predicate<Person> { boolean test(Person t); }
此参数化类型包含一个方法,该方法的参数和返回类型与 CheckPerson.boolean test(Person p)
相同。因此,可以用 Predicate<T>
代替 CheckPerson
:
public static void printPersonsWithPredicate( List<Person> roster, Predicate<Person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
因此,以下方法调用和在类中指定搜索条件有相同的效果:
printPersonsWithPredicate( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
这不是使用 lambda表达式
的唯一方式。
重新看一下 printPersonsWithPredicate
方法,看看在什么地方还可以使用lambda表达式:
public static void printPersonsWithPredicate( List<Person> roster, Predicate<Person> tester) { for (Person p : roster) { if (tester.test(p)) { p.printPerson(); } } }
我们接下来将 printPerson
方法用Lambda表达式代替,那么我们需要一个 functional interface
,该方法可以传入一个Person类型的参数并返回void。 很幸运,JDK提供的 Consumer<T>
接口满足这样的需求。
public static void processPersons( List<Person> roster, Predicate<Person> tester, Consumer<Person> block) { for (Person p : roster) { if (tester.test(p)) { block.accept(p); } } }
调用该方法时的写法如下:
processPersons( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.printPerson() );
如果想对个人资料进行更多处理而不仅仅打印出来,该怎么办?假设要验证用户的个人资料或检索他们的联系信息?在这种情况下,需要一个 functional interface
,其中包含一个有返回值的抽象方法。很幸运,JDK提供的 Function<T,R>
接口接口满足这样的需求。
public static void processPersonsWithFunction( List<Person> roster, Predicate<Person> tester, Function<Person, String> mapper, Consumer<String> block) { for (Person p : roster) { if (tester.test(p)) { String data = mapper.apply(p); block.accept(data); } } }
以下调用先从符合条件的Person中获取电子邮件信息,然后打印出来:
processPersonsWithFunction( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25, p -> p.getEmailAddress(), email -> System.out.println(email) );
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
如果指定单个表达式,将计算表达式并返回其值。另外,可以使用return语句:
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
return语句
不是表达式。在 lambda表达式
中,必须将语句括在大括号{}中。但是,对 void方法
的调用不用括在大括号中。例如,以下是有效的lambda表达式:
email -> System.out.println(email)
注意,lambda表达式看起来很像方法声明。可以将lambda表达式视为匿名方法,即没有名称的方法。
以下示例, Calculator定义 多个参数
的 lambda表达式
示例:
public class Calculator { 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) { Calculator myApp = new Calculator(); IntegerMath addition = (a, b) -> a + b; IntegerMath subtraction = (a, b) -> a - b; System.out.println("40 + 2 = " + myApp.operateBinary(40, 2, addition)); System.out.println("20 - 10 = " + myApp.operateBinary(20, 10, subtraction)); } }
该方法 operateBinary
对两个整数进行数学运算。由 IntegerMath
的具体实现来计算。示例中定义了两个Lambda表达式: addition
和 subtraction
。示例输出以下内容:
40 + 2 = 42 20-10 = 10
像本地和匿名类一样, lambda表达式
可以访问变量。它们对局部变量具有相同的访问权限。
import java.util.function.Consumer; public class LambdaScopeTest { public int x = 0; class FirstLevel { public int x = 1; void methodInFirstLevel(int x) { // The following statement causes the compiler to generate // the error "local variables referenced from a lambda expression // must be final or effectively final" in statement A: // // x = 99; Consumer<Integer> myConsumer = (y) -> { System.out.println("x = " + x); // Statement 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
如果在myConsumer声明里用参数x代替y,编译器将报错:
Consumer<Integer> myConsumer = (x) -> { // ... }
错误信息: methodInFirstLevel(int)
方法已经定义了变量x 。这是因为lambda表达式未引入新的作用域级别。因此,您可以直接访问该范围的字段、方法和局部变量。例如,lambda表达式直接访问 methodInFirstLevel
方法的x参数。要访问类中的变量,请使用关键字this。在此示例中,this.x引用成员变量FirstLevel.x。
与本地和匿名类一样, lambda表达式
只能访问用final或effectively final的局部变量和参数。例如,假设您在 methodInFirstLevel
方法内部添加以下赋值语句:
void methodInFirstLevel(int x){ x = 99; // ... }
由于改变了x的值,所以该变量不再是final或实际上final类型的变量。由于lambda表达式 myConsumer
会访问 FirstLevel.x
变量,结果Java编译器报一条错误信息,类似于 lambda表达式引用的本地变量必须是final或实际上是final
。
System.out.println(“ x =” + x);
您如何确定Lambda表达式的类型?回忆一下选择年龄在18至25岁之间的男性用户的lambda表达式:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
此lambda表达式用在以下两个方法:
public static void printPersons(List<Person> roster, CheckPerson tester) public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
当调用 printPersons
方法时,它期望的数据类型为 CheckPerson
,因此lambda表达式为该类型。但是,当调用 printPersonsWithPredicate
方法时,它期望的数据类型为 Predicate<Person>
,因此lambda表达式就是这种类型。
这些方法期望的数据类型称为目标类型。
关注:Java提升营
→「技术干货」每日推送
→「2T免费资料」随时领取