JDK8新特性(一)-Lambda表达式

参考https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

Lambda 表达式

我们知道java中有匿名内部类,匿名内部类的缺陷之一就是如果你的实现很简单,简单到只有一个方法,甚至只有一行代码,但还是要编写冗长的内部类代码,而且内部类的语法看起来不简洁。Lambda表达式可以解决这个问题,它可以看做把函数当成一个参数传递给另一个函数,

Lambda 表达式的理想应用场景

假定我们正在开发一个社交类应用,其中有一个功能,是让管理员给指定条件的用户发信息。下表详细描述了这个功能:

属性

描述

名称

对指定用户执行操作

主要参与者

系统管理员

前置条件

系统管理员登入系统

后置条件

操作只在符合指定条件的用户上执行

主要场景

  1. 系统管理员指定过滤条件
  2. 系统管理员指定要执行的动作
  3. 系统管理员选择’提交’
  4. 系统根据提交的过滤条件查询出特定用户
  5. 系统对上一步过滤出的用户执行操作

扩展

执行操作前,系统管理员可以预览选中的用户。

使用频率

每天多次

用Person代表用户

public class Person {

public enum Sex {
    MALE, FEMALE
}

String name;
LocalDate birthday;
Sex gender;
String emailAddress;

public int getAge() {
    // ...
}

public void printPerson() {
    // ...
}

}

假定使用List存储所有用户。下面我们先用最笨的办法实现这个功能,然后一步步进行优化。

实现 1:针对某一个属性写特定查询方法

最简单的实现就是针对Person的每一个属性,都写一个对应的查询方法来完成过滤用户的工作。如下所示:打印年龄大于某个值的用户

public static void printPersonsOlderThan(List roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}

这样实现的缺点很明显,如果Person类的age字段从Int改成String,或者新增了一个Habit属性,都需要改动或者新增相应的方法;而且这种实现限制性太强,如果想筛选小于某个年龄的用户,printPersonsOlderThan方法并不满足要求,还需要再写一个方法。

实现 2:针对某一个属性写通用查询方法

下面的方法比printPersonsOlderThan更通用,它会筛选某个范围内的用户

public static void printPersonsWithinAgeRange(
List roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}

显然printPersonsWithinAgeRange方法比printPersonsOlderThan更通用,但是如果要筛选特定性别的用户,或者要选特定性别和年龄的用户,这个方法都不能满足要求,为每个属性都写这样的方法还是太麻烦了。

实现 3: 使用接口

新增一个CheckPerson接口,printPersons接收不同的CheckPerson实现来筛选不同属性的Person。

public static void printPersons(
List roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

interface CheckPerson {
boolean test(Person p);
}

下面是CheckPerson的一种实现,功能是过滤年龄在18到25之间的男性:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}

只需要把具体实现CheckPersonEligibleForSelectiveService传入printPersons中即可

printPersons(
roster, new CheckPersonEligibleForSelectiveService());

实现4: 使用匿名内部类

我们还可以在调用printPersons时传入匿名内部类代替实现3中的CheckPersonEligibleForSelectiveService:

printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);

以上几种方式都是JDK8之前的实现方法,匿名内部类相比前面几种实现已经简洁了不少,但是在JDK8中还可以更简洁:使用lambda表达式代替匿名内部类。

实现 5: 使用Lambda表达式指定过滤条件

前面代码中涉及的CheckPerson接口是函数式接口,就是只有一个抽象方法的接口,所以实现类只有一个可以实现的方法,那么就可以把方法名省略掉,看下面黑体部分代码:

printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

);

可以看出来lambda表达式分为三部分,(Person p)代表操作对象,Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25则定义了操作逻辑,中间用’->‘连接。在此基础上,使用标准函数式接口替代CheckPerson接口,进一步简化代码。

实现 6:使用标准函数式接口简化Lambda表达式

我们重新看一下CheckPerson这个接口的定义:

interface CheckPerson {
boolean test(Person p);
}

这个接口很简单,只有一个抽象方法,这种接口也叫函数式接口。我们再考察下CheckPerson中的test方法,这个方法接收一个参数,并返回boolean值,JDK已经帮我们内置了一个这种接口,省去我们自己再去定义了。那就是java.util.function包中的Predicate接口,我们看下这个接口的代码

interface Predicate {
boolean test(T t);
}

这是一个泛型接口。只有一个test方法,接收一个参数,并返回boolean值,完全可以替代CheckPerson这个接口。所以printPersonsWithPredicate方法定义更新如下:

public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

调用printPersonsWithPredicate方法时,第二个参数tester可以接收lambda表达式,如下:

printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

实现 7: 更广泛的应用Lambda表达式

考察printPersonsWithPredicate这个方法,看下哪里还可以应用lambda表达式:

public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

printPersonsWithPredicate这个方法接收一个List实例roster和一个Predicate实例tester,对于roster中的每一个对象p,如果满足tester.test(p),就调用p.printPerson() 如果对满足条件的p进行别的操作呢?比如要发邮件sendEmails();那就需要再写一个sendEmailsWithPredicate()?

public static void sendEmailsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.sendEmails();
}
}
}

这样写显然太麻烦,把”p.printPerson();”和”p.sendEmails();”抽象化,就是调用某个对象p的方法,该方法没有返回值。要使用lambda表达式,需要实现函数式接口,jdk8中提供了一个Consumer接口如下:

public interface Consumer {
void accept(T t);
}

刚好满足要求,把printPersonsWithPredicate方法改写成:

public static void processPersons(
List roster,
Predicate tester,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}

