Java世界里的Lambda
作为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!");
详细分析下等号右边这段表达式,把二元操作符->
两边的内容分别来看
左边
()
等同于函数接口中唯一的那个抽象函数的参数。因为Runnable
中run()
函数没有参数,所以就写成()
。该函数有多少参数,就写多少,比如(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相关的参考资料很多,想深入学习的话,大家可以多在网上找找。