Java lambda expression

Lambda表达式

匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。Lambda表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。

Lambda表达式

匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。Lambda表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。

上一节“ 匿名类 ”向您展示了如何在不给它命名的情况下实现基类。虽然这通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过分和繁琐。Lambda表达式允许您更紧凑地表达单方法类的实例。

Lambda表达式的理想用例

假设您正在创建社交网络应用程序。您希望创建一项功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。下表详细描述了此用例:

领域描述
名称 对选定的成员执行操作
主要演员 管理员
前提条件 管理员已登录系统。
后置条件 仅对符合指定条件的成员执行操作。
主要成功案例
  1. 管理员指定要执行特定操作的成员的条件。
  2. 管理员指定要对这些选定成员执行的操作。
  3. 管理员选择“ 提交”按钮。
  4. 系统将查找与指定条件匹配的所有成员。
  5. 系统对所有匹配成员执行指定的操作。
扩展

1A。管理员可以选择在指定要执行的操作之前或选择“ 提交”按钮之前预览符合指定条件的成员

发生频率 白天很多次。

假设此社交网络应用程序的成员由以下Person表示 

公共类人{

    public enum Sex {
        男,女
    }

    字符串名称;
    LocalDate生日;
    性别;
    字符串emailAddress;

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

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

假设您的社交网络应用程序的成员存储在一个List<Person>实例中。方法我介绍一种,其他的可以查看其官网。

创建搜索匹配一个特征的成员的方法

一种简单的方法是创建几种方法; 每种方法都会搜索与一个特征匹配的成员,例如性别或年龄。以下方法打印超过指定年龄的成员:

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

注意:A List是有序的 Collection集合是一个对象,该组中的多个元素到单个单元中。集合用于存储,检索,操作和传递聚合数据。有关集合的更多信息,请参阅 集合跟踪。

这种方法可能会使您的应用程序变得脆弱,这是由于引入了更新(例如更新的数据类型)导致应用程序无法工作的可能性。假设您升级应用程序并更改Person的结构,使其包含不同的成员变量; 也许该类记录和测量年龄与不同的数据类型或算法。您必须重写大量API以适应此更改。此外,这种方法是不必要的限制; 例如,如果您想要打印年龄小于某个年龄的成员,该怎么办?

Lambda表达式的语法

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

    如果指定单个表达式,则Java运行时将计算表达式,然后返回其值。或者,您可以使用return语句:

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

    return语句不是表达式; 在lambda表达式中,必须用braces({}括起语句但是,您不必在大括号中包含void方法调用。例如,以下是有效的lambda表达式:

    电子邮件 - > System.out.println(电子邮件)

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

以下示例 Calculator是一个lambda表达式的示例,它采用多个形式参数:


公共类计算器{
  
    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){
    
        计算器myApp = new Calculator();
        IntegerMath add =(a,b) - > a + b;
        IntegerMath减法=(a,b) - > a  -  b;
        System.out.println(“40 + 2 =”+
            myApp.operateBinary(40,2,另外));
        System.out.println(“20  -  10 =”+
            myApp.operateBinary(20,10,减法));    
    }
}

该方法operateBinary对两个整数操作数执行数学运算。操作本身由实例指定IntegerMath的例子中定义了lambda表达式两个操作,additionsubtraction该示例打印以下内容:

40 + 2 = 42
20  -  10 = 10

访问封闭范围的局部变量

像本地和匿名类一样,lambda表达式可以 捕获变量 ; 它们对封闭范围的局部变量具有相同的访问权限。但是,与本地和匿名类不同,lambda表达式没有任何阴影问题(有关更多信息,请参阅 阴影)。Lambda表达式是词法范围的。这意味着它们不会从超类型继承任何名称或引入新级别的范围。lambda表达式中的声明与封闭环境中的声明一样被解释。以下示例 LambdaScopeTest演示了这一点:


import java.util.function.Consumer;

公共类LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x){
            
            //以下语句导致编译器生成
            //错误“从lambda表达式引用的局部变量
            //必须是最终的或有效的最终“在声明A中:
            //
            // x = 99;
            
            消费者<整数> myConsumer =(y) - > 
            {
                System.out.println(“x =”+ x); //声明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代替,则编译器会生成错误:ymyConsumer

消费者<整数> myConsumer =(x) - > {
    // ...
}

编译器生成错误“变量x已在方法methodInFirstLevel(int)中定义”,因为lambda表达式不会引入新的作用域级别。因此,您可以直接访问封闭范围的字段,方法和局部变量。例如,lambda表达式直接访问x方法的参数methodInFirstLevel要访问封闭类中的变量,请使用关键字this在此示例中,this.x引用成员变量FirstLevel.x

但是,与本地和匿名类一样,lambda表达式只能访问最终或有效最终的封闭块的局部变量和参数。例如,假设您在methodInFirstLevel定义语句后立即添加以下赋值语句:

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

由于这个赋值语句,变量FirstLevel.x不再是有效的最终结果。因此,Java编译器生成类似于“从lambda表达式引用的局部变量必须是final或者final final”的错误消息,其中lambda表达式myConsumer尝试访问FirstLevel.x变量:

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

目标打字

你如何确定lambda表达式的类型?回想一下lambda表达式,它选择了男性和年龄在18到25岁之间的成员:

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

这个lambda表达式用于以下两种方法:

当Java运行时调用该方法时printPersons,它期望数据类型为CheckPerson,因此lambda表达式属于此类型。但是,当Java运行时调用该方法时printPersonsWithPredicate,它期望数据类型为Predicate<Person>,因此lambda表达式属于此类型。这些方法所期望的数据类型称为目标类型要确定lambda表达式的类型,Java编译器将使用发现lambda表达式的上下文或情境的目标类型。因此,您只能在Java编译器可以确定目标类型的情况下使用lambda表达式:

  • 变量声明

  • 分配

  • 退货声明

  • 数组初始化器

  • 方法或构造函数参数

  • Lambda表达体

  • 条件表达式, ?:

  • 转换表达式

目标类型和方法参数

对于方法参数,Java编译器使用另外两种语言特性确定目标类型:重载解析和类型参数推断。

考虑以下两个功能接口( java.lang.Runnable和 java.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

公共接口Callable <V> {
    V call();
}

该方法Runnable.run不返回值,而是返回值Callable<V>.call

假设您已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表达式进行序列化。

各种方法参考

有四种方法参考:

参考静态方法 ContainingClass::staticMethodName
引用特定对象的实例方法 containingObject::instanceMethodName
引用特定类型的任意对象的实例方法 ContainingType::methodName
引用构造函数 ClassName::new

参考静态方法

方法引用Person::compareByAge是对静态方法的引用。

引用特定对象的实例方法

以下是对特定对象的实例方法的引用示例:

class ComparisonProvider {
    public int compareByName(Person a,Person b){
        return a.getName()。compareTo(b.getName());
    }
        
    public int compareByAge(Person a,Person b){
        return a.getBirthday()。compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray,myComparisonProvider :: compareByName);

方法引用myComparisonProvider::compareByName调用compareByName作为对象一部分的方法myComparisonProviderJRE推断出方法类型参数,在本例中是(Person, Person)

对特定类型的任意对象的实例方法的引用

以下是对特定类型的任意对象的实例方法的引用示例:

String [] stringArray = {“芭芭拉”,“詹姆斯”,“玛丽”,“约翰”,
    “Patricia”,“Robert”,“Michael”,“Linda”};
Arrays.sort(stringArray,String :: compareToIgnoreCase);

方法引用的等效lambda表达式String::compareToIgnoreCase将具有形式参数列表(String a, String b),其中ab是用于更好地描述此示例的任意名称。方法引用将调用该方法a.compareToIgnoreCase(b)

参考构造函数

您可以使用名称以与静态方法相同的方式引用构造函数new以下方法将元素从一个集合复制到另一个集合:

public static <T,SOURCE扩展Collection <T>,DEST扩展Collection <T >>
    DEST transferElements(
        SOURCE sourceCollection,
        供应商<DEST> collectionFactory){
        
        DEST result = collectionFactory.get();
        for(T t:sourceCollection){
            result.add(T);
        }
        返回结果;
}

功能接口Supplier包含一个get不带参数并返回对象的方法。因此,您可以transferElements使用lambda表达式调用该方法,如下所示:

设置<Person> rosterSetLambda =
    transferElements(roster,() - > {return new HashSet <>();});

您可以使用构造函数引用代替lambda表达式,如下所示:

设置<Person> rosterSet = transferElements(roster,HashSet :: new);

Java编译器推断您要创建HashSet包含类型元素的集合Person或者,您可以按如下方式指定:

设置<Person> rosterSet = transferElements(名册,HashSet <Person> :: new);

 

posted @ 2019-04-24 15:39  山水心  阅读(132)  评论(0编辑  收藏  举报