Lambda 表达式

2019-07-26  本文已影响0人  bern85

Lambda 表达式

想要更好的了解Lambda,请先了解匿名类, 匿名类通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点繁琐,Lambda表达式允许更紧凑地表达单方法类的实例。
我们接下来一步步的来了解Lambda。

首先我们有一个Person

import java.util.List;
import java.util.ArrayList;
import java.time.chrono.IsoChronology;
import java.time.LocalDate;

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

    Person(String nameArg, LocalDate birthdayArg,
           Sex genderArg, String emailArg) {
        name = nameArg;
        birthday = birthdayArg;
        gender = genderArg;
        emailAddress = emailArg;
    }

    public int getAge() {
        return birthday
                .until(IsoChronology.INSTANCE.dateNow())
                .getYears();
    }

    public void printPerson() {
        System.out.println(name + ", " + this.getAge());
    }

    public Sex getGender() {
        return gender;
    }

    public String getName() {
        return name;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    public static List<Person> createRoster() {

        List<Person> roster = new ArrayList<>();
        roster.add(
                new Person(
                        "Fred",
                        IsoChronology.INSTANCE.date(1980, 6, 20),
                        Person.Sex.MALE,
                        "fred@example.com"));
        roster.add(
                new Person(
                        "Jane",
                        IsoChronology.INSTANCE.date(1990, 7, 15),
                        Person.Sex.FEMALE, "jane@example.com"));
        roster.add(
                new Person(
                        "George",
                        IsoChronology.INSTANCE.date(1991, 8, 13),
                        Person.Sex.MALE, "george@example.com"));
        roster.add(
                new Person(
                        "Bob",
                        IsoChronology.INSTANCE.date(2000, 9, 12),
                        Person.Sex.MALE, "bob@example.com"));

        return roster;
    }
}

我们先用这个简单的案例,然后逐步使用lambda表达式来完成示例的学习,该示例代码参考RosterTest

方法1:搜索年龄大于age的人员

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

方法2:搜索年龄在一个区间范围的人员

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

如果你想要打印指定性别的成员,或指定性别和年龄范围的组合,该怎么办? 如果您决定更改Person类并添加其他属性(如关系状态或地理位置),该怎么办? 显然为每个可能的搜索查询创建单独的方法会导致很多臃肿的代码。 如果你学过设计模式的话,策略模式是比较适合这种情况的。

方法3:使用策略模式

1、UML图


Strategy

2、接口

interface CheckPerson {
    boolean test(Person p);
}

3、策略实现

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

4、caller

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

5、测试

printPersons(roster, new CheckPersonEligibleForSelectiveService());

方法4:使用匿名类

方法3,可以依据条件随时定义新的策略,看起来应该都满足开闭原则,也能随时满足不断变化的需求了,但是代码似乎有点臃肿,那么匿名类似乎不用定义一个新的类了。

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

是不是简洁了许多

方法5:使用Lambda表达式

接口CheckPerson是一个 functional interface。 functional interface的定义是:接口包含且只包含一个抽象方法(functional interface可以包含一个或多个 default 方法 or static 方法.)
这种类型的接口也称为SAM接口,即Single Abstract Method interfaces。
由于functional interface只包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。 因此, 我们可以使用 lambda expression代替匿名类,如下所示:

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

方法6:使用通用接口并使用Lambda表达式

我们回过头来再看一眼CheckPerson接口

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口,这个接口仅仅包含了一个抽象方法,该方法接受一个参数,并返回一个布尔值。在应用程序中定义一个这个的接口并不值得。因此JDK在java.util.function包中定义了若干个标准的接口。
我们可以使用java.util.function.Predicate替代CheckPerson,该接口包含一个boolean test(T t)方法

interface Predicate<T> {
    boolean test(T t);
}

因此,以下方法调用和方法3调用printPersons是一样的效果

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

这不是lambda表达式的唯一的使用方式。下面我们将解锁lambda表达式的其他姿势。

方法7:在整个函数中使用Lambda表达式

我们再回头查看printPersonsWithPredicate,看看其他地方是否也可以使用lambda表达式呢:

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

