Java 8 Lambda表达式详细解析(一)

Java 8开始引入Lambda表达式。官网介绍:
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

匿名内部类的介绍:
https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html

嵌套类的介绍:
https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html#shadowing

以下为对官网介绍的理解翻译。

以前可以使用匿名类的方式让语法变得更加简洁,不用重新定义一个新的类,比如接口回调,很多情况下,接口中就定义了一个方法,那么这样会使得匿名内部类的语法显得笨拙。

官方建议,如果接口中有一个以上方法时,使用匿名内部类,如果接口中就只有一个方法,那么就使用Lambda表达式,这样使得语法更加简洁。

官方用一个例子介绍了使用lambda表达式的一个理想场景,假设要查找符合指定条件的人群,通常用以下方法:

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

方法1: 根据一个指定的条件创建一个搜索方法。

传入要查找的List和条件,看似简单,但该方法比较脆弱,如果Person类型修改了,比如age不再是一个int型的,那么所有有关的搜索API都要做出相应的修改,并且该方法增加了不必要的限制性,比如要搜索年龄小于指定值的人,那岂不是又要写一个相似的方法了。

方法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: 在一个内部类中写明搜索方法

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

该方法在一个新的类中指明具体的搜索条件,即具体实现交给一个具体的实现类来做。首先定义一个接口,然后定义一个实现类,其中可以指明具体的搜索条件,比如以下代码查找符合美国服兵役的人员。

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

使用该类的时候,通过以下声明内部类的形式调用。

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

虽然该方法减少了代码的脆弱性(不可扩展性),即改变Person结构的时候不影响现有代码,但是该方法增加了一个接口,每多一个搜索需求,就会多创建一个新的类。因此可以考虑使用匿名内部类替代内部类的方式。

方法4: 在一个匿名内部类中写明搜索方法。

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接口是一个函数式接口( functional interface),函数式接口即接口中只有一个abstract方法的接口,当然从Java8开始函数式接口可以有其他的default或者static的方法。由于函数式接口中只有一个abstract方法,通过使用lambda表达式可以省略该方法名,代码如下所示:

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

通过使用标准的函数式接口standard functional interface,可以进一步减少代码

方法6: 通过使用标准函数式接口和lambda表达式指明搜索方法。

由于CheckPerson接口中只有一个方法,其太过于简单以至于根本没有必要单独写一个接口类,在jdk8的java.util.function包中预定义了许多标准的函数式接口,比如可以用以下预定义的Predicate接口替换CheckPerson接口:

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

因此修改后的方法定义为:

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

当然这不是使用lambda表达式唯一的场景,以下方法7至9介绍了其他的一些使用场景,可以使得lambda表达式广泛应用于整个项目中各个地方。

方法7: 在整个项目中普遍应用lambda表达式。

重新思考以下代码:

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

如果针对符合条件的这些结果,除了打印出来之外,还有一些其他的操作需求,比如查找出他们的Email,那么可以使用lambda表达式来明确这些操作。

注:使用lambda表达式,必须要实现一个相应的函数式接口,在这个例子中,需要一个这样的函数式接口,持有一个person的引用,并且返回void值, Consumer <T>  接口中有一个方法void accept(T t),符合上述特性需求,所以可以将对搜索结果的操作通过block来指定,对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(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);
如果要对搜索结果进行其他操作,比如获取搜索结果中的人的联系信息,Function<T,R> 接口中有一个方法R apply(T t),可以有返回值,如下代码,通过mapper可以取得数据,然后通过block指定对数据的操作:
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: 将泛型更加广泛的应用于lambda表达式。

上述方法7中指定了操作Person类型的集合,并且对搜索结果操作的数据类型为String,通过使用泛型,可以使得上述代码更加具有通用性,如下:

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

方法9: 使用接收lambda表达式作为参数的聚合操作。

上述方法8中的调用形式可以修改为使用聚合操作的方式,如下:

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

其中stream()获得操作对象的数据源,filter()是对符合Predicate条件的对象进行过滤,map()是根据Function的描述将过滤后的结果取出需要的值,forEach()是对Consumer对象指定的操作进行处理。
filter、map、forEach都是聚合操作,聚合操作处理stream流中的数据,并不是处理collection中的数据,一个stream流是一系列元素的集合,但其中存储的并不是某一数据结构,stream通过一个管道从一个collection中运输数据,在管道中的是一系列数据操作,比如在本例中为filter-> map-> forEach,其中第一个stream()方法的目的就是做这个工作。

聚合操作接受lambda表达式作为参数的特性,使得你可以定制对collection数据的各类操作。

在GUI程序中使用lambda表达式

例如在android开发中,会有如下的写法:

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) { 
    System.out.println("Hello World!")
    }
}

使用lambda表达式替换上述匿名内部类的形式,简化后的代码为:

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

Lambda表达式的语法

1 一个在圆括号里的,以逗号分隔开的传入形参,在此例中,CheckPerson.test方法有一个参数p,代表Person类型的一个实例,
注:可以在lambda表达式中省略参数的类型,此外如果只有一个参数,还可以省略圆括号,如下形式的代码也是合法的(其实我没看出怎么省略的…)

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

2 - >箭头符号,
3 方法体body部分,可以由一个简单的表达式或者一个代码段表示,在这个例子中为:

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

如果body部分为一个简单的表达式,那么Java运行时就会求出表达式的值并返回,或者可以直接使用return声明:

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

return声明不是语句,在lambda表达式中,必须加{},如果方法返回值为void,那么也可以不用加{},如下所示:

email -> System.out.println(email)

lambda表达式看起来就像是方法声明,我们可以把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 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));    
    }
}

关于进一步介绍Lambda语法和方法引用的内容参考以下大神的博客:
https://my.oschina.net/benhaile/blog/175012
https://my.oschina.net/benhaile/blog/177148

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/52986389