Java 8 Lambda表达式一看就会

匿名内部类的一个问题是:当一个匿名内部类的实现很是简单,好比说接口只有一个抽象函数,那么匿名内部类的语法有点笨拙且不清晰。咱们常常会有传递一个函数做为参数给另外一个函数的实际需求,好比当点击一个按钮时,咱们须要给按钮对象设置按钮响应函数。lambda表达式就能够把函数当作函数的参数,代码(函数)当作数据(形参),这种特性知足上述需求。当要实现只有一个抽象函数的接口时,使用lambda表达式可以更灵活。java

使用Lambda表达式的一个用例

假设你正在建立一个社交网络应用。你如今要开发一个可让管理员对用户作各类操做的功能,好比搜索、打印、获取邮件等操做。假设社交网络应用的用户都经过Person类表示:算法

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    private String name;
    
    private LocalDate birthday;

    private Sex gender;
    
    private String emailAddress;

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

    public void printPerson() {
        // ...
    }
}
复制代码

假设社交网络应用的全部用户都保存在一个 List<Person>的实例中。express

咱们先使用一个简单的方法来实现这个用例,再经过使用本地类、匿名内部类实现,最终经过lambda表达式作一个高效且简洁的实现。编程

方法1:建立一个根据某一特性查询匹配用户的方法

最简单的方式是建立几个函数,每一个函数搜索指定的用户特征,好比searchByAge()这种方法,下面的方法打印了年龄大于某特定值的全部用户:微信

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

这个方法是有潜在的问题的,若是引入一些变更(好比新的数据类型)这个程序会出错。假设更新了应用且变化了Person类,好比使用出生年月代替了年龄;也有可能搜索年龄的算法不一样。这样你将不到再也不写许多API来适应这些变化。网络

方法2:建立一个更加通用的搜索方法

这个方法比起printPersonsOlderThan更加通用;它提供了能够打印某个年龄区间的用户:数据结构

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类,添加其余属性,好比恋爱状态、地理位置呢?尽管这个方法比printPersonsOlderThan方法更加通用,可是每一个查询都建立特定的函数都是有能够致使程序不够健壮。你可使用接口将特定的搜索转交给须要搜索的特定类中(面向接口编程的思想——简单工厂模式)。闭包

方法3:在本地类中设定特定的搜索条件

下面的方法能够打印出符合搜索条件的全部用户信息app

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

复制代码

这个方法经过调用tester.test方法检测每一个roster列表中的元素是否知足搜索条件。若是tester.test返回true,则打印符合条件的Person实例。ide

经过实现CheckPerson接口实现搜索。

interface CheckPerson {
    boolean test(Person p);
}
复制代码

下面的类实现了CheckPerson接口的test方法。若是Person的属性是男性而且年龄在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表达式取代匿名内部类表达式,像下面这样调用:

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

lambda表达式的语法后面会作详细介绍。你还可使用标准的函数式接口取代CheckPerson接口,这样会进一步减小代码量。

方法6:使用标准的函数式接口和Lambda表达式

CheckPerson接口是一个很是简单的接口:

interface CheckPerson {
    boolean test(Person p);
}
复制代码

它只有一个抽象方法,所以它是一个函数式接口。这个函数有个一个参数和一个返回值。它太过简单以致于没有必要在你应用中定义它。所以JDK中定义了一些标准的函数式接口,能够在java.util.function包中找到。好比,你可使用Predicate<T>取代CheckPerson。这个接口中只包含boolean test(T t)方法。

interface Predicate<T> {
    boolean test(T t);
}
复制代码

Predicate<T>是一个泛型接口,泛型须要在尖括号(<>)指定一个或者多个参数。这个接口中只包换一个参数T。当你声明或者经过一个真实的类型参数实例化泛型后,你将获得一个参数化的类型。好比,参数化后的类型Predicate<Person>像下面代码所示:

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();
        }
    }
}
复制代码

那么,能够这样调用这个函数:

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

这个不是使用lamdba表达式的惟一的方式。建议使用下面的其余方式使用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();
        }
    }
}
复制代码

这个方法检测roster中的每一个Person实例是否知足tester的标准。若是Person实例知足tester中设定的标准,那么Person实例的信息将会被打印出来。

你能够指定一个不一样的动做来执行打印知足tester中定义的搜索条件的Person实例。你能够指定这个动做是一个lambda表达式。假设你想要一个功能和printPerson同样的lambda表示式(一个参数、返回void),你须要实现一个函数式接口。在这种状况下,你须要一个包含一个只有一个Person类型参数和返回void的函数式接口。Consumer<T>接口包换一个void accept(T t)函数,它符合上述需求。下面的函数使用 Consumer<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);
            }
        }
}
复制代码

那么能够这样调用processPersons函数:

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

复制代码

若是你想对用户的信息进行更多处理而不止打印出来,那该怎么办呢?假设你想验证成员的我的信息或者获取他们的联系人的信息呢?在这种状况下,你须要一个有返回值的抽象函数的函数式接口。Function<T,R>接口包含了R apply(T t)方法,有一个参数和一个返回值。下面的方法获取参数匹配到的数据,而后根据lambda表达式代码块作相应的处理:

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);
        }
    }
}
复制代码

