在理解lambda表达式之前,先来看下行为参数化的概念。
什么是行为参数化
在软件开发过程中,我们面对的需求总是在不断变化。所以在开发过程中,需要考虑代码的通用性和复用性,在实现新功能时让代码改动尽量简单。在Java中,泛型就是一个很好的例子,泛型通过把数据类型参数化,将方法实现和数据类型解耦,使一个方法或者一个类可以适用于多种数据类型,进而实现类和方法的复用。除了像泛这种需要处理不同类型的数据,在某些场景下,我们也需要一个方法或者类可以实现不同的行为。例如,需要一个排序的方法既可以按照高低排序也可以按照重量排序,这时候就需要把行为参数化。泛型是将数据类型作为参数传入一个方法进而实现代码复用,那么类比泛型的概念,行为参数化可以理解为把方法A(行为)作为一个参数传入另外一个方法或者类B,然后在方法或者类B的内部调用传入的方法A来实现不同的逻辑。这样方法B的行为就基于方法A被参数化了。
为什么需要行为参数化
行为参数化的作用是让方法或者类在面对不断变化的需求时,可以改动的尽量少的代码。下面来看一个例子。在使用美团的时候,我们都会根据各种条件来筛选符合我们需求的餐厅,假设现在有一个需求是要根据餐厅的类型来筛选餐厅。
需求1:根据餐厅类型来筛选餐厅。
我们先定义一个餐厅的实体类Restaurant
public class Restaurant {
private String name;
private String type;
private int distance;
private double evaluate;
public Restaurant(String name, String type, int distance, double evaluate){
this.name = name;
this.type = type;
this.distance = distance;
this.evaluate = evaluate;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public int getDistance() {
return distance;
}
public double getEvaluate() {
return evaluate;
}
}
然后定义一个按照餐厅类型来筛选餐厅的方法。
public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, String type){
List<Restaurant> result = new ArrayList<>();
for (Restaurant restaurant:restaurants) {
if (type.equals(restaurant.getType())) {
result.add(restaurant);
}
}
return result;
}
这样就可以满足需求了。但是,现在又有了新的需求,期望按照餐厅的距离来筛选餐厅。
需求2:根据餐厅距离来筛选餐厅。
例如,我们需要筛选出距离<=目标距离的餐厅,那么我们可以有两种方案。
方案1 重新定义方法
public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, int distance){
List<Restaurant> result = new ArrayList<>();
for (Restaurant restaurant:restaurants) {
if (restaurant.getDistance() <= distance) {
result.add(restaurant);
}
}
return result;
}
重新定义一个方法很好的满足了需求,而且也和原来的筛选类型的方法解耦,但是,经过仔细观察,可以发现筛选距离的方法和筛选类型的方法很多地方是重复的,仅仅是筛选条件不一样。这不符合软件设计的原则。所以,我们考虑通过修改原来的方法以满足新的需求。
方案2 修改原有方法
public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, String type, int distance, int filterType){
List<Restaurant> result = new ArrayList<>();
for (Restaurant restaurant:restaurants) {
if (filterType == 1) { //按照餐厅类型筛选
if (type.equals(restaurant.getType())) {
result.add(restaurant);
}
}else if (filterType == 2) { //按照餐厅距离筛选
if (restaurant.getDistance() <= distance) {
result.add(restaurant);
}
}
}
return result;
}
这样写虽然满足了需求,但是代码的可维护性和扩展性会非常差,如果后续再加一种筛选方式,比如按照评价来筛选,那么我们还要修改这个方法。随着筛选方式的不断增加,方法的入参会越来越多,而且方法中会有很多if-else分支,会变得非常复杂。这也违反了面向对象设计的开闭原则。而且如果想将筛选条件进行组合,比如筛选类型是火锅,同时距离小于5的餐厅,那么还要对上面的方法进行修改。
通过上面可以看到,通过添加参数的方法来满足不断变化的需求并不是一个很好的解决方案,其实对于filterRestaurant
来说,它最核心的逻辑是判断一个餐厅是否满足筛选的条件,所以我们可以把这个判断做更高一级的抽象,并和filterRestaurant
方法解耦,如下所示。
public interface RestaurantFilter {
boolean filter(Restaurant restaurant);
}
然后我们把filterRestaurant
方法做一下改造。
public static List<Restaurant> filterRestaurant(List<Restaurant> restaurants, RestaurantFilter restaurantFilter){
List<Restaurant> result = new ArrayList<>();
for (Restaurant restaurant:restaurants) {
if (restaurantFilter.filter(restaurant)) {
result.add(restaurant);
}
}
return result;
}
此时,如果我们想筛选某种类型的餐厅,我们可以实现RestaurantFilter
接口并传入filterRestaurant
方法,例如我们要筛选烧烤类型的餐厅,可以定义烧烤餐厅筛选类并实现RestaurantFilter
接口。
public class BarbecueRestaurantFilter implements RestaurantFilter {
@Override
public boolean filter(Restaurant restaurant) {
return "烧烤".equals(restaurant.getType());
}
}
然后通过调用filterRestaurant
方法来筛选烧烤类型的餐厅。
filterRestaurant(restaurants, new BarbecueRestaurantFilter());
同样,如果需求更复杂一点,想筛选5km以内,而且评价高于4.5分的餐厅,我们可以定义筛选器如下所示。
public class ComplexFilter implements RestaurantFilter {
@Override
public boolean filter(Restaurant restaurant) {
return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5;
}
}
对于需求变动,我们只需要定义相应的筛选类就可以,不会对现有的逻辑造成影响。这其实就是行为参数化,在上面的例子中,把筛选苹果这个行为通过一个对象传入了filterRestaurant
方法,让filterRestaurant
方法用一套代码实现不同的能力。行为参数化带来的益处是,行为和使用行为的方法解耦,增加代码的扩展性和复用性。
但是这种方式有一个问题,虽然我们把筛选苹果的行为做了抽象,但是每次增加一种新的筛选方式,我们都要增加一个筛选类,而且这个类的对象被创建后只使用了一次,随着筛选条件的增多,这种类的数量会越来越多。如果不想创建这么多类该怎么办呢?在Java中,匿名内部类特别适用于这种场景,匿名内部类适用于类的对象只使用一次或者定义回调方法的场景。所以,这里可以使用匿名内部类来代替定义具体的筛选类。如下所示。
List<Restaurant> filterResult = filterRestaurant(restaurants, new RestaurantFilter() {
@Override
public boolean filter(Restaurant restaurant) {
return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5;
}
});
这样,就可以避免定义多个筛选类,代码会变得简洁一点。
至此,通过行为参数化,把行为和使用行为的方法解耦,我们可以定义扩展性和复用性良好的filterRestaurant
方法。通过使用匿名内部类,代码的形式得以进一步简化,但是匿名内部类还是略微显得有点啰嗦,因为匿名内部类中有很多无用的模板代码。比如,在上面的例子中,我们想传给filterRestuarant
方法的其实主要是filter
方法中的代码块,但是我们确要写很多模板代码。所以为了解决这个问题,Java8引入了lambda表达式。
Lambda表达式
通过上面的例子可以看到,在Java中,想传递一段代码非常不方便,我们必须首先构建一个对象,然后通过对象调用代码,因为Java是面向对象的,任何事物的传递都需要通过对象来实现。为了解决这个问题,Java8引入了Lambda表达式,我们可以把它看成是一种语法糖,它允许把函数当做参数来使用,这是一种是面向函数式编程的思想,使用lambda表达式来代替匿名内部类可以使代码更加简洁。
什么是Lambda表达式
Lambda表达式是传递匿名函数的一种方式,它没有名称,但是包含参数列表,函数主体,以及返回值。Lambda表达式其实表示的是一个函数,只不过这个函数没有名字,但是它仍然包含构成函数的主体:参数列表,函数体以及返回值。
Lambda表达式语法
Lambda表达式语法如下所示。
参数列表 -> 函数主体
其中
参数列表:Lambda表达式所表示的匿名函数的参数。
箭头:把参数列表和函数体分隔开。
函数主体:Lambda表达式表示的匿名函数的函数体。
返回值:无需指定lambda表达式的返回类型,因为返回类型总能根据上下文推导出来。
在使用时,Lambda具体的形式有以下两种。
1. (parameters) -> expression
// parameters: 参数列表,准确的说是形参列表。
// expression: 表达式,表达式后面没有分号,表达式的结果作为匿名函数的返回值。例如,1+2,“hello,world”
2. (parameters) -> {statements; }
// parameters: 参数列表,准确的说是形参列表。
// statements: 语句,语句后面带分号,并且外面要加大括号,就是Java中的普通的代码块,如果想要定义返回值,需要使用return.
// 举例:
// (1) ()-> {} 没有入参,函数体为空
// (2) () -> 1 没有入参,返回值为1
// (3) (int a, int b) -> a+b 入参为a,b,返回值为a+b
// (4) (String a) -> a.toUpperCase() 入参为a,返回值为将a的所有字母转换成大写
// (5) (o1,o2) -> o1>o2 如果可以从上下文推断出参数的类型,在参数列表中可以不写参数类型
// (6) (int a, int b) -> {return a>b;}
// (7) a -> {return a*2;} //如果只有一个参数,可以省略小括号。
了解了Lambda表达式的定义,我们可以把筛选餐厅的方法改写成Lambda表达式来实现。
filterRestaurant(restaurants, (Restaurant restaurant) -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5 ); //形式1
也可以写成
filterRestaurant(restaurants, (Restaurant restaurant) -> { return restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5; }); //形式2
可以看到,Lambda表达式就是对Restaurant
接口中filter
方法的一个实现。在底层,编译器会将Lambda表达式转换成Restaurant
类型的对象。所以从这个层面来说,Lambda表达式其实就是一个语法糖。
另外,在使用Lambda表达式时,编译器能够根据上下文推断出一个lambda表达式的参数类型,所以在参数列表中可以不写参数类型。上面的方法调用可以进一步简化为:
filterRestaurant(restaurants, (restaurant) -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5 );
如果只有一个参数,那么参数外面的小括号是可以省略的。因此,我们可以进一步简化。
filterRestaurant(restaurants, restaurant -> restaurant.getDistance() <= 5 && restaurant.getEvaluate() >= 4.5);
什么情况下使用Lambda表达式
从上面的例子可以看到,Lambda表达式通常用于代替匿名内部类实现一个接口。那什么情况下才能使用Lambda表达式呢?如果一个接口有两个方法,能使用Lambda表达式吗?Java8规定,只能在函数式接口上使用Lambda表达式。下面来看下什么事函数式接口。
函数式接口
在Java中,只能用lambda表达式来表示函数式接口。所谓的函数式接口,是指只定义了一个抽象方法的接口。因为Java8允许在接口中定义默认实现,所以这里的只定义一个抽象方法其实包含两层含义:
接口只有一个方法
接口中不止定义了一个方法,但是只有一个抽象方法需要被实现,其他的方法在接口中都定义了默认实现。
总的来说,函数式接口,就是只有一个抽象方法需要被实现的接口。Lambda表达式以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。在底层,方法还是会将lambda表达式转换成一个接口类型的对象,并通过对象调用lambda表达式中的方法。所以,Lambda表达式的签名和函数式接口中抽象方法的签名必须是一致的。
在Java8中,可以使用@FunctionalInterface
注解来定义一个函数式接口,使用@FunctionalInterface
用于表示该接口会设计成
一个函数式接口。如果你用@FunctionalInterface
定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误
例如,我们可以将RestaurantFilter
接口定义为一个函数式接口。
@FunctionalInterface
public interface RestaurantFilter {
boolean filter(Restaurant restaurant);
}
加了@FunctionalInterface
注解以后,如果我们再在RestaurantFilter
中定义抽象方法,编译器就会报错。错误提示如下所示。
Multiple non-overriding abstract methods found in interface RestaurantFilter
错误提示表明这个接口存在多个抽象法。所以,函数式接口有且只能有一个抽象方法。添加@FunctionalInterface
注解后,并不会改变函数式接口的本质,只是起到了一个提示作用。
函数式接口中的抽象方法的签名和Lambda表达式的签名是一致的。这种抽象方法叫作函数描述符。例如,RestaurantFilter
中抽象方法为filter
,该方法的签名可以表示为 Restaurant -> boolean,也就是入参为Restaurant类型,返回值为boolean类型。所以,只要是满足Restaurant -> boolean这个类型的Lambda表达式,都能用于实现RestaurantFilter
接口。
方法引用
方法引用是Lambda表达式的一种快捷写法,它通过引用已有的方法来代替Lambda表达式,可以把方法引用看作是Lambda表达式的一种语法糖。例如,在上面筛选餐厅的例子中,定义一个判断餐厅距离是否满足需求的方法。
public static boolean isRestaurantNotFar(Restaurant restaurant) {
return restaurant.getDistance() <= 5;
}
这个方法的签名和RestaurantFilter
接口中filter
方法的签名是一致的,通过方法引用,我们可以把isRestaurantNotFar
这个方法作为Lambda表达式传入filterRestaurant
方法,如下所示。
filterRestaurant(restaurants, LambdaTest::isRestaurantNotFar);
其中LambdaTest
是isRestaurantNotFar
所在类的类名。下面来具体看下如何创建方法引用。
方法引用分类
我们可以通过三种方式来创建方法引用。如下所示。
-
指向类静态方法的方法引用
格式:ClassName::staticMethod 含义:引用类中的静态方法 等效的Lambda表达式: (args) -> ClassName.staticMethod(args)
-
指向对象实例方法的方法引用
格式:instance::instanceMethod 含义:引用对象实例的方法 等效的Lambda表达式: (args) -> instance.instanceMethod(args)
-
指向类实例方法的方法引用
格式:ClassName::instanceMethod 含义:引用对象的实例方法,但是这个对象本身是Lambda表达式的一个参数 等效的Lambda表达式: (arg0, restArgs) -> instance.instanceMethod(arg0,restArgs)
下面看一个例子,我们新建一个类如下所示。
public class CompareMethod {
public int compareAscend(String s1, String s2) {
return s1.compareTo(s2);
}
public static int compareDescend(String s1, String s2) {
return s2.compareTo(s1);
}
}
这里有两个对字符串排序的方法,一个升序排序,一个降序排序方法。下面通过方法引用来对一个字符串List进行排序。
// 方式1 类::静态方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(CompareMethod::compareDescend);
list.forEach(s-> System.out.println(s)); // [f c b a]
// 方式2 对象::实例方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(new CompareMethod()::compareAscend);
list.forEach(s-> System.out.println(s)); // [a b c f]
// 方式3 类::实例方法
List<String> list = Arrays.asList("a","f","c","b");
list.sort(String::compareTo);
list.forEach(s-> System.out.println(s)); // [a b c d]
下面看下String类的compareTo
方法签名。
public int compareTo(String anotherString);
这里看到compareTo
方法的签名和List.sort方法中Comparator
接口抽象方法的签名不一致,那为什么可以使用呢?这里可以认为编译器将这个方法引用转换成了一个等效的Lambda表达式。
String::compareTo ===== (s1,s2) -> s1.compareTo(s2)
其实下次每次看到形式3的方法引用,我们都可以将其转换为等效的Lambda表达式来理解。
总结
行为参数化的作用:使代码可以应对不断变化的需求,便于扩展和复用。
Lambda表达式:用于传递匿名函数,比匿名内部类更便捷。
函数式接口:只有函数式接口才能使用Lambda表达式来实现。
方法引用:Lambda表达式的一种便捷形式。