此方法是检查Person集合中的实例是否满足Predicate中的条件。如果Person实例满足tester指定的条件,则在Person实例上调用printPersron方法。
我们如果想要再满足tester条件下,执行不同的操作,而不仅仅是调用printPersron方法,那么我们可以使用新的lambda表达式。如果要使用lambda表达式,需要创建一个functional interface,该函数接口包含一个Person类型的参数,并返回一个void类型。而jdk java.util.functionConsumer<T>已经帮我们实现了这样的接口,我们使用便是,Consumer<T> 包含一个 void accept(T t)方法,符合的要求,下面我们将p.printPerson()替换为Consumer<T> 的accept方法.

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

因此,我们调用该方法和方法3一样,如下所示:

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

看起来我们已经很完美的解决了一些问题,那么新的需求又来了,讨厌的产品经理总是不让人省心一点,他不要求打印Person了,要求如果通过验证了,打印Person的其他信息。幸好jdk的java.util.function.Function<T, R>帮我们写好了相关的函数接口,Function<T, R>包含一个R apply(T t)方法,该方法指定一个mapper参数,然后返回mapper加工后的数据,然后调用Consumer block消费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);
        }
    }
}

调用方式如下,我们获取人员的Email联系信息,然后将联系方式打印出来

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

方法8:更广泛地使用泛型

我们再次改造一下processPersonsWithFunction,将参数类型变成更加通用的泛型,可以接受任何类型的参数,而不仅仅是Person。如下所示:

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

调用方式如下所示:

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

该方法调用执行如下操作:
1、从集合中获取元素。该事例中表示的是Person对象的List集合。请注意:List类型的集合是Iterable类型的子类。
2、使用Predicate对象过滤Person对象。在此示例中,Predicate对象是一个lambda表达式,匹配相关对象是否符合条件。
3、将每个过滤出来的对象传递赋值到Function mapper对象,在此示例中,Function对象是一个lambda表达式,它返回Person的email属性。
4、对Consumer对象进行消费操作。 在此示例中,Consumer对象是一个lambda表达式,用于输出字符串,该字符串是Function对象返回的email值。
你可以使用聚合操作替换每一个操作。

方法9:使用Lambda表达式作为聚合操作的参数

使用聚合操作来打印集合中符合条件的Person的email地址,如下所示:

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

下表将方法processElements执行的每个操作映射到相应的聚合操作:

processElements 操作 聚合操作
获取集合对象 Stream<E> stream()
Predicate 对象过滤器 Stream<T> filter(Predicate<? super T> predicate)
*Function *将对象转化为其他值 <R> Stream<R> map(Function<? super T,? extends R> mapper)
*Consumer *消费对象 void forEach(Consumer<? super T> action)

filter, map,forEach都属于聚合操作,聚合操作处理流中的元素,而不是直接从集合中获取元素。流是元素的一个序列,不同于集合,它不是存储元素的数据结构。流通过管道携带来自源(例如集合)的值。 管道是一系列流操作,在此示例中为filter-map-forEach。 此外,聚合操作通常接受lambda表达式作为参数,你也可以自定义聚合操作。
该章节主要论述lamabda表达式,以后会更加深入的讲解stream的相关知识。

Lambda 表达式在GUI应用中的使用

在 GUI 应用程序中的事件(例如键盘操作,鼠标操作和滚动操作),通常会创建事件处理程序,这通常涉及实现特定接口。 通常,事件处理程序接口是functional interfaces; 他们往往只有一种方法。
在示例HelloWorld.java 中的原来使用的匿名内部类,我们可以使用lamabda替代。

          btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

方法btn.setOnAction的参数EventHandler的作用是指定响应方法,该 EventHandler<ActionEvent>接口只包含一个void handle(T event)方法,那么该接口就属于一个functional interface,所以我们可以使用下面的lambda表达式替代它:

        btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

Lambda表达式的语法

lambda表达式包含以下内容:

Note: 你可以省略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表达式中,必须将语句括在大括号({})中。 但是,没必要在大括号中包含只有一条void方法调用。 例如,以下是有效的lambda表达式:

email -> System.out.println(email)

Note: lambda表达式看起来很像方法声明; 您可以将lambda表达式视为匿名方法 - 没有名称的方法。

下面的示例Calculator.java 是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表达式定义了两个操作,加法和减法。 该示例打印内容如下:

40 + 2 = 42
20 - 10 = 10

局部变量的访问界限

Shadowing

如果特定范围(例如内部类或方法定义)中的类型声明(例如成员变量或参数名称)与封闭范围中的另一个声明具有相同的名称,则声明将隐藏声明封闭范围。 你不能仅通过其名称引用带Shadowing的声明。 以下ShadowTest.java示例说明了这一点:

public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

输出如下:

x = 23
this.x = 1
ShadowTest.this.x = 0

此示例定义了三个名为x的变量:类ShadowTest的成员变量,内部类FirstLevel的成员变量,以及methodInFirstLevel方法中的参数。 方法methodInFirstLevel的参数变量x,隐藏了内部类FirstLevel的变量。 因此,当你在方法methodInFirstLevel中使用变量x时,它引用的是方法参数。 要引用内部类FirstLevel的成员变量,需要使用关键字this来确认范围:

System.out.println("this.x = " + this.x);

关于更大范围的外部类的成员变量,如果要在方法methodInFirstLevel中访问的话,需要使用如下语句:

System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

和本地和匿名类一样,lambda表达式也可以捕获变量; 它们对封闭范围的局部变量具有相同的访问权限。 但是,与本地和匿名类不同,lambda表达式没有任何Shadowing问题。 Lambda表达式是词法范围的。意思是Lambda表达式中声明的参数变量不是从父类中继承的,也不会引入新的一个范围(很拗口,直接看代码吧),lambda表达式中的声明与封闭环境中的声明一样被识别。下面的LambdaScopeTest说明了这一点。

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表达式myConsumer的声明中用参数x代替y,会发生什么呢?:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

编译器会报错:"variable x is already defined in method methodInFirstLevel(int)",因为lambda表达式不会引入新的范围。因此,你可以直接访问封闭范围的字段,方法和局部变量。 例如,lambda表达式直接访问methodInFirstLevel方法的参数x。如果要访问内部类中的变量,需要使用关键字this。 在此示例中,this.x引用成员变量FirstLevel.x

与本地和匿名类一样,lambda表达式访问局部变量和参数只能属于final类型的。 例如,假设在methodInFirstLevel语句之后立即添加以下赋值语句:

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

因为x=99这条赋值语句,变量FirstLevel.x就不再是effectively final的了,结果就是,编译器会再如下语句的位置(lambda表达式myConsumer尝试访问FirstLevel.x变量:),抛出error--"local variables referenced from a lambda expression must be final or effectively final".

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

目标类型确定

如何确定lambda表达式的类型?我们回顾一下上面得例子:

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

这个表达式在下面两个地方用到了:

当虚拟机调用printPersons方法时,它期望CheckPerson的数据类型,因此lambda表达式属于这种类型。但是,当调用方法printPersonsWithPredicate时,它期望数据类型为Predicate <Person>,因此lambda表达式属于此类型。这些方法所期望的数据类型称为目标类型。Java编译器根据lambda表达式的上下文或场合的确定lambda表达式的目标类型。因此,只能在Java编译器可以确定目标类型的情况下使用lambda表达式:

目标类型和方法参数

对于方法参数,Java编译器使用两种语言特性确定目标类型:重载解析和类型参数推断。
例如以下两个功能接口(java.lang.Runnablejava.util.concurrent.Callable <V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

方法Runnable.run没有返回值,而Callable <V> .call则有返回值。
假设您已按如下方式重载方法调用:

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

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

那么下面得语句将会调用那个方法呢?

String s = invoke(() -> "done");

方法invoke(Callable<T>)将会被调用。在这种情况下,lambda表达式() -> "done"返回 "done"字符串,所以根据返回值推导,目标类型为Callable<T>.

上一篇下一篇

猜你喜欢

热点阅读