作为Java世界的大事件Java 8终于在2014年3月18日发布了。在Java 8中,最令人期待的新特性,就属Lambda表达式的支持。其实Lambda在一些脚本语言(如Python,Ruby)中早已存在,但是对于Java程序员来说,这还是新鲜事。Lambda是函数式编程的基础。对习惯Java语法的朋友们来说,理解Lambda有点小困难,至少我是花了不少时间才搞懂的。当然,如果你是从脚本语言开始学习的,估计就不成问题了。本文的主要目的,就是帮助Java程序员,理解Lambda表达式。

Java中多线程的实现,我假设大家都知道。一般的做法是创建一个类去实现Runnable接口,实现其中的run方法。再通过这个类的对象创建一个线程对象。然后再调用这个对象的start()函数。

public class ThreadTest implements Runnable {
    public void run() {
        System.out.println("I'm running!");
    }
}

...

ThreadTest tt = new ThreadTest();
Thread t = new Thread(tt);
t.start();

这样写很繁琐,所以大部分程序员都会用匿名内部类。

Thread t = new Thread(new Runnable {
    public void run() {
        System.out.println("I'm running!");
    }
});

t.start();

上面的代码看上去简洁不少,但还是有些晦涩。当Lambda出现后,你所要做的就是一行代码。

Thread t = new Thread(() -> System.out.println("I'm running!"));
t.start();

让我们看看发生了什么。对于只有一个显式声明的抽象方法的接口,Java 8引入了一个新的概念,叫”函数接口(functional interface)“。如果你自己创建,建议用@FunctionalInterface标注出来,当然不标也不会报错。而Lambda表达式的作用,就是可以为这个函数接口赋值,同样来说,也就可以替代匿名内部类的作用。如上面线程的例子,你就可以用下面的表达式为Runnable接口赋值。

Runnable r = () -> System.out.println("I'm running!");

详细分析下等号右边这段表达式,把二元操作符->两边的内容分别来看

  • 左边() 等同于函数接口中唯一的那个抽象函数的参数。因为Runnablerun()函数没有参数,所以就写成()。该函数有多少参数,就写多少,比如(x, y, z) -> xxx

  • 右边System.out.println("I'm running!");就是那个抽象函数的实现。这个部分,可以包含多条语句,需要用{}括起来,并用分号分隔。如

button.addActionListener(e -> {
    ui.dazzle(e.getModifiers());
    ui.showSomething();
});

注意,这边建议即使只有一条语句,也要加上{}。就像Java代码规范里,if语句后面一样。这样做,是为了避免将来代码改变可能带来的隐患。

  • 如果需实现的抽象函数有返回值,那么右边最后一条语句,也必须返回同样类型的返回值。如果右边只有一条语句,那return关键字就可以省去
File dir = new File("/home/user/test");
File[] files = dir.listFiles((File f) -> {f.isFile();});

留个问题给大家,这里的参数f前面为什么要加File类型声明?(当然,不加也能跑,不过强烈建议加上)

到这里,大家是不是已经领会Lambda表达式的使用了?那么有人不禁要问,这个Lambda除了写起来简洁一些,它到底有什么作用呢?这边说下我的理解:

  • 减少无意义的类的创建
    如果你有一个Animal接口如下,里面包括一个”吼叫”的函数
public interface Animal {
    public void roar() ;
}

你要创建几种不同的小动物,那你就要创建好几个类

public class Cat implements Animal {
    public void roar() {
        System.out.println("Miu Miu!");
    }
}

public class Dog implements Animal {
    public void roar() {
        System.out.println("Wow Wow!");
    }
}

public class Cow implements Animal {
    public void roar() {
        System.out.println("Moo Moo!");
    }
}

…

Cat cat = new Cat();
Dog dog = new Dog();
Cow cow = new Cow();

这看上去多麻烦啊。让我们用Lambda试试:

Animal cat = () -> System.out.println("Miu Miu!");
Animal dog = () -> System.out.println("Wow Wow!");
Animal cow = () -> System.out.println("Moo Moo!");

立马代码简化了好多。如果这些类不常用,使用Lambda会省不少事。

  • 传递操作,而不只是传递数据
    有点类似于设计模式中的策略模式,我把程序的流程写好,但是中间核心逻辑留给别人来实现。
public interface NumberValidation {
    public boolean validate(int num) ;
}

…

public int sum(List<Integer> numbers, NumberValidation v) {
    int total = 0;
    // 对集合中的数求和
    for (int number: numbers) {
        // 过滤数字,但此时并不知道过滤的逻辑是什么
        if (v.validate(number)) {
            total += number;
        }
    }

    return total;
}

所以我们就可以用下面方法对不同情况求和: 求奇数和

sum(numbers, n -> n % 2 == 1);

求偶数和

sum(numbers, n -> n % 2 == 0);

求小于10的数的和

sum(numbers, n -> n < 10);
  • 流式操作
    Java 8为集合类引入了stream(流)的概念。一个流以集合的实例作为输入数据源,然后像管道一样从一个操作流到另一个操作。每个操作返回另一个流,并作为下个操作的输入。每一步操作都非常原子,通常用一个Lambda表达式来实现。大家看看下面的例子:
List<Integer> numbers = xxx;               // 这里假设创建一个整数集合从1到10
numbers.stream()                           // 将集合变成一个流,也就是集合里的数据会一条一条从这里流出
    .filter(n -> n % 2 == 0)               // 对每条数据过滤,由Lambda表达式实现,这里是取偶数
    .map(n -> n * n)                       // 对每条数据做映射,这里是将原来的数据映射成其平方值
    .forEach(n -> System.out.println(n));  // 对每条数据做打印。

在这个例子中,集合类对象numbers作为数据源,通过stream()方法生成一个流;filter()方法过滤出偶数,然后返回新的流,并作为map()方法的输入;map()方法将每个数映射到它的平方,然后返回另一个新的流,并作为forEach()方法的输入;最后由forEach()方法将结果打印出来。forEach()不再返回流了。上面这段程序的最后输出就是:

4
16
36
64
100

这是一个典型的函数式编程的例子。函数式编程是一种思想,这篇我就不多解释了,感兴趣的朋友们可以学习下Groovy或Scala语言来深入了解其中的奥秘。

到这里,我们介绍了Lambda表达式在Java中的使用,及其价值,大家是不是觉得挺有趣的呢?当然使用Lambda表达式还有很多好处,里面的知识点也远不止我写的这些。这里就不一一赘述了。现在Lambda相关的参考资料很多,想深入学习的话,大家可以多在网上找找。