下面的函数从roster中获取符合搜索条件的用户的邮箱地址,并将地址打印出来。

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

方法8:使用泛型使之更加通用

再处理processPersonsWithFunction函数,下面的函数能够接受包含任何数据类型的集合:

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实例的roster集合。roster是一个List类型,同时也是一个Iterable类型。
  2. 过滤符合Predicate数据类型的tester的对象。在这个例子中,Predicate对象是一个指定了符合搜索条件的lambda表达式。
  3. 使用Function类型的mapper映射每一个符合过滤条件的对象。在这个例子中,Function对象时要给返回用户的邮箱地址。
  4. 对每一个映射到的对象执行一个在Consumer对象块中定义的的动做。在这个例子中,Consumer对象时一个打印Function对象返回的电子邮箱的lamdba表达式。

你能够经过一个聚合操做取代上述操做。

方法9:使用lambda表达式做为参数的合并操做

下面的例子使用了聚合操做,打印出了符合搜索条件的用户的电子邮箱:

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 stream()
过滤符合Predicate对象(lambda表达式)的实例 Stream filter(Predicate<? super T> predicate)
使用Function对象映射符合过滤标准的对象到一个值 Stream map(Function<? super T,? extends R> mapper)
执行Consumer对象(lambda表达式)设定的动做 void forEach(Consumer<? super T> action)

filter,mapforEach是聚合操做。聚合操做是从stream中处理各个元素的,而不是直接从集合中(这就是为何第一个调用的函数是stream())。steam是对各个元素进行序列化操做。和集合不一样,它不是一个储存数据的数据结构。相反地,stream加载了源中的值,好比集合经过pipeline将数据加载到stream中。pipeline是stream的一种序列化操做,这个例子中的就是filter- map-forEach。还有,聚合操做一般能够接收一个lambda表达式做为参数,这样你可自定义须要的动做。

在GUI程序中使用lambda表达式

为了处理一个图形用户界面(GUI)应用中的事件,好比键盘输入事件,鼠标移动事件,滚动事件,你一般是实现一个特定的接口来建立一个事件处理。一般,时间处理接口就是一个函数式接口,它们一般只有一个函数。

以前使用匿名内部类实现的时间相应:

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

可使用以下代码替代:

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

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运行时将会计算表达式并最终返回结果。同时,你可使用一个返回声明:

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

在lambda表达式中返回的不是一个表达式,那么就必须使用{}将代码块括起来。可是,当返回的是一个void类型时则不须要括号。好比,下面的也是一个合法的lambda表达式:

email -> System.out.println(email)
复制代码

lambda表达式看起来有点像声明函数,能够把lambda表达式看作是一个匿名函数(没有名称的函数)。

下面是一个有多个形参的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
复制代码

获取闭包中的本地变量

像本地类和匿名类同样,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) {
            
            Consumer<Integer> myConsumer = (y) -> 
            {
                System.out.println("x = " + x); 
                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表达式所在做用域已经有x被定义了)。所以,能够直接访问lambda表达式所在的闭包的成员变量、函数和闭包中的本地变量。好比,lambda表达式能够直接访问方法methodInFirstLevel的参数x。可使用this关键字访问类级别的做用域。在这个例子中this.x对成员变量FirstLevel.x的值。

然而,像本地和匿名类同样,lambda表达式值能够访问被修饰成final或者effectively final的本地变量和形参。好比,假设在methodInFirstLevel中添加定义声明以下:

Effectively Final:一个变量或者参数的值在初始化后就不在发生变化,那么这个变量或者参数就是effectively final类型的。

void methodInFirstLevel(int x) {
    x = 99;
}
复制代码

因为x =99的声明使methodInFirstLevel的形参x再也不是effectively final类型。结果java编译器就会报相似"local variables referenced from a lambda expression must be final or effectively final"的错误。

目标类型

在运行时java是怎么判断lambda表达式的数据类型的?再看一下那个要选择性别是男性,年龄在18到25岁之间的lambda表达式:

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

这个lambda表达式已参数的形式传递到以下两个函数:

  • public static void printPersons(List roster, CheckPerson tester)
  • public void printPersonsWithPredicate(List roster, Predicate tester)

当java运行时调用方法printPersons时,它指望一个CheckPerson类型的数据,所以lambda表达式就是这种类型。当java运行时调用方法printPersonsWithPredicate时,它指望一个Predicate<Person>类型的数据,所以lambda表达式就是这样一个类型。这些方法指望的数据类型就叫目标类型。为了肯定lambda表达式的类型,java编译器会在lambda表达式的的上下文中判断它的目标类型。只有java编译器可推测出来了目标类型,lambda表达式才能够被执行。

目标类型和函数参数

对于函数参数,java编译器能够肯定目标类型经过两种其余语言特性:重载解析和类型参数推断。看下面两个函数式接口( java.lang.Runnable and java.util.concurrent.Callable):

public interface Runnable {
    void run();
}

public interface 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>

最后

感谢阅读,有兴趣能够关注微信公众帐号获取最新推送文章。

微信二维码
相关文章
相关标签/搜索