Java SE基础巩固(十五):lambda表达式

450 阅读10分钟

1 概述

Java8据说是Java诞生以来最大的一次演进,说实话,对我个人来说没有什么特别大的感受,因为我学Java也就最近一两年的事,Java8在2014年3月18日发布,新增的特性确实非常惊艳,在语言特性层面上新增了lambda,Optional,默认方法,Stream API等,在虚拟机层面上新增了G1收集器(不过在Java9之后才改为默认的垃圾收集器)......

我个人认为Java8和语言相关的几个最重要的特性是如下几个:

  • lambda表达式和方法引用(其实是lambda表达式的一种特例)
  • Stream API
  • 接口的默认方法
  • Optinal
  • CompletableFuture

本系列文章的后面几篇文章会围绕这几个主题来展开,今天就先上个开胃菜,lambda表达式!

2 什么是lambda表达式

lambda表达式也叫做匿名函数,其基于著名的λ演算得名,关于λ演算,推荐大家去找找关于“丘奇数”相关的资料。Java一直被人诟病的一点就是“啰嗦”,通常为了实现一个小功能,就不得不编写大量的代码,而用其他的语言例如Python等,也许寥寥几行代码就解决了,但支持lambda表达式之后,这一情况得到了大大的改善,现在只要使用得当,可以大大缩减代码里,使代码的目的更加清晰,易读,纯粹。

在Java中,很多时候在使用一些API的时候,必须要给出一些接口的实现,但因为该实现其实也就用一次,专门去创建一个新的实现类并不划算,所以一般大多数人采取的措施应该是创建一个匿名实现类,比较典型就是Collections.sort(List list, Comparator<? super T> c)方法,该方法接受一个Comparator类型的参数,Comparator是一个接口,表示“比较器”,如果要使用该方法对集合元素进行排序,就必须提供一个Comparator接口的实现,否则无法通过编译。如下所示:

Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

其实这个实现类的核心只有一行,即return o1.compareTo(o2);但我们却不得不编写其他“啰嗦”的代码,如果使用lambda表达式,会是怎么个样子呢?

Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));

没错,就是那么简单粗暴,就是一行核心代码。其他的比如方法签名啥的统统可以省略了,不仅简洁,而且语义也更加清晰,读起来就好像是说:“sort方法,帮我吧numbers这个序列排个序,排序规则就按照n1.compareTo(n2)的返回值来决定”。现在,是不是感觉,写代码就像在和计算机对话一样简单?但(n1, n2) -> n1.compareTo(n2)这玩意是个什么鬼?还带个箭头?不用着急,下面马上介绍lambda表达式的语法。

2.1 lambda表达式的语法

FsV8BQ.png

  • 第一部分是lambda的参数列表,因为Comparator.compare()方法接受两个参数,所以这里给出两个参数n1和n2,可以省略具体的类型,Java编译器会自动推断。
  • 第二部分是箭头,没什么特殊的地方,只是Java语言觉得使用这个,各个语言的实现也不太一样,例如Python是:号,简单理解的就当是把参数列表和函数主体分开的东西吧。
  • 第三部分就是函数主体,也就是真正执行逻辑的地方。

如果函数主体仅仅包含一行代码,可以省略花括号{}和return关键字(如果有的话)。对于我们的例子,可以改写成这样:

Collections.sort(numbers, (n1, n2) -> {return n1.compareTo(n2);});

注意分号!因为此时return n1.compareTo(n2);就是一条普通的Java语句了,必须遵守Java的语法规则。好了,尽管我们现在明白了lambda语句的语法规则,但还有一个关键的问题,就是为什么要这样写,换句话说,为什么要有俩参数,这return又是几个意思?还有到底哪里才可以使用lambda表达式?说到这,就不得不说一下和lambda息息相关的东西了:函数式接口

3 函数式接口

函数式接口是这样的:只有一个抽象方法的接口就是函数式接口。为什么要特别强调抽象方法呢?Java接口里声明的方法不都是抽象方法吗?在Java8之前,这么说确实没有任何问题,但Java8新增了接口的默认方法,可以在接口里给出方法的具体实现,这里先不多说,后面的文章会详细讨论这个东西。

