美文网首页
Lambda 表达式

Lambda 表达式

作者: bern85 | 来源:发表于2019-07-26 16:21 被阅读0次

    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表达式包含以下内容:

    • 括号中用逗号分隔的形式参数列表。 CheckPerson.test方法包含一个参数p,它表示Person类的实例。

    Note: 你可以省略lambda表达式中参数的数据类型。 此外,如果只有一个参数,则括号也可以省略。 例如,以下lambda表达式也是有效的:

    p -> p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    
    • 箭头标记,->
    • 一个body,由单个表达式或语句块组成。 此示例使用以下表达式:
    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
    

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

    • public static void printPersons(List<Person> roster, CheckPerson tester) #方法3

    • public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) #方法6

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

    • 变量声明
    • 赋值声明
    • 返回声明
    • 数组初始化
    • 方法参数或构造函数参数
    • 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>.

    相关文章

      网友评论

          本文标题:Lambda 表达式

          本文链接:https://www.haomeiwen.com/subject/zbcwlctx.html