黑体部分引入新接口Consumer,调用processPersons()代码如下所示,黑体部分是新增的lambda表达式

processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);

有了上面的经验,那么猜测一定存在接收一个参数,并且有返回值的函数式接口,这样你可以把Predicate过滤的对象转换成另一个操作对象。比如上一步过滤出了年龄在18~25岁之间的人,下一步你想验证这些人的email是不是合法,那么就需要有一个方法,传入Person对象,返回email,然后进行判断。jdk8中提供了Function<T,R>接口,该接口源码如下:

public interface Function<T, R> {
R apply(T t);
}

引入Function:

public static void processPersonsWithFunction(
List roster,
Predicate tester,
Function<Person, String> mapper,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}

processPersonsWithFunction方法调用如下:

processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

实现 8: 使用泛型

实现7中的processPersonsWithFunction方法,Iterable、Predicate、Function、Consumer接口使用的都是具体类型,接下来可以引入泛型,使得processElements方法更通用,代码如下:

public static <X, Y> void processElements(
Iterable source,
Predicate tester,
Function <X, Y> mapper,
Consumer block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}

调用如下:processElements

processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

上面

实现 9: 引入聚合操作

下面使用聚合操作加lambda表达式来完成上面的功能,这样连processElements这样的方法都不用定义了。

roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));

The following table maps each of the operations the method processElements performs with the corresponding aggregate operation:

processElements 中的步骤

对应的聚合操作

获取元素流

Stream stream()

使用Predicate实例过滤元素

Stream filter(Predicate<? super T> predicate)

映射到另一个类型上

Stream map(Function<? super T,? extends R> mapper)

使用Consumer实例对元素做操作

void forEach(Consumer<? super T> action)

Lambda表达式语法

lambda表达式由以下几部分组成:

  • 圆括号包围的若干参数,参数之间用逗号分隔。例如:(String s, Person p)。可以省略类声明,改写成:(s, p);如果只有一个参数,圆括号也可以省略,直接写p。下面这个表达式就是最简略的那种情况:

    p -> p.getGender() == Person.Sex.MALE

    && p.getAge() >= 18
    && p.getAge() <= 25
  • 箭头, ->

  • 函数体,可以是一个表达式,也可以是代码块。如果是表达式,那表达式的值会作为返回值。

    p.getGender() == Person.Sex.MALE

    && p.getAge() >= 18
    && p.getAge() <= 25

    如果是代码块,必须用中括号包围(无返回值的方法例外)。

    p -> {

    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;

    }

    注意:return 语句并不是表达式。

    email -> System.out.println(email)

lambda表达式看起来很像一个方法,最开始我们就是用来替代匿名内部类,只有一个方法的匿名内部类,省去接口声明,再省去方法声明,剩下的部分跟lambda表达式很像,所以lambda表达式可以看做匿名方法。 下面这个例子Calculator,展示了处理多个参数的lambada表达式:

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));    
}

}

Calculator的operateBinary方法接收两个操作数a、b,还有一个IntegerMath接口,IntegerMath是函数式接口,定义IntegerMath接口时可以用lambda表达式。

访问局部变量、Lambda表达式变量的作用范围

像内部类一样,lambda表达式也可以访问局部变量,不过lambda表达式中的变量只在表达式中生效,但是完全不用担心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

如果把lambda表达式中的参数x换成y:

Consumer myConsumer = (x) -> {
// …
}

编译器会报错”variable x is already defined in method methodInFirstLevel(int)”,因为x是方法参数,已经被使用了。 jdk8之后,局部类、匿名内部类、lambda表达式都可以访问所属代码块的被final修饰的局部变量(方法参数)或值未发生变化的局部变量(方法参数),考虑下面代码:

void methodInFirstLevel(int x) {
x = 99;
// …
}

x=99这个赋值语句破坏了参数x的不变性,所以编译不通过,编译器报错“Local variable x defined in an enclosing scope must be final or effectively final”。

System.out.println(“x = “ + x);

Lambda表达式中的类型推断

怎么判断lambda表达式中的类型?回顾下实现3中表达式:

p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

这个表达式在两个地方用到了,分别是实现3和实现6,回看这两个实现的代码:

  • public static void printPersons(List roster, CheckPerson tester) ----实现3
  • public void printPersonsWithPredicate(List roster, Predicate tester) ----实现6

运行printPersons方法时,这个方法接收一个CheckPerson参数,所以lambda表达式就应该是这个类型。而运行printPersonsWithPredicate方法时,该方法只有一个参Predicate,所以 lambda表达式就是这个类型。只有当编译器能根据上下文判断出lambda表达式的目标类型时,你才能使用lambda表达式,包含以下场景:

  • 变量申明语句
  • 赋值语句
  • Return语句
  • 数组初始化语句
  • 方法参数
  • Lambda expression bodies
  • 条件表达式
  • 类型转换语句

目标类型与方法参数

看一下下面两个函数式接口 ( java.lang.Runnable and java.util.concurrent.Callable):

public interface Runnable {
void run();
}

public interface Callable {
V call();
}

一个有返回值,一个没有返回值. 下面定义几个重载方法:

void invoke(Runnable r) {
r.run();
}

T invoke(Callable c) {
return c.call();
}

如下调用,那么最终是执行的哪个方法呢?

String s = invoke(() -> “done”);

答案是invoke(Callable c)这个方法,因为lambda表达式() -> “done”,表示字符串”done”是返回值。而invoke(Runnable r)没有返回值。

序列化

不推荐对lambda表达式序列化,这部分内容以后有机会再详细了解。