Java8Lambda表达式教程
1. 什么是λ表达式
λ表达式本质上是⼀个匿名⽅法。让我们来看下⾯这个例⼦:
public int add(int x, int y) {
return x + y;
}
转成λ表达式后是这个样⼦:
(int x, int y) -> x + y;
参数类型也可以省略,Java编译器会根据上下⽂推断出来:
(x, y) -> x + y; //返回两数之和
或者
(x, y) -> { return x + y; } //显式指明返回值
可见λ表达式有三部分组成:参数列表,箭头(->),以及⼀个表达式或语句块。
下⾯这个例⼦⾥的λ表达式没有参数,也没有返回值(相当于⼀个⽅法接受0个参数,返回void,其实就是Runnable⾥run⽅法的⼀个实现):
() -> { System.out.println("Hello Lambda!"); }
如果只有⼀个参数且可以被Java推断出类型,那么参数列表的括号也可以省略:
c -> { return c.size(); }
2. λ表达式的类型(它是Object吗?)
λ表达式可以被当做是⼀个Object(注意措辞)。λ表达式的类型,叫做“⽬标类型(target type)”。λ表达式的⽬标类型是“函数接⼝(functional interface)”,这是Java8新引⼊的概念。它的定义是:⼀个接⼝,如果只有⼀个显式声明的抽象⽅法,那么它就是⼀个函数接⼝。⼀般⽤@FunctionalInterface标注出来(也可以不标)。举例如下:
@FunctionalInterface
public interface Runnable { void run(); }
public interface Callable<V> { V call() throws Exception; }
public interface ActionListener { void actionPerformed(ActionEvent e); }
public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); }
注意最后这个Comparator接⼝。它⾥⾯声明了两个⽅法,貌似不符合函数接⼝的定义,但它的确是函数接⼝。这是因为equals⽅法是Object 的,所有的接⼝都会声明Object的public⽅法——虽然⼤多是隐式的。所以,Comparator显式的声明了equals不影响它依然是个函数接⼝。
你可以⽤⼀个λ表达式为⼀个函数接⼝赋值:
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
然后再赋值给⼀个Object:
Object obj = r1;
但却不能这样⼲:
Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!必须显式的转型成⼀个函数接⼝才可以:
Object o = (Runnable) () -> { System.out.println("hi"); }; // correct
⼀个λ表达式只有在转型成⼀个函数接⼝后才能被当做Object使⽤。所以下⾯这句也不能编译:
可以啪的游戏System.out.println( () -> {} ); //错误! ⽬标类型不明
必须先转型:
System.out.println( (Runnable)() -> {} ); // 正确
假设你⾃⼰写了⼀个函数接⼝,长的跟Runnable⼀模⼀样:
@FunctionalInterface
public interface MyRunnable {
public void run();
}开车过程
那么
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};
争气不要生气都是正确的写法。这说明⼀个λ表达式可以有多个⽬标类型(函数接⼝),只要函数匹配成功即可。
但需注意⼀个λ表达式必须⾄少有⼀个⽬标类型。
JDK预定义了很多函数接⼝以避免⽤户重复定义。最典型的是Function:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
这个接⼝代表⼀个函数,接受⼀个T类型的参数,并返回⼀个R类型的返回值。
另⼀个预定义函数接⼝叫做Consumer,跟Function的唯⼀不同是它没有返回值。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
还有⼀个Predicate,⽤来判断某项条件是否满⾜。经常⽤来进⾏筛滤操作:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
咸鸭蛋怎么腌}
综上所述,⼀个λ表达式其实就是定义了⼀个匿名⽅法,只不过这个⽅法必须符合⾄少⼀个函数接⼝。3. λ表达式的使⽤
3.1 λ表达式⽤在何处
λ表达式主要⽤于替换以前⼴泛使⽤的内部匿名类,各种回调,⽐如事件响应器、传⼊Thread类的Runnable等。看下⾯的例⼦:
Thread oldSchool = new Thread( new Runnable () {
@Override
public void run() {
System.out.println("This is from an anonymous class.");
}
} );
Thread gaoDuanDaQiShangDangCi = new Thread( () -> {
System.out.println("This is from an anonymous method (lambda exp).");
} );
注意第⼆个线程⾥的λ表达式,你并不需要显式地把它转成⼀个Runnable,因为Java能根据上下⽂⾃动推断出来:⼀个Thread的构造函数接受⼀个Runnable参数,⽽传⼊的λ表达式正好符合其run()函数,所以Java编译器推断它为Runnable。
从形式上看,λ表达式只是为你节省了⼏⾏代码。但将λ表达式引⼊Java的动机并不仅仅为此。Java8有⼀个短期⽬标和⼀个长期⽬标。短期⽬标是:配合“集合类批处理操作”的内部迭代和并⾏处理(下⾯将要讲到);长期⽬标是将Java向函数式编程语⾔这个⽅向引导(并不是要完全变成⼀门函数式编程语⾔,只是让它有更多的函数式编程语⾔的特性),也正是由于这个原因,Oracle并没有简单地使⽤内部类去实现λ表达式,⽽是使⽤了⼀种更动态、更灵活、易于将来扩展和改变的策略(invokedynamic)。
3.2 λ表达式与集合类批处理操作(或者叫块操作)
上⽂提到了集合类的批处理操作。这是Java8的另⼀个重要特性,它与λ表达式的配合使⽤乃是Java8的最主要特性。集合类的批处理操作API的⽬的是实现集合类的“内部迭代”,并期望充分利⽤现代多核CPU进⾏并⾏计算。
Java8之前集合类的迭代(Iteration)都是外部的,即客户代码。⽽内部迭代意味着改由Java类库来进⾏迭代,⽽不是客户代码。例如:
for(Object o: list) { // 外部迭代
System.out.println(o);
}
可以写成:
list.forEach(o -> {System.out.println(o);}); //forEach函数实现内部迭代
焖鸡的做法集合类(包括List)现在都有⼀个forEach⽅法,对元素进⾏迭代(遍历),所以我们不需要再写for循环了。forEach⽅法接受⼀个函数接⼝Consumer做参数,所以可以使⽤λ表达式。
这种内部迭代⽅法⼴泛存在于各种语⾔,如C++的STL算法库、、ruby、scala等。
Java8为集合类引⼊了另⼀个重要概念:流(stream)。⼀个流通常以⼀个集合类实例为其数据源,然后在其上定义各种操作。流的API设计使⽤了管道(pipelines)模式。对流的⼀次操作会返回另⼀个流。如同IO的API或者StringBuffer的append⽅法那样,从⽽多个不同的操作可以在⼀个语句⾥串起来。看下⾯的例⼦:
List<Shape> shapes = ...
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.forEach(s -> s.tColor(RED));
⾸先调⽤stream⽅法,以集合类对象shapes⾥⾯的元素为数据源,⽣成⼀个流。然后在这个流上调⽤filter⽅法,挑出蓝⾊的,返回另⼀个流。最后调⽤forEach⽅法将这些蓝⾊的物体喷成红⾊。(forEach⽅法不再返回流,⽽是⼀个终端⽅法,类似于StringBuffer在调⽤若⼲append之后的那个toString)
filter⽅法的参数是Predicate类型,forEach⽅法的参数是Consumer类型,它们都是函数接⼝,所以可以使⽤λ表达式。
还有⼀个⽅法叫parallelStream(),顾名思义它和stream()⼀样,只不过指明要并⾏处理,以期充分利⽤现代CPU的多核特性。
shapes.parallelStream(); // 或shapes.stream().parallel()
来看更多的例⼦。下⾯是典型的⼤数据处理⽅法,Filter-Map-Reduce:
public void numbers) {
List<String> l = Arrays.asList(numbers);
List<Integer> r = l.stream()
.map(e -> new Integer(e))
.filter(e -> Primes.isPrime(e))
.distinct()
.List());
System.out.println("distinctPrimary result is: " + r);
}
第⼀步:传⼊⼀系列String(假设都是合法的数字),转成⼀个List,然后调⽤stream()⽅法⽣成流。
第⼆步:调⽤流的map⽅法把每个元素由String转成Integer,得到⼀个新的流。map⽅法接受⼀个Function类型的参数,上⾯介绍
了,Function是个函数接⼝,所以这⾥⽤λ表达式。
第三步:调⽤流的filter⽅法,过滤那些不是素数的数字,并得到⼀个新流。filter⽅法接受⼀个Predicate类型的参数,上⾯介绍了,Predicate 是个函数接⼝,所以这⾥⽤λ表达式。
清蒸鲈鱼简单做法第四步:调⽤流的distinct⽅法,去掉重复,并得到⼀个新流。这本质上是另⼀个filter操作。
第五步:⽤collect⽅法将最终结果收集到⼀个List⾥⾯去。collect⽅法接受⼀个Collector类型的参数,这个参数指明如何收集最终结果。在这个例⼦中,结果简单地收集到⼀个List中。我们也可以⽤Map(e->e, e->e)把结果收集到⼀个Map中,它的意思是:把结果收到⼀个Map,⽤这些素数⾃⾝既作为键⼜作为值。toMap⽅法接受两个Function类型的参数,分别⽤以⽣成键和值,Function是个函数接⼝,所以这⾥都⽤λ表达式。
你可能会觉得在这个例⼦⾥,List l被迭代了好多次,map,filter,distinct都分别是⼀次循环,效率会不好。实际并⾮如此。这些返回另⼀个Stream的⽅法都是“懒(lazy)”的,⽽最后返回最终结果的collect⽅法则是“急(eager)”的。在遇到eager⽅法之前,lazy的⽅法不会执⾏。
当遇到eager⽅法时,前⾯的lazy⽅法才会被依次执⾏。⽽且是管道贯通式执⾏。这意味着每⼀个元素依次通过这些管道。例如有个元
病毒性感冒和细菌性感冒区别素“3”,⾸先它被map成整数型3;然后通过filter,发现是素数,被保留下来;⼜通过distinct,如果已经有⼀个3了,那么就直接丢弃,如果还没有则保留。这样,3个操作其实只经过了⼀次循环。
除collect外其它的eager操作还有forEach,toArray,reduce等。
下⾯来看⼀下也许是最常⽤的收集器⽅法,groupingBy:
//给出⼀个String类型的数组,找出其中各个素数,并统计其出现次数
public void numbers) {
List<String> l = Arrays.asList(numbers);
Map<Integer, Integer> r = l.stream()
.map(e -> new Integer(e))
.filter(e -> Primes.isPrime(e))
.collect( upingBy(p->p, Collectors.summingInt(p->1)) );
System.out.println("primaryOccurrence result is: " + r);
}
注意这⼀⾏:
它的意思是:把结果收集到⼀个Map中,⽤统计到的各个素数⾃⾝作为键,其出现次数作为值。
下⾯是⼀个reduce的例⼦:
public void numbers) {
List<String> l = Arrays.asList(numbers);
int sum = l.stream()
.map(e -> new Integer(e))
.filter(e -> Primes.isPrime(e))
.distinct()
.
reduce(0, (x,y) -> x+y); // equivalent to .sum()
System.out.println("distinctPrimarySum result is: " + sum);
}
reduce⽅法⽤来产⽣单⼀的⼀个最终结果。
流有很多预定义的reduce操作,如sum(),max(),min()等。
再举个现实世界⾥的栗⼦⽐如:
// 统计年龄在25-35岁的男⼥⼈数、⽐例
public void boysAndGirls(List<Person> persons) {
Map<Integer, Integer> result = persons.parallelStream().filter(p -> p.getAge()>=25 && p.getAge()<=35).
collect(
);
System.out.print("boysAndGirls result is " + result);
System.out.println(", ratio (male : female) is " + ((Person.MALE)/(Person.FEMALE));
}
3.3 λ表达式的更多⽤法
// 嵌套的λ表达式
Callable<Runnable> c1 = () -> () -> { System.out.println("Nested lambda"); };
c1.call().run();
// ⽤在条件表达式中
Callable<Integer> c2 = true ? (() -> 42) : (() -> 24);
System.out.println(c2.call());
// 定义⼀个递归函数,注意
protected UnaryOperator<Integer> factorial = i -> i == 0 ? 1 : i * this.factorial.apply( i - 1 );
...
System.out.println(factorial.apply(3));
在Java中,随声明随调⽤的⽅式是不⾏的,⽐如下⾯这样,声明了⼀个λ表达式(x, y) -> x + y,同时企图通过传⼊实参(2, 3)来调⽤它:
int five = ( (x, y) -> x + y ) (2, 3); // ERROR! try to call a lambda in-place
这在C++中是可以的,但Java中不⾏。Java的λ表达式只能⽤作赋值、传参、返回值等。
4. 其它相关概念
4.1 捕获(Capture)
捕获的概念在于解决在λ表达式中我们可以使⽤哪些外部变量(即除了它⾃⼰的参数和内部定义的本地变量)的问题。
答案是:与内部类⾮常相似,但有不同点。不同点在于内部类总是持有⼀个其外部类对象的引⽤。⽽λ表达式呢,除⾮在它内部⽤到了其外部类(包围类)对象的⽅法或者成员,否则它就不持有这个对象的引⽤。离骚翻译
在Java8以前,如果要在内部类访问外部对象的⼀个本地变量,那么这个变量必须声明为final才⾏。在Java8中,这种限制被去掉了,代之以⼀个新的概念,“effectively final”。它的意思是你可以声明为final,也可以不声明final但是按照final来⽤,也就是⼀次赋值永不改变。换句话说,保证它加上final前缀后不会出编译错误。