我们要从匿名类开始讲起,一点点引出Lambda表达式。我比较喜欢Lambda这个词,显得比较有学问似的,一笑。
我讲过,技术点不是孤立的,它们之间是有关联的,按照某种层次结构关联在一起就构成一个体系。我们在学习某个技术的时候,要了解它的来龙去脉,把某个技术点放在整体中学习会更有收获。事实上,学术论文一般都要求开头一段讲学术史,这是有道理的。当然,有个别天才能在不引用任何参考文献的情况下提出划时代的理论,横空出世。最著名的就是爱因斯坦在1905年写的《论动体的电动力学》。
以前提到过Inner Class内部类中有一种是可以不用起名字的,称之为匿名类。使用匿名类是因为我们有些场景中不需要知道名字,也不关心它的名字,而是关心它里面的方法,这在事件响应模型中会常用。
先看一个例子吧,先用普通写法,代码如下:
interface Adder {
int add(int x, int y);
}
class MyAdder implements Adder {
@Override
public int add(int x, int y) {
return (x+y);
}
}
class AdderDemo {
public static void main(String[] args) {
MyAdder adder = new MyAdder();
System.out.println(adder.add(21,37));
}
}
程序简单,定义了一个interface,一个MyAdder class实现它,用AdderDemo测试。运行无误。我们看,其实概念上可以不显示声明一个MyAdder类的,我们可以在使用的时候直接弄一个,这样程序更加简洁。
看下面这个匿名类版本,代码如下:
interface Adder {
int add(int x, int y);
}
class AdderDemo {
public static void main(String[] args) {
Adder adder = new Adder() {
@Override
public int add(int x, int y) {
return (x+y);
}
};
System.out.println(adder.add(21,37));
}
}
这次,我们没有显示声明一个MyAdder类,而是在使用的时候临时创建了一个对象,而这个对象依赖的类是没有名字的,临时定义的。这种写法比原始版本要简洁了。
我们再仔细看这个接口,它就只有一个add方法,我们用这个接口其实是想用这个方法。这种情况,我们叫它functional interface函数式接口。有一种更加简洁的写法,见下面的代码(Test1.java):
public class Test1 {
public static void main(String[] argv) {
engine((int x, int y)-> {return x + y;});
engine((x, y)-> {return x + y;});
engine((x,y)-> x + y);
}
private static void engine(Calculator calculator){
int x = 4, y = 2;
int result = calculator.calculate(x,y);
System.out.println(result);
}
}
@FunctionalInterface
interface Calculator{
int calculate(int x, int y);
}
上面的代码中,我们定义了一个Calculator接口,里面就一个calculate方法,这是典型的函数式接口。使用的程序中,我们提供一个engine方法,它以Calculator为参数,调用接口中的calculate方法。
再看对engine方法的调用,仔细看一看,现在不是生成一个匿名类的写法了,而是写成了(int x, int y)-> {return x + y;},比较一下匿名类里面的写法,似乎就是把方法体拷贝过来了,省略了接口名和方法名。概念上这么理解是对的。这里就用到了Lambda表达式,只显示定义参数和方法体。实际上还可以进一步简化成engine((x, y)-> {return x + y;});以及engine((x,y)-> x + y);。
这种写法是把实现方法的函数体当成参数使用了,跟传一个普通变量一样。我们要把上面的程序略加增强就能更加清晰地看出来了,把上面main()中的engine((x,y)-> x + y);扩充成四个语句:
engine((x,y)-> x + y);
engine((x,y)-> x - y);
engine((x,y)-> x * y);
engine((x,y)-> x / y);
你们运行一下看结果。我们把+-/操作当成了方法参数传进去并运行。所以这种机制的作用是干这个,并不只是简单的简化代码的编写。有一个术语叫“code as data”就是说的这件事情。 Lambda表达式就是让你能够实现上面的code as data机制而规定的一种写法。Lambda 表达式给Java中引入了操作符-> ,表达式分成两部分: (n) -> nn
左边的就是表达式的参数,如果没有参数,就用一个空括号。右边的就是操作语句,操作语句不仅仅只有一个数学表达式,它可以是任意的程序语句。如:
RevString revStr = (str) -> {
String result = "";
for(int i = str.length()-1; i >= 0; i--)
result += str.charAt(i);
return result;
};
Lambda表达式不是Java的发明,实际上大家千呼万唤,才姗姗来迟纳入到Java8中。自然,延迟主要的原因是因为Java命运坎坷,被Oracle收购,中间折腾了好几年,早就计划好的事情迟迟不能实现。
Lambda表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),即没有函数名的函数。在数学和工程领域使用的比较多,是科技工作者的心头好,最主要的是简洁显得有geek范。在别的许多语言中,早就支持了Lambda表达式,如C#和Javascript。
函数式语言提供了一种强大的功能:闭包。闭包是一个可调用的对象,它使用和处理一些信息,这些信息来自于创建它的作用域。Java 现在提供的最接近闭包的概念便是 Lambda 表达式。Java里面没有独立函数,因此,为了实现函数式编程,就用了函数式接口进行包装,一个有且仅有一个方法的接口。通过这种退化的方式模拟独立函数。有人却嘲讽Java是以名词为中心的,也确实是这样的,其实所有面向对象的语言都是以名词为中心的,RESTful风格的接口也是名词为中心的。从技术哲学角度,这个好不好,见仁见智,我个人觉得要以名词为中心辅助以动词。Java里面名词中心主义色彩确实太重了,正如这个物理世界,除了实体,还有实体之间的相互作用就是力,Java里面缺乏这个。AOP的思想有一点这个意思,能够穿透实体,加上装饰(Docoration/Advice),统一做某些事情。但是这是架构层面实现的,语言本身的层面没有提出来。比较起人类自己用的科学语言来,计算机语言还有很大的不足。虽然我们现在没有办法用一个方程描述世界,但是几大支柱概念都已经建立起来了,particle粒子,filed场,force力,dimension维度,symmetry对称性。
再看一个在list中使用Lambda表达式的例子,代码如下(Test.java):
public class Test{
public static void main(String[] argv) {
ArrayList<Integer> al = new ArrayList<Integer>();
al.add(1);
al.add(2);
al.add(3);
al.forEach(n -> System.out.println(n));
}
}
上面代码对arraylist中的每一个元素,执行打印动作。
如果我们的好奇心再强一点,看看编译之后的结果,Java究竟怎么处理的上面的Lambda表达式,我们可以试着反编译一下看结果,我们会看到编译器自动生成了一个私有方法,然后使用Lambda表达式的地方都会改成动态调用这个私有方法,形如:
private static void Test$main$0(Integer n) {
System.out.println(n);
}
你们可以自己试一下,不过不同的编译器结果可能会有不同。
把Lambda表达式当成方法引用的时候,还可以进一步用::双冒号简化代码:
al.forEach(System.out::println);
jdk8中使用了::的用法。就是把方法当做参数,每个元素都传入到该方法里面执行一下。代码进一步简化,生人也更难理解了。科技就是这样,符号用得越多越怪就越有学问。
Lambda表达式有一个限制,它不能是Generic泛型的。但是我们结合一下泛型接口有可以实现不同类型的操作。举一个例子,代码如下(Test2.java):
public class Test2 {
public static void main(String[] argv) {
Adder adder = (int x, int y)-> {return x + y;};
System.out.println(adder.add(12,34));
}
}
@FunctionalInterface
interface Adder{
int add(int x, int y);
}
这个例子中用到了一个add方法,是用于把两个整型相加的。如果我们想把两个字符串相加就做不到了,Lambda表达式不支持泛型。我们来看怎么用泛型改造上面的代码:
public class Test2 {
public static void main(String[] argv) {
Adder<Integer> adder = (Integer x, Integer y)-> {return x + y;};
System.out.println(adder.add(12,34));
Adder<String> adder2 = (String x, String y)-> {return x.concat(y);};
System.out.println(adder2.add("12","34"));
}
}
@FunctionalInterface
interface Adder<T>{
T add(T x, T y);
}
先把接口改成泛型,再在用Lambda表达式的时候指定不同类型。两者结合在一起就可以了。
Lambda 表达式改进了Collection库,更容易遍历,过滤。比如Comparator类是用来给list排序的,以前这么写:
Collections.sort(studentList, new Comparator<Student>(){
public int compare(Student p1, Student p2){
return p1.getName().compareTo(p2.getName());
}
});
用Lambda表达式这么写,简化不少:
Collections.sort(studentList, (Student p1, Student p2) -> p1.getName().compareTo(p2.getName()));
数据过滤也是,我们现在能把数据集进行一系列处理了。看一个例子。
先定义一个Student类,代码如下(Student.java):
public class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
再写一个使用的类,是Student列表,进行过滤排序等操作。代码如下(StudentTest.java):
public class StudentTest {
public static void main(String args[]) {
Student s1 = new Student("Alice", 87);
Student s2 = new Student("Bob", 57);
Student s3 = new Student("Chris", 102);
Student s4 = new Student("Donald", 110);
Student s5 = new Student("Cohn", 108);
List<Student> students = Arrays.asList(s1, s2, s3, s4, s5);
students.stream()
.filter(s -> s.getScore() > 100)
.sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
.collect(Collectors.toList())
.forEach(System.out::println);
}
}
上面的核心代码就是最后一句,我们用了一个Stream流水式写法,将列表先过滤再排序最后打印,通过.符号一步步流水执行。代码非常简洁,难怪成为数学家工程师们的最爱。
filter里面,我们用的Lambda表达式是s -> s.getScore() > 100,过滤分数超过100的数据。这是简单表达式,也可以用复杂点的,如s -> s.getScore() > 100 && s.getName().startsWith("C"),过滤分数超过100并且名字以C开头的数据。
sorted里面,我们用的Lambda表达式是(p1, p2) -> p1.getName().compareTo(p2.getName()),用getName拿到的名字字符串进行比较排序,因为String类里面提供了compareTo方法,我们直接使用。要是不指定这个表达式,写成sorted()会有什么效果?我们试一下,编译是不会出错的,但是运行结果有错(如果数据为空或者只有一条数据不会出错,因为这种情况下其实没有进行比较排序):
java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
at java.util.Comparators$NaturalOrderComparator.compare(Unknown Source)
原因是sorted没有参数,系统拿着这个student不知道该怎么比较,这个时候,我们可以实现Comparable提供一个compareTo方法就可以了:
public int compareTo(Student s2) {
return this.getName().compareTo(s2.getName());
}
说到重写,要再提一句,上面最后执行System.out::println的时候,或许出现的是类似这个结果:
Student@7ba4f24f
Student@3b9a45b3
Student@7699a589
那是因为println其实是调用的对象的toString()方法,我们没有自己给Student提供这个方法,就出这么一个结果,自己提供一个就可以了。综合上面的考虑,最后Student类代码变成这个样子,代码如下(Student.java):
public class Student implements Comparable<Student>{
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public String toString() {
return name + " " + score;
}
public int compareTo(Student s2) {
return this.getName().compareTo(s2.getName());
}
}
上面看了过滤和排序,还可以把list里面的数据做一个平方转换,示例如下:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
现在用Lambda表达式,简写成这样:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
以上就是Lambda的基本内容。大家自己多练习。
我们说过,用函数式编程,我们可以把函数本身当成参数,也就可以有返回函数的函数,我们把这种叫高阶函数,还可以用一种级联式写法串在一起。
看一段代码:
public stat int filterSum(List<Integer> values, Predicate<Integer> filter) {
return values.stream()
.filter(filter)
.reduce(0, Integer::sum);
}
这个函数filterSum接收另一个函数filter作为参数,并执行两部操作,一是执行filter,然后执行sum。如果是要求选择所有偶数,就这么用:filterSum(numbers, e -> e % 2 == 0);
函数可以接收函数、lambda 表达式或方法引用作为参数。同样地,函数也可以返回 lambda 表达式或方法引用。在此情况下,返回类型将是函数接口。
再看一段代码:
public static Predicate<Integer> isOdd() {
Predicate<Integer> result = (Integer n) -> n% 2 != 0;
return result ;
}
还可以更加简洁:return n-> n% 2 != 0;
这是返回一个判断数是否为奇数的函数。使用的时候这么用:
Predicate<Integer> check = isOdd();
check.test(4);
我们接着往下面看,怎么真的通过级联的方式把一个Lambda表达式写成一个函数调用。我们的例子就是筛选出被2整除,被3整除,被4整除,被5整除的数据。
自然我们可以这么写:
List<Integer> divide2 = nlist.stream().filter(n % 2 == 0).collect(toList());
List<Integer> divide3 = nlist.stream().filter(n % 3 == 0).collect(toList());
List<Integer> divide4 = nlist.stream().filter(n % 4 == 0).collect(toList());
List<Integer> divide5 = nlist.stream().filter(n % 5 == 0).collect(toList());
我们用了四个Lambda表达式,只是参数略有不同。我们知道内部其实是四个私有方法,我们不想这样,想用一个。
我们做一个函数:
Function<Integer, Predicate<Integer>> divide = (Integer d) -> {
Predicate<Integer> dividen = (Integer n) -> {
return n % d == 0;
};
return dividen;
};
如上,我们定义了一个叫divide的函数,参数是d,即除数(2,3,4,5)。内部是又定义了一个dividen,这个dividen就是一个Lambda表达式判断是否能整除。这里就涉及到两层级联。
整体代码如下(Test4.java):
public class Test4 {
public static void main(String[] argv) {
ArrayList<Integer> al = new ArrayList<Integer>();
al.add(2);
al.add(3);
al.add(4);
al.add(5);
al.add(6);
al.add(7);
Function<Integer, Predicate<Integer>> divide = (Integer d) -> {
Predicate<Integer> dividen = (Integer n) -> {
return n % d == 0;
};
return dividen;
};
List<Integer> values = al.stream().filter(divide.apply(3)).collect(Collectors.toList());
values.forEach(System.out::println);
}
}
运行结果是把被3整除的数据打印出来了。
我们招收简化divide定义。明显的是参数的类型不需要重复写了,然后是内部的dividen其实是不需要显示定义的。这样简化成了:
Function<Integer, Predicate<Integer>> divide = (d) -> {
return (n) -> {
return n % d == 0;
};
};
上面代码还可以简化,你看最里面这个return只有一行,所以可以省掉return。变成:
Function<Integer, Predicate<Integer>> divide = (d) -> {
return (n) -> n % d == 0;
};
留下的这个Lambda表达式还是只有一行,所以再次简化成级联表示:
Function<Integer, Predicate<Integer>> divide = d -> n -> n % d == 0;
上面我们演示了怎么一步一步简化,洋葱一层层耐心地剥,代码一点点简化,头脑越来越明朗。自然,用到的不熟悉的写法和符号也会越来越多,刚才说过这样越来越显得有学问。虽然这么说是开玩笑,但是也不是完全无理。我们看看数学物理的发展史,不断引入新的符号演算,越来越看不懂。为什么会是这样?是为了显得高深而故弄玄虚吗?是科学家为了保饭碗故意这样吗?肯定不是这样。我个人的观点是这是抽象带来的必然结果。人类认识的进程是越来越抽象,不是为了玄虚,而是为了普适,用一条或者几条简单的规则来描述世界。最开头,人是没有抽象能力的,见到的东西都是不同的具体的,后来抽象出类别来,动的东西,不动的东西,固体液体气体,开始对各种相互作用也是分开描述,后来抽象出力这个概念,后来统一成几种力,天上地下一起描述。每一次抽象都会带来新的术语与算符,初看起来就越来越难以弄懂了,其实都是为了简化问题。我们学技术的人,要顺着这个路子往前走,做的程序不光是应对眼前的具体场景,要有普适性,通过抽象来简化问题。
Shakespeare莎士比亚在《Hamlet》写到:Brevity is the soul of wit(简洁是智慧的灵魂)。