前言
本文原载于我的博客,地址:https://blog.guoziyang.top/archives/55/
如果你像我一样使用IDEA作为Java开发的IDE,那么你一定也注意到了lambda表达式。
当你在IDEA中实现一个创建一个匿名接口实现类时,例如,在对一个list进行逆序排序:
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
IDEA就会出现一个小提示,replace with lambda
,选择后,代码就会变成下面这样:
Collections.sort(list, (o1, o2) -> o2.compareTo(o1));
这就是lambda表达式,Java 8新引入的一个语法糖,是Java进入函数式编程的第一步。
组成
lambda表达式的组成:
一个逗号分隔的形参列表,被包裹在括号内,如果只有一个形参,则可以参略括号,如果没有参数,那么写一对空小括号即可
箭头符号
->
-
函数体,由单条表达式或者一个方法体组成
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
如果使用的是单条表达式,那么在运行时,Java运行时就会计算表达式的值并返回。
当然也可以使用一个return语句:
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
return语句不是一个表达式,所以必须由一个大括号包裹。但是,如果方法体只调用了一个返回void的方法,那么也不需要包裹在大括号内,例如
email -> System.out.println(email)
单一方法接口
例如,我们要实现一个事件监听器,在Java中,事件监听器通常被定义为具有单个方法的接口,如下:
interface StateChangeListener {
public void onStateChange (State oldState,State newState);
}
假设你有一个类StateOwner可以注册状态监听器:
public class StateOwner {
public void addStateListener (StateChangeListener listener) {...}
}
在Java 7中,我们可以使用匿名接口来添加事件监听器:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener(){
public void onStateChange (State oldState, State newState) {
...
}
});
在Java 8中,我们就可以使用Lambda表达式来添加事件监听器:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState,newState) -> System.out.println("状态已更改")
);
Lambda表达式只能在与它们匹配的类型是单个方法接口的地方使用。在上面的示例中,lambda表达式用作参数,其中参数类型为 StateChangeListener
接口。该接口只有一个方法。因此,lambda表达式已与该接口成功匹配,该表达式就被转化为实现与该参数相同接口的函数。
注意,lambda表达式与匿名接口实现的一个很大的区别就是,匿名接口实现类可以有成员变量,而lambda表达式不能,因此lambda表达式也被称为无状态的。
lambda匹配规则
单个方法接口也被称为功能接口,lambda与功能接口的匹配主要有以下步骤:
- 接口只有一个未实现的方法
- lambda表达式的参数与该方法的参数匹配
- lambda表达式的返回类型与该方法的返回类型匹配
如果三个条件都是真,那么lambda表达式即可与该接口匹配。
变量捕获
lambda表达式在某些情况下可以访问在lambda函数体外部声明的变量。
局部变量
当lambda使用一个外部的局部变量时,如果传入的是一个引用,那么lambda表达式不可以改变这个引用,否则会出现编译错误。
实例变量
注意这一段代码:
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
在lambda表达式中,使用了this
关键字,访问到了EventConsumerImpl类中的name变量,即this指向的是EventConsumerImpl实例对象。
这和匿名接口实现是不同的,在匿名接口实现中,是可以拥有自己的实例变量的,this指向的也是匿名接口实现对象。
静态变量
当然,lambda可以访问静态变量,因为只要在该变量的访问域内,从Java程序的任何地方都可以访问到静态变量。
方法引用
Java 8中加入到一个特性,名为方法引用。如果你的lambda表达式的作用仅仅是使用传递给其的参数来调用另一个方法,那么我们就可以使用方法引用来简化其写法。
例如,我们有一个接口:
public interface MyPrinter {
public void print(String s);
}
如果我们使用lambda来创建实现MyPrinter的实例,如下:
MyPrinter myPrinter = (s) -> {System.out.println(s);};
如果使用方法引用的话:
MyPrinter myPrinter = System.out::println;
双冒号::
告诉编译器这是一个方法引用,引用的方法是双冒号之后的内容,引用的方法所在的类或对象在双冒号之前。
静态方法引用
最容易引用的应当就是静态方法了,例如前面我们举的MyPrinter的例子,由于MyPrinter.print()
和System.out.println()
方法的参数相匹配,所以就可以创建实现MyPrinter.print()
且引用System.out.println()
方法的lambda表达式。
参数方法引用
我们还可以将参数之一的方法引用到lambda,例如接口:
public interface Finder {
public int find(String s1, String s2);
}
该接口用于查找s2在s1中首次出现的位置,如果我们直接使用lambda表达式:
Finder finder = (s1, s2) -> s1.indexOf(s2);
我们可以直接将s1的indexOf()
方法引用到lambda中:
Finder finder = String::indexOf;
Java编译器将尝试将第二个参数作为引用方法的参数,将引用方法与第一个参数类型进行匹配。
实例方法引用
我们还可以从lambda中引用实例的方法,例如我们定义接口:
public interface Deserializer {
public int deserialize(String v1);
}
该接口用于将String转换为int。那么现在我们创建一个StringConverter类:
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
我们注意到,StringConverter中的convertToInt方法与接口方法具有相同的参数和返回值,所以我们可以进行如下的方法引用:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
第二行的lambda表达式引用了第一行创建的stringConverter对象的convertToInt实例方法。
构造方法引用
引用一个构造方法很容易,你只需要将类名后面加上::new
,像这样:
MyClass::new
例如我们有一个创建String的工厂接口:
public interface Factory {
public String create(char[] val);
}
我们想要create方法调用new String(char[] chars)
方法来创建String,直接使用lambda表达式如下:
Factory factory = chars -> new String(chars);
而使用构造器引用的话,即可简写为:
Factory factory = String::new;