lambda表达式仅可以用在函数式接口上,我们在上面遇到的Comparator就是一个函数式接口,他只有一个抽象方法:compare(),其方法签名是这样的:

int compare(T o1, T o2);

现在来看看 (n1, n2) -> n1.compareTo(n2)这个表达式,是不是发现了什么?没错,其实lambda表达式的参数列表就是对应的函数式接口的抽象方法的参数列表,并且类型可以省略(编译器自动推断),然后n1.compareTo(n2)的返回值是int类型,也符合compare()的方法描述。这样就算是把lambda表达式和接口的抽象方法签名匹配成功了,不会出现编译错误。

除此之外,Runnable也是一个函数式接口,它只有一个抽象方法,即run(),run()方法的方法签名如下所示:

public abstract void run();

不接受任何参数,也没有返回值。那如果要编写对应的lambda表达式,该如何做呢?其实非常简单,下面是一个示例:

Runnable r = () -> {
    System.out.println(Thread.currentThread().getName());
    //do something
};

如果观察仔细的话,会发现,示例代码中把这个lambda表达式赋值给了Runnable类型的变量r!经过上面的讨论,我们知道,其实lambda就是一个方法实现(其实叫做函数会更加合适),这条赋值语句看起来就好像是再说:“把方法(函数)赋值给变量!”。如果没有接触过函数式编程,会觉得这样很奇怪,怎么能把方法赋值给变量呢?计算机就是这样有意思,总是有各种各样奇奇怪怪的东西冲击我们的思维!那这有什么用呢?咱先不说什么高阶函数,科里化啥的(这些是函数式编程里的概念),就说一点:意味着我们可以把方法(函数)当做变量来使用!即现在方法就是Java世界里的“一等公民”了!既可以将其作为参数传递给其他方法(函数),还可以将其作为其他方法(函数)的返回值(以后会讲到具体的案例)

4 策略模式

策略模式是著名的23种设计模式中的一种,关于它的描述,我这里就不多说了。直接来看个例子吧。

例子是这样的,现在有一个代表汽车的Car类以及一个Car列表,现在我们想要筛选列表中符合要求的汽车,为了应对多变的筛选方法,我们打算用策略模式来实现功能。

下面是Car类的代码:

public class Car {

    //品牌
    private String brand;

    //颜色
    private Color color;

    //车龄
    private Integer age;
	
    //三个参数的构造函数以及setter和getter
    
    //颜色的枚举
    public enum Color {
        RED,WHITE,PINK,BLACK,BLUE;
    }
}

//包含Car对象的列表
List<Car> cars = Arrays.asList(
        new Car("BWM",Car.Color.BLACK, 2),
        new Car("Tesla", Car.Color.WHITE, 1),
        new Car("BENZ", Car.Color.RED, 3),
        new Car("Maserati", Car.Color.BLACK,1),
        new Car("Audi", Car.Color.PINK, 5));

我们希望用一个方法来封装筛选的逻辑,其方法签名伪代码如下所示:

cars carFilter(cars, filterStrategy);

接下来实现策略模式,下面是相关的代码:

public interface CarFilterStrategy {
    boolean filter(Car car);
}

public class BWMCarFilterStrategy implements CarFilterStrategy {
    @Override
    public boolean filter(Car car) {
        return "BWM".equals(car.getBrand());
    }
}

public class RedColorCarFilterStrategy implements CarFilterStrategy {

    @Override
    public boolean filter(Car car) {
        return Car.Color.RED.equals(car.getColor());
    }
}

为了简单,仅仅实现了两种筛选策略,第一种是删选出品牌是“BWM”的汽车,第二种是删选出颜色为红色的汽车。最后来实现carFilter方法,如下所示:

