Java8新特性之Lambda表达式

写久了java,总是给人一种很啰说的感觉。最近使用java 8以后,这个感觉明显变了。Java 8中增加了几个非常有用的特性,像Stream,Lambda表达式,以及函数的引用。这些新的特性,使得java的代码更加紧凑,也更容易理解。更短的代码意味着,我们只要做很少的修改就能完成很多新的问题。

Lambda 表达式

Java 的tutorial上给了一个很好的例子, 我们就用这个例子来介绍Lambda表达式吧。

1. 简单地有一个Person类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Person {

 public enum Sex {
   MALE, FEMALE
 }

 String name;
 LocalDate birthday;
 Sex gender;
 String emailAddress;
 int age;

 public int getAge() {
   return age;
 }

 public void printPerson() {
   System.out.println(toString());
 }
 
 @Override
 public String toString() {
   return "Name :" + name + " " +
           "Birthday :" + birthday + " " +
           "Gender :" + gender + " " +
           "Email Address :" + emailAddress + " " +
           "Age :" + age;
 }

这个类很简单,有名字,生日,年龄和email,函数也就是打印而已。

如果我们想打印所有年龄大于一定数值的,可以这么写:

1
2
3
4
5
6
7
public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

这个函数很好写,但是如果我想改一下要求,打印一定年龄段的所有的人,那么我们就需要改一下。

2. 稍微再通用化一点
1
2
3
4
5
6
7
8
public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

通过代入参数,我们的函数可以稍微的好了一点。但是呢,这个还是只能操作年龄。如果我们想换一个条件,那么就麻烦了。

3. 使用Local class

那么好吧,这个时候我们能想到的是定义一个接口。类似Comparable那种,然后自己去给出具体的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
 }
}

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

通过上面的接口,如果我想进一步优化代码的话,那么我们可以使用Anonymous class.

1
2
3
4
5
6
7
8
9
10
printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

Anonymous Class使我们可以不用再定义类,而是直接重写相应的函数就好了。这个基本上就是Java 8 之前的最好的办法了吧。但是呢,我们发现,我们还是重复地写了很多没有用的code。每一次都要写test的定义,难道编译器就不能帮我们把这个部分infer出来吗?这样我们不是省了很多的事。嗯,接下来就是Lambda表达式出场了。

5. 使用Lambda表达式
1
2
3
4
5
6
printPersons(
       roster,
       (Person p) -> p.getGender() == Person.Sex.MALE
               && p.getAge() >= 18
               && p.getAge() <= 25
);

这个就是Lambda表达式啦。我们只要写好函数的主体部分,其他的部分编译器自动的会帮我们做好infer。那我们在什么情况下,才可以使用Lambda表达式呢?CheckPerson是一个functional interface,对于functional interface,我们就可以使用Lambda表达式了。那什么是functional interface呢?就是只有一个abstract method的interface。注意,他是可以有其他的default或者static的方法的。

6. 使用标准的Functional Interface
1
2
3
4
5
6
7
8
9
10
11
12
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();
        }
    }
}

像CheckPerson这么简单的functional interface,JDK已经帮我们预先写好了一个,就是Predict接口。在我们使用的时候,可以直接使用Predict就好了。

7. 写更多的Lambda表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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());

通过上面的方法,我们不仅可以使用printPerson函数,我们甚至可以使用Person中任意一个函数。更进一步,我们先通过map的方式,将Person对象转化成为另外一个string对象。然后再对map的对象进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
        }
    }
}
processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);
8.使用泛型编程

看到上面的定义,我们就会嘀咕,如果我们map出来的对象不是string怎么办?假如我想使用的函数不是println怎么办?那我们是不是又要重写啦?这个时候,我们又想到了泛型编程这个老话题。Lambda表达式+泛型编程,简直完美。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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. 终极完全版

其实上面的一切都是假的。忘记吧。我们直接使用stream就好了。不过通过上面这样一轮迭代,我们知道了stream到底是如何实现的。这样我们在使用的时候也更加地明白。我相信在我们自己定义的时候,我们也能够定义出更加灵活的Lambda表达式。

1
2
3
4
5
6
7
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));

是的,通过就这么一行的代码,我们几乎可以完成之前10行甚至20行代码可以完成的事情。而且,在我们需要有什么改变的时候,重写代码也是非常的方便。如果使用Method inference,上面的代码甚至可以更简单,如下:

1
2
3
4
5
6
7
8
roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(Person::getEmailAddress)
    .forEach(System.out::println);

总体上来看,Java 8完全是进入到了一个新的台阶,但是还是太慢了,只是在初步阶段。Scala已经都走出去这么远了,Java 8需要更多新的特性。

参考资料

pipelines

streams

method references

Lambda Expressions