介绍关于 Lambda 表达式产生的背景及典型使用场景、解决的问题。

简单匿名类与 Lambda 表达式将功能代码像方法参数那样传来传去9 种 Lambda 典型使用场景在 GUI 编程中 Lambda 表达式的妙用访问闭包作用域的局部变量

通过结合具体应用场景,进一步加深 Lambda 的理解。

简单匿名类与 Lambda 表达式缘起匿名类

在很多场景下,使用匿名类可以使我们的代码看起来更加精简。它可以实现在声明类的同时实例化类的对象,假如你只需要使用一次某个类,这时你可以考虑运用匿名类来实现,相较于内部类而言,匿名类可以省略类名称。

匿名类的声明本质上是一个表达式语句,由以下几部分组成:

    new  需要实现的接口或继承的基类名称(){    
           类定义语句块
    };  

例如:

     HelloWorld frenchGreeting = new HelloWorld() {
                String name = "tout le monde";
                public void greet() {
                    greetSomeone("tout le monde");
                }
                public void greetSomeone(String someone) {
                    name = someone;
                    System.out.println("Salut " + name);
                }
            };

我们在表达式中定义了一个实现了 HelloWorld 接口的匿名类,它还有一个属性和两个方法。

假如我们采用内部类来实现的话,我们首先要定义一个实现了接口 HelloWorld 的署名类,然后再 new 一个该署名类的对象,如下:

     class EnglishGreeting implements HelloWorld {
                String name = "world";
                public void greet() {
                    greetSomeone("world");
                }
                public void greetSomeone(String someone) {
                    name = someone;
                    System.out.println("Hello " + name);
                }
            }
            HelloWorld englishGreeting = new EnglishGreeting();

我们可以看出,匿名类是对内部类的一种精简,它在定义类的同时直接 new 了一个该类的对象。

Lambda 表达式是对匿名类的精简

假设我们有这样一个需求:根据特定的查询标准返回人员列表中的数据。

    public static void printPersons(
    List roster, CheckPerson tester) ;

CheckPerson 是一个接口,如下:

    interface CheckPerson {
        boolean test(Person p);
    }

该接口定义了一个 test 方法,如果返回 true 则表示当前对象符合筛选条件,否则不符合。

在实现时我们传入不同的匿名类来实现该接口,例如:

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

这里采用匿名类就避免了创建一个署名实现类,然后再 new 一个该实现类的对象,再将对象传入 printPersons 作为第二个参数的麻烦,我们在匿名类中一次性完成了所有这些操作。

经过仔细观察我们发现 CheckPerson 接口只有一个接口方法(这样的接口称为函数接口)。函数接口只含有一个抽象方法,可以进一步精简成 Lambda 表达式。

将功能代码像方法参数那样传来传去

上一节的 CheckPerson 接口中的抽象方法传入一个参数同时返回一个布尔值,我们完全可以使用 JDK 中为我们提供的标准函数接口而无需自己定义,这些标准函数接口位于 java.util.function 包下面。

我们可以在使用 CheckPerson 的地方直接使用 JDK 为我们提供的 Predicate 接口,该接口含有一个接口方法 boolean test(T t):

    interface Predicate {
        boolean test(T t);
    }

该接口是一个泛型接口,其中 T 是类型参数,我们在使用的时候具体指定实际的类型,例如:

    interface Predicate {
        boolean test(Person t);
    }

该接口含有和 CheckPerson 中的 test 方法完全相同的返回值类型。 我们可以在 使用 CheckPerson 的地方直接使用 Predicate 来代替。

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

该方法定义了 2 个参数,第一个参数 roster 代表要处理的人员列表数据,第二个参数 tester 是一个函数接口,它包含了一个 test 方法用来在方法体中判断人员是否符合筛选条件。具体调用该方法的时候,我们只需要传入一个实现了该函数接口的 Lambda 表达式即可。

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

上述 Lambda 表达式筛选出了性别为男,岁数大于 18 小于 25 的人。 我们可以发现,Lambda 表达式本质上是对函数接口的一种实现,是对匿名类的简化,它可以非常方便的实现在一个方法的参数中传递一段功能代码。

Lambda 典型使用场景

Lambda 表达式还有更多的使用场景。

在上面的例子中,我们使用 Lambda 表达式指定了人员信息的筛选条件,其实我们深圳可以使用 Lambda 表达式指定一种具体的操作,因为 Lambda 表达式本质上是对一段功能代码的精简封装。我们可以定义一个函数接口,该函数接口含有一个方法 void doSomething(Person p); 然后我们使用 Lambda 表达式来实现该函数接口。

不过 JDK 中已经提供这样的函数接口,我们无需重复定义,可以直接拿来使用就可以了。在 JDK 中 Consumer 接口含有一个接口方法void accept(T t)闭包的使用场景,我们可以在该接口方法的实现中来完成具体的处理逻辑。

我们重构一下之前的方法,

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

我们除了定义了筛选条件,还定义了对筛选出来的数据进行处理的函数接口,并通过调用 accept 方法将筛选出来的数据传入进行处理。

这样,我们在具体调用该方法时,只需要传入表达筛选逻辑的 Lambda 表达式和表达处理逻辑的 Lambda 表达式即可。 假设我们的处理逻辑仅仅是打印人员信息。

那么我可以这样来调用:

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

其中 p -> p.printPerson() 就是表达处理逻辑的 Lambda 表达式。

不过真实使用场景中很少是只打印信息那么简单的,我们很可能要做更复杂的处理逻辑,例如我们需要返回每个人的联系方式等等,在这样的情况下,我们就需要我们的函数接口中的抽象方法具有返回值。同样 jdk 也为我们定义好了这样的函数接口 Function,该接口有一个接口方法 R apply(T t),该方法接收一个处理数据对象,并返回一个泛型处理结果。

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

在上述定义中,我们定义了 mapper 接口,它的接口方法接受一个 Person 类型的参数并返回一个 String 类型的处理结果。

我们看到在方法体中,当筛选出了符合条件的人员数据以后,我们调用 mapper.apply(p); 来完成了处理,并将处理的结果保存到了 data 中。至于处理的具体逻辑是什么样的,取决于我们在调用该方法的时候传入的 Function

接口的具体实现,而这种函数接口的具体实现我们完全可以通过 Lambda 表达式来描述。

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

上面的例子中,我们通过 Lambda 表达式描述了我们的处理逻辑是获取每个符合筛选条件的人员的 email 地址。

我们通过上面的例子可以发现,在我们的编程实践中闭包的使用场景,我们都是针对接口和抽象来编程的,而具体的实现逻辑都是留到调用该方法的时候来描述的,而针对只含有一个抽象方法的函数接口,我们完全可以通过在方法调用中传入 Lambda 表达式的方式来描述我们具体的处理逻辑,这不仅仅在代码上实现了极大的精简,而且让我们的整个逻辑更加清晰了。

为了让这个处理更加通用,我们将参数类型使用泛型来代替,如下:

    public static  void processElements(
        Iterable source,
        Predicate tester,
        Function  mapper,
        Consumer 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()  p.getEmailAddress(),
        email -> System.out.println(email)
    );

调用时我们首先从 roster 集合中获得 Person 的对象,这里需要注意的是 roster 集合好似 List 类型,它也实现了 Iterable 接口。接着采用 Lambda 表达式来过滤,然后才有 Lambda 表达式来对过滤后的对象进行处理,返回每个人的 email 地址。最后采用 Lambda 表达式打印出返回的 email 地址。

在 GUI 编程中 Lambda 表达式的妙用

在图形界面编程(GUI 编程)中,我们经常会遇到处理事件,例如处理键盘按键事件、鼠标事件,这个时候通常会要求我们创建事件处理函数,而且这种事件处理函数一般是要实现某个特定的接口的。通常事件处理接口都是函数接口,只有一个抽象方法。

例如在按钮选择事件的处理中,我们有如下的代码:

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

它指定了当选中按钮 btn 时要做什么事情,setOnAction 方法需要接收一个实现了 EventHandler() 接口的对象,而该接口只含有一个方法 void handle(T event)。所以除了采用上面的那种匿名类的方式来实现外,我们完全可以使用 Lambda 表达式来简化代码,其效果是一样的。 如下:

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

在 GUI 图形应用程序编程中,采用 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) {
                int z = 2;
                Consumer myConsumer = (y) -> 
                {
                    // The following statement causes the compiler to generate
                    // the error "Local variable z defined in an enclosing scope
                    // must be final or effectively final" 
                    //
                    // z = 99;
                    System.out.println("x = " + x); 
                    System.out.println("y = " + y);
                    System.out.println("z = " + z);
                    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
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

综上所述,Lambda 表达式是函数接口实现类的精简写法,在很多场合下它可以很方便的代替匿名类,特别是当我们只需要一次性使用某些功能逻辑时,采用 Lambda 表达式是再合适不过了。可以大大减少我们的代码书写量。

限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688