private static List<Car> carFilter(List<Car> cars, CarFilterStrategy strategy) {
    List<Car> filteredCars = new ArrayList<>();
    for (Car car : cars) {
        if (strategy.filter(car)) {
            filteredCars.add(car);
        }
    }
    return filteredCars;
}

最后的最后是测试代码:

public static void main(String[] args) {
    System.out.println(carFilter(cars, new BWMCarFilterStrategy()));
    System.out.println("----------------------------------------");
    System.out.println(carFilter(cars, new RedColorCarFilterStrategy()));
}

分别实例化两个策略,将其作为参数传递给carFilter()方法,最终的输出如下所示:

[Car{brand='BWM', color=BLACK, age=2}]
----------------------------------------
[Car{brand='BENZ', color=RED, age=3}]

确实符合预期。是不是就到此为止了呢?当然不!我们发现,其实BWMCarFilterStrategy以及RedColorCarFilterStrategy的实现代码都非常简单,仅仅寥寥几行代码,而且CarFilterStrategy接口仅仅有一个filter抽象方法,显然是一个函数式接口,那我们能不能用lambda表达式来简化呢?答案是:完全可以!而且更加推荐用lambda表达式来简化这种情况。

4.1 用lambda表达式来简化代码

只要略微做一些修改就行了:

System.out.println(carFilter(cars, car -> "BWM".equals(car.getBrand())));
System.out.println("----------------------------------------");
System.out.println(carFilter(cars, car -> Car.Color.RED.equals(car.getColor())));

这里不再使用BWMCarFilterStrategy以及RedColorCarFilterStrategy两个类了,直接用lambda表达式就行了!最后把这俩实现删除掉!是不是顿时感觉整个项目的代码清爽了许多?

4.2 需要注意的

其实本小节的例子有些过于特殊了,如果你项目中的策略模式的实现非常复杂,其策略不是简简单单的几行代码就能解决的,此时要么进一步封装代码,要么就最好不要用lambda表达式了,因为如果逻辑复杂的话,强行使用lambda不仅仅不能简化代码,反而会使得代码更加晦涩。

5 方法引用

最后简单讲一下方法引用吧,方法引用其实是lambda表达式的一种特殊情况的表示,语法规则是:

<class name or instance name>:<method name>

如果lambda表达式的主体逻辑仅仅是一个调用方法的语句的话,那么就可以将其转换为方法引用,如下所示:

//普通的lambda表达式
numbers.forEach(n -> System.out.println(n));
//转换成方法引用
numbers.forEach(System.out::println);

他俩效果是完全一样的,但显然方法引用更加简洁,语义也更加明确了,这一语法糖“真香!”。具体的我就不多说了,建议看看《Java8 实战》一书,里面有非常非常详细的介绍。

6 小结

本文简单介绍了lambda表达式的语法以及使用。lambda表达式确实能大大简化原本复杂啰嗦的Java代码,而且更加灵活,语义也更加清晰明了,写代码的时候就好像用自然语言和计算机对话一样!但也不是哪里都能使用的,一个最基本的要求就是:其放置的位置要对应着一个函数式接口。函数式接口即只有一个抽象方法的接口,例如Comparator,Runnable等。除此之外,使用lambda表达式的时候,其主体逻辑最好不要超过10行,否则最好还是换一种方式来实现,这里10行并不是那么严格,具体情况还要具体分析。方法引用是一种特殊情况下的lambda表达式的表示方法,可以理解为是lambda的一个语法糖,其语义更加明确,语法也更加简洁,用起来还是非常舒服的!

最后,作为一个补充,来简单看看JDK内置的一些通用性比较强的函数式接口,这些接口都在java.util.function包下,我没数过,咋一看估计得有40多个吧。常用的有Function,Predicate,Consumer,Supplier等。Function的抽象方法的方法签名如下所示:

R apply(T t); //T,R是泛型

简单从语义上来看,就是传入一个T类型的值,然后apply函数将其转换成R类型的值,即一对一映射。其他的接口就不做介绍了。

7 参考资料

《Java8 实战》

阿隆佐.丘奇的天才之作——lambda演算中的数字