这篇文章我们来按照Java核心技术关于这部分的讲解研究一下lambda表达式。
直接看一段代码:
class LengthComparator implements Comparator<String>
{
public int compare(String first,String second)
{
return first.length() - second.length();
}
}
...
Arrays.sort(strings,new LengthComparator());
完成排序我们需要给这个sort方法传入一个对象,然后它会在将来的某个时间去调用。对于这样的排序我们希望的是最好这段代码拿来就用,而不去创建对象,像其他一些语言支持函数式l编程,写好个函数拿来就用,可以独立存在。但是Java不行,因为它是一种面向对象的语言,做任何事需要创建对象,不能直接传递代码块。不过经过设计者们的尝试,从jdk8起,Java引入了一种新特性,就是lambda表达式。
回头看上边的这段排序代码,内容是first.length()-second.length(),我们再给first和second指定一个类型String,那么:
(String first,String second)
->first.length() - second.length();
这就是一个典型的lambda表达式。可以看出其实lambda表达式就是个代码块,加上必须要有的变量规范。当然这只是一种写法:
参数,箭头(->),表达式。
如果代码需要完成的计算在一个表达式中写不完,我们还可以加个{},并且包含显示的return语句。像这样:
(String first,String second) ->
{
if(first.length() < second.length()) return -1;
else if(first.length() > second.length()) return 1;
else return 0;
}
有的时候我们需要的表达式没有什么参数,比如一个简单的循环,但是我们仍然需要(),就像没有参数的方法一样:
() -> {for(int i = 100;i>=0;i--)System.out.println(i);}
如果可以推导出表达式的参数类型我们甚至不需要提供类型,比如刚才的排序代码:
(first,second)
->first.length() - second.length();//first,second肯定是字符串
最后再介绍一种,如果只有一个参数的话,而且这个参数的类型可以推倒出来,我们可以省略():
event
-> System.out.println("The time is"
+Instant.ofEpochMilli(event.getWhen()));//代替了ActionEvent event
lambda表达式的语法大概就是这么5种,第2种有一个地方需要注意一下,就是lambda表达式不能只在部分分支返回值,别的分支不返回值,看下这段代码:
(int x)
->{if(x >= 0) return 1;}
很明显x<0 的分支没有返回值,这是不允许的。
你可能会想既然lambda表达式这么方便,那么是不是对于任意一个封装了代码块的接口都可以用lambda表达式来实现呢?毕竟Java有很多接口。但是事实并不是这样。我们要求对于只有一个抽象函数的接口,需要这种接口的对象时,才可以提供一个lambda表达式,而这样的接口我们称之为函数式接口。比如之前的Arrays.sort()方法,需要实现的接口Comparator只有一个抽象函数compare(),很显然它就是一个函数式接口。
现在我们知道了函数式接口可以用lambda表达式来替代,那么回头看之前的排序代码,我们就可以把原来传递对象的参数用lambda表达式换一下:
Arrays.sort(words,
(first,second) -> first.length()-secind.length());
很好理解吧,是不是有点像其他函数式编程语言,我需要个函数就直接传入进去了,实际上你也可以把lambda表达式看成一个函数,而不是对象,不过lambda表达式能做的也仅仅这么多了。下边我们再看一个例子加深一下理解:
java.util.function里面有个有用的接口Predicate:
public interface Predicate<T>
{
boolean test(T t);
}
里面有个返回值是布尔型的抽象方法,很容易理解。然后ArrayList类里有个removeIf方法,它可以按照一定的规则过滤掉集合中的一些元素,它的参数就是一个Predicate,所以我们可以设置一个规则过滤掉数组列表里的null值,就像这样:
list.removeIf(e -> e == null);
很不错吧。
接下来我们来了解另一个概念——方法引用。有的时候lambda表达式涉及一个方法,比如这样:
var timer = new Timer(1000,event -> System.out.println(event));
这个lambda表达式涉及了一个println方法,能不能更简单一点,直接给timer传递一个println方法(可以看出一直在向函数式编程靠近),具体操作如下:
var timer = new Timer(1000,System.out::println);
这个System.out::println就是一个方法引用,它指示编译器生成一个函数式接口的实例,然后覆盖这个接口的抽象方法来调用给定的方法。
再看一个例子,对一个字符串进行排序,而不考虑大小写:
Arrays.sort(strings,String::compareToIgnoreCase);
我们从这些例子可以总结出三种情况:
1.objects::instanceMethod
2.Class::instanceMethod
3.Class:staticMethod
第一种情况就像那个打印例子,这里的方法引用可以等价于向方法传递参数的lambda表达式:x -> System.out.println(x)
第二种情况,第一个参数会成为方法的隐式参数,等价于:
(x,y) -> x.compareToIgnoreCase(y),x是隐式参数,别的参数就传到方法里
第三种情况就是所有的参数传递到静态方法:
Math::pow 等价于(x,y)->Math.pow(x,y)
有一点需要特别注意一下:只有当lambda表达式的体只调用一个方法而不做其它操作时才能把lambda表达式重写为方法引用。看个例子:
s -> s.length == 0
这里s调用了一个length方法,但是还进行了比较的操作,这是不允许的。回头看看之前的例子,它们是不是都只调用了一个方法。
另外this参数和super参数也是可以在方法引用中使用的,比如:
this::equals等同于x -> this.equals(x)
super::instanceMethod 可以在一个重写的方法里调用超类的版本。
这个很简单了,跟方法引用类似,只不过方法名是new。比如:
Person[] = stream.toArray(Person[]::new)
得到一个Person引用数组。
说到这还是让我们看一段代码:
public static void repeatMessage(String text,int delay)
{
ActionListener Listener = event ->
{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
repeatMessage("Hello",1000);
我们可以看到这段代码写了一个方法以及一个一个调用。方法里有个lambda表达式。注意lambda表达式中的输出语句居然调用了外围方法的参数text,是不是有些不可思议。通过这个例子我们来加深一下对lambda表达式的理解,它有三个部分:
1.一个代码块
2.参数
3.自由变量的值,这里指非参数而且不在代码中定义的变量
关于这个代码块和自由变量值有个专业词——闭包,没错,java也是有闭包的。
在这个例子中,text是一个自由变量,lambda表达式使用了它,我们称这个过程叫做捕获。对于捕获,java有个重要限制:
在lambda表达中只能引用值不会发生改变的变量。也就是事实最终变量。
也就是说不论是在表达式内部还是外部这个变量一旦被初始化后被捕获就不允许发生更改。看下边这两段代码你就会理解了:
public static void countDown(int start,int delay)
{
ActionListener listener = event ->
{
start--;//这里值发生改变了,不允许!
System.out.println(start);
};
new Timer(delay,listener).start();
}
public static void repeat(String text,int count)
{
for(int i=0;i<=count;i++)
{
ActionListener listener = event ->
{
System.out.println(i+": "+text);//i在外围循环中被改变了,也是不允许的
};
new Timer(1000,listener).start();
}
}
另外还有一些地方需要注意:
1.lambda表达式中不允许声明一个与局部变量同名的参数或者局部变量。
Path first = Path.of("/usr/bin");
Comparator<String> comp =
(first,second) ->first.length() - second.length(); //first已经被定义了
2.在lambda表达式中this指的是创建这个表达式方法的this参数。
public class Application
{
public void init()
{
ActionListener listener = event ->
{
System.out.println(this.toString());
...
}
...
}
}
这里是Application类的init方法中定义的lambda表达式,也是就说这里的this.toString()会调用Application对象的toString方法,而不是ActionListener的。
我们知道了lambda表达式的作用就类似于一个函数一样,我不一定需要立刻执行,而是可以在我需要它的时候去调用它,就像排序一样,我需要这个排序了我再去用它,这就是延迟执行。可以用到lambda表达式的情况有很多:
1.在一个单独的线程中运行代码
2.多次运行代码
3.在算法的适当位置运行代码(例如,排序中的比较操作)
4发生某种情况时执行代码(如,点了一个按钮,数据到达等)
5.只在必要时运行代码。
现在让我们来看最后一个例子:
public void repeat(int n,Runnable action)
{
for(int i=0;i<10;i++)
action.run();
}
...
repeat(10,()->System.out.println("good!"));
这个repeat方法可以按照一定次数重复打印的动作,可以看出这里这里传递参数的时候使用了lambda表达式来代替这个函数式接口runnable。实际上我们为了实现lambda表达式需要一些通用的函数式接口,它们就像模板一样,没有什么实际意义但是很好用。有关常用的通用函数式接口大家可以自行查阅API或者去搜索一下,这里就不一一列举了。
到这里有关lambda表达式的知识就介绍完了,它的出现让Java具有了函数化编程的能力,代码可以更加简单紧凑,这也算是一次飞跃。
参考资料:
[1]凯·S.霍斯特曼著;林琪等译.Java核心技术 卷I.[M].第十一版.北京:机械工业出版社,2019.242-254.