Lambda表达式

来源:互联网 发布:面试官面试技巧知乎 编辑:程序博客网 时间:2024/06/01 17:59

Lambda表达式介绍

官方文档翻译(java tutorials)

    自己翻译了官方文档的这一部分,希望能够对自己Lambda表达式有更进一步的了解,同时也希望能够对大家有帮助。英语有点蹩脚,有待提高 .^_^.

    匿名类存在的一个问题,当这个匿名类的实现非常简单时,如仅包含一个方法,那么这个匿名类的语法看起来会很笨拙和模糊不清。这种情况,你通常试图给另一个方法传递函数作为参数,如,点击按钮时该执行什么操作。lambda表达式可以满足你的这个需求,把函数当作方法的参数,或者代码作为数据。
     在前一节中,即匿名类,介绍了不用名字的方法实现一个基本类,尽管这样方法通常比有名字类更简洁,对于只有一个方法的类,用匿名类看起来还是有点多余和笨重。lambda表达式能够让你更简洁的来实现仅有一个方法类的实例。
     这一章节涵盖一下内容:

  • lambda理想使用例子
    • 方法1:创建一个方法,用于查询符合一个字符的成员
    • 方法2:创建泛型查询方法
    • 方法3:在内部类中定义查询条件代码
    • 方法4:在一个匿名类中定义查询条件代码
    • 方法5:在一个lambda表达式中定义查询条件代码
    • 方法6:通过lambda表达式使用标准函数接口
    • 方法7:在你的整个应用中使用lambda表达式
    • 方法8:使用泛型增加扩展性
    • 方法9:用lambda表达式作为参数的聚合操作
  • lambda在GUI中应用
  • lambda语法
  • 访问作用域中的局部变量
  • 目标类型
    • 目标类型和方法参数
  • 序列化

lambda表达式理想使用例子
     假设你正在创建一个社交网络应用。你想要这个应用有一个特性,允许管理员对应用中的满足一定条件的成员执行所有的操作,如发送消息。下面表格详细描述这个用例:

字段 描述 用例名称 在选中的成员执行操作 主要参与者 管理员 前置条件 管理员登入系统 后置条件 操作只在满足条件的成员中被执行 成功场景 1.管理员确定在哪些成员执行操作的条件
2.管理员确定被选中成员中的执行操作
3.管理员选择提交按钮
4.系统查找所有满足条件的成员
5.系统在所有满足条件的成员中执行指定的操作 扩展 1a.管理员能够在执行操作之前对满足条件的成员进行预览选择 发生频率 每天多次

假设这个社交网络应用中的成员为如下 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表达式来有效和简洁的完成这个例子。在这个章节的RosterTest 例子中查看代码片段。

方法1:创建方法,用于查询匹配一个字符的成员

一个极度简单的方法是创建几个方法;每个方法用于查询匹配一个字符成员,如性别或者年龄。下面代码打印出大于指定年龄的成员:

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

备注:List是一个有序集合,一个collection是一个将多个元素聚集在一个单元中的对象。Collections被用于存储、检索、操控和聚合数据通信。了解更多关于collections信息,查看Collections部分
这种方法会使你的应用变得脆弱,在需求变更的时候可能会导致应用不能正常工作,如有新的数据类型。假如你升级你的应用,并修改了Person类的结构,修改后的类包含了不同的成员变量;可能是这个类记录和度量年龄的数据类型变了,或者算法变了。你将不得不重写你的api来适应这个变化。另外,这种方法有不必要的限制;假如你想要打印出小于指定年龄的成员,将会怎么样?
方法2:创建一个更通用的查询方法
下面的方法比printPersonOlderThan更通用;它打印出指定范围的年龄:

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

假如你想要打印一个指定性别的成员呢?又或者一个指定性别并且指定年龄段的呢?或者你决定修改Person类并添加其他的属性,如关系状态或者地理位置?尽管这个方法比printPersonOlderThan更通用,试图给每种可能的查询情况创建一些列方法仍然会使得代码变得脆弱。你可以通过创建一个包含查询条件的类来替代一些列代码。
方法3:在局部类中定义搜索条件代码
下面的代码打印匹配你所定义查询条件的成员:

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

这个方法,使用参数tester对象的test方法来检查参数roster集合中每个person对象是否满足查询tester中定义的查询条件。如果test方法返回true,那么person对象的printPersons方法被调用。
为了定义查询标准,你实现CheckPerson接口:

interface CheckPerson{    boolean test(Person p);}

下面的类通指定test方法来实现了CheckPerson接口。这个方法过滤那些在美国符合义务兵役的成员:如果一个对象时男的并且年龄在18到25之间,则返回true:

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

使用这个类,你会创建一个它的新对象,并调用printPersons方法:

printPersons(roster,new CheckPersonEligibleForSelectiveService());

尽管这种方法可以减少代码的脆弱性,你不需要在修改person结构的时候重写。但是对于每种查询,你仍然有另外的代码,一个新的接口和一个局部类。因为CheckPersonEligibleForSelectiveService 实现一个接口,你可以使用匿名类替代局部类,避免为没个查询声明一个新的类。
方法4:在一个匿名类中定义查询标准
下面例子,调用printPersons方法的一个参数是过滤符合在美国义务兵役并且年龄在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表达式替代一个匿名类。
方法5:使用lambda表达式指定查询标准代码
CheckPerson接口是一个功能性接口,一个功能性接口是指那些只包含一个抽象方法的接口(一个功能性接口可能包含一个或者多个默认实现的方法或者静态方法)。因为一个功能性接口只包含一个抽象方法,在你实现它的时候,你可以忽略这个方法的名字。为了实现这个,你可以是用lambda表达式来替代匿名类,下面高亮的代码为lambda表达式:

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

你可以使用一个标准的功能性接口代替CheckPerson接口,这样可以减少更多的代码。
方法6:使用标准的功能性接口和lambda表达式

interface CheckPerson{    boolean test(Person p);}

这是一个非常简单的接口,它是一个功能性接口,因为它只包含一个抽象方法。这个方法有一个参数并返回一个boolean值,这方法是如此的简单以致于没有必要在你的应用中定义它;因此,jdk在包Java.util.function中定义一些标准功能性接口。
例如,你可以使用Predicate< T >接口替代CheckPerson。这个接口包含test方法:

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

Predicate< T >是一个泛型接口,泛型类型(如泛型接口)在<>中定义一个或者多个参数类型,这个接口只包含一个类型参数T.当你使用具体类型参数声明或者实例化一个泛型类型时,你有一个参数化类型,例如,

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

结果,

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

并不是只有这个地方能够使用lambda表达式,下面方法给出了其他使用lambda表达式的方式。
方法7:在你的整个应用中使用lambda表达式

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

可以通过tester的定义给这些满足条件的person对象指定一个不同的操作,而不是调用printPerson方法。你可以使用lambda表达式定义这些操作。假设你想要一个类似printPerson的lambda表达式,需要一个Person类型的对象参数和返回void。记住,使用lambda表达式时,你需要实现一个功能性接口。在这个例子中,你需要一个包含一个有一个Person参数和返回void的抽象方法的功能性接口。Consumer< T >接口包含一个accept(T t)方法,这方法有这些字符,下面的例子使用Cuonsumer< Person>调用accept方法来取代调用p.printPerson():

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中调用printPersons方式是一样的:

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

如果你想在成员特性中处理更多的事情而不仅仅是打印出信息会怎么样?又或者假设你想要校验成员的特性或者检索他们的联系信息?在这个例子中,你需要一个功能性接口,这接口包含一个有返回值的抽象方法,Function

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

下面的例子将从那些符号义务兵役的roster中检索出他们的邮箱地址并打印出来:

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

方法8:使用泛型增加扩展性
重构方法

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. 从集合source中获取一个对象。在这个例子中,从集合roster中获取一个Person类型的对象。注意集合roster是List类型的,也是迭代器Iterable的一个对象
  2. 过滤出满足Predicate类型tester条件的对象。在这个例子中,Predicate对象是一个lambda表达式,它定义了满足义务兵役标准的条件
  3. 将过滤后的对象映射到Function对象mapper。在这个例子中,Function对象是一个lambda表达式,它返回成员的邮箱地址
  4. 在每个映射对象上执行一个指定的Consumer对象block。在这个例子中,Consumer对象是一个lambda表达式,打印出由Function对象返回的邮箱地址字符串。

你可以使用一个完整测操作来替代上述一些列操作
方法9:用lambda表达式作为参数的聚合操作

下面例子使用聚合操作来打印集合roster中符合义务兵役成员的邮箱地址:

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中的操作:

processElemnets 处理 聚合操作 获取一个源对象 Stream< E> stream() 过滤匹配Predicate对象的对象 Stream< T> filter(Predicate predicate) 将匹配对象映射到由Function对象定义的对象中 < R> Stream< R> map(Function mapper) 执行Consumer对象定义的操作 void forEach(Consumer action)

在GUI应用中的Lambda表达式
为了在一个用户图形界面中处理事件,如键盘事件,鼠标事件,你会使用创建事件处理器,这通常包含实现一个特定的接口,通常时间接口都是功能性结构,他们趋向于只有一个方法。
在JavaFX例子HelloWorld.java中

btn.setOnAction(    new EventHandler<ActionEvent>(){        System.out.println("hello world!");    });

btn.setOnAction 定义了选中btn对象发生的事件,这方法需要一个EvenetHandle< ActionEvent>类型的对象,接口EventHandler< ActionEvent>仅包含一个方法 void handle(T event).这是一个功能性接口。所以你可以使用lambda表达式来替代它:

btn.setOnAction(event -> System.out.println("hello world!"));

Lambda表达式语法
一个lambda表达式包含以下部分:
注意:可以忽略掉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

如果你定义一个简单表达式,在运行时计算表达式并返回它的值,或者你可以使用一个返回语句:

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

一个返回语句不是一个表达式;在一个lambda表达式中,你必须使用花括号将它封装起来。然而,你不需要使用花括号封装一个返回控制的方法,如:

email -> System.out.println(email)

注意,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 addtion = (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表达式定义了两个操作,addtion和subtraction。打印出如下结果:

40 + 2 =4220 - 10 =10

访问作用域中的局部变量
类似局部类和匿名类,lambda表达式可以获取变量;他们同样可以访问作用域中的局部变量。然而,不同于内部类和匿名类,lambda表达式没有任何shadowing问题。lambda表达式是语法上的范围。这意味着,他们不能继承从上一级来的任何名称或者定义一个新的范围。lambda表达式中的声明范围如同他们被封装范围一样。下面例子:

import java.util.function.Consumer;public class LambdaScopeTest{    public int x = 0;    void methodInFirstLevel(int x){        Consumer<Integer> myConsumer =(y) ->        {            System.out.println("x = "+x);            System.out.println("y = "+y);            System.out.println("this.x = "+this.x);            System.out.println("LambdaScopeTests.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 = 23y = 23this.x = 1LambdaScopeTest.this.x = 0

如果你使用参数替代y,如下所示,编译将报错:

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

编译错误信息为“变量x已经在方法methodInFirstLevel(int)中定义”,因为lambda表达式不能声明一个新的范围级别。因此你可以直接访问作用域中的属性、方法和本地变量。例如,lambda表达式直接访问方法methodInFirstLevel参数x,若要访问类中变量,使用关键字this,这个例子中的this.x引用了成员变量FirstLevel.x。

类似局部类和匿名类,lambda表达式只能访问局部变量和封装块中的final参数,假设你添加如下声明:

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

由于这任务声明,变量FirstLevel.x不再有效。相反,编译器将会报“用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)

当Java运行时调用printPersons,它希望一个CheckPerson数据类型,那么这个lambda表达式就是这种类型。然而,当Java运行时调用printPersonsWithPredicate,它希望一个Predicate< Person>数据类型,那么这个lambda表达式就是这种类型。这些方法所需要的数据类型被称为目标类型。Java编译器使用上下文的目标类型或者这个lambda表达式使用情景来决定一个lambda表达式类型。

  • 变量声明
  • 任务
  • 返回值
  • 数组初始化
  • 方法或者构造参数
  • lambda表达式主体
  • 条件表达式,?
  • 转换表示大

目标类型和方法参数
对于方法参数,Java编译器根据其他两种语言特性来决定目标类型,分别为重载和类型参数引用
如下两个功能性接口(java.lang.Runnable和java.util.concurrent.Callable< V>):

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

方法Runnable.run 没有返回值,Callable< V>.class 则有。
假设你有重载方法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表达式。然而,和内部类一样,lambda表达式序列化不被推荐使用。

1 0
原创粉丝点击