从替换匿名内部类开始
下面的代码为button添加响应事件:给addActionList()方法传入匿名内部类,该匿名内部类是ActionListener接口的实现类
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { System.out.println("button clicked"); } });
我们只是想要为button对象添加一个行为:点击按钮时调用一条打印语句
而代码没有清晰的表明我们的意图:它传入的是一个类,而不是一种行为。
使用lambda表达式简写这条语句:传入一个打印行为
button.addActionListener(event -> System.out.println("button clicked"));
我们先来看一下lambda表达式的写法,以及为什么能够在这里使用lambda表达式替换匿名类:
lambda表达式的写法
lambda表达式是一种匿名函数,没有显示的方法名,可以有参数和返回值:
() -> System.out.println("无参数无返回值") (x)-> System.out.println(x) //有参数无返回值 单个参数可以省略调括号:x-> System.out.println(x) (x,y) -> x+y //有两个入参参数,返回值为x+y (x,y)->{ System.out.println(x+y) retrun x+y //方法体内有多条语句需要用{}括起来,使用return返回匿名函数的值 }
除了没有方法名之外,lambda表达式还省略了函数的参数类型和返回值类型
因为编译器能够通过上下文信息推断出它们类型,这也是为什么能够使用lambda表达式替换匿名内部类的原因:函数式接口
函数式接口
在上面的例子中,lambda表达式作为参数,传给了button的addActionListener()方法
而addActionListener()方法是参数是ActionListener接口类型
public interface ActionListener extends EventListener { public void actionPerformed(ActionEvent e); }
像这样的,仅有一个抽象方法的接口,称为函数式接口。
这样的接口,让编译器有足够的上下文信息去使用lambda表达式推导类型:我要使用哪一个方法(仅有一个方法),方法的签名是什么(该方法的参数个数及类型 和 返回值类型)
所以我们可以省略匿名内部类实现接口的方式,直接传入一个符合方法签名的lambda表达式即可:
event -> System.out.println("button clicked") //单个参数,无返回值类型
这里的参数名称event,可以是任意的,但名称最好能够体现出它的类型,增加代码的可读性。 //省略代码的目的是增加可读性
关于:@FunctionalInterface注解,该注释可以让编译器检查一个接口是否符合函数式接口的标准
现在我们知道了为什么lambda表达式能够替代匿名内部类,知道了函数式接口的形式。
回顾一下使用lambda表达式替换匿名内部类的初衷:我们想要在代码中清晰地表明,这段代码在做什么。
函数式编程的作用
上面的例子中已经可以看到,使用lambda表达式消除匿名内部类移除样板代码,看起来更清晰,
另一方面函数式编程使我们能够用更自然的语言去写代码,而不是命令式的写代码。
Stream流的引入,简化了对集合的操作,我们不需要写大段的命令式代码:编写forEach和声明许多局部变量,来表明集合做了哪些操作:
而是编写更自然的函数式代码:集合执行了哪些操作。
可以看之前那篇文章 https://www.cnblogs.com/gss128/p/11032504.html
更重要的,函数式编程带来了高水平的抽象
以java8封装好的Stream来说明函数式编程的优点可能还不够,我们更想知道要怎样去用函数式接口封装自己的代码
在此之前,我们先引入高阶函数这个概念。
高阶函数
引用Kotlin in action中的一个例子来体现高阶函数以及它的作用:
我们有一个SiteData数据类,记录用户访问不同页面路径的访问时间,以及用户的操作系统:
@Getter @Setter @AllArgsConstructor public class SiteData { private String path; //页面路径 private Double duration; //访问时间 private OS os; //操作系统,枚举类型 }
现在我想知道来自某个系统的平均访问时间,可以写一个这样的函数:
private static double averageDuration(List<SiteData> siteDataList, OS os) { return siteDataList.stream() .filter(siteData -> os.equals(siteData.os)) .collect(Collectors.averagingDouble(SiteData::getDuration)); }
调用这个函数:
public static void main(String[] args) { List<SiteData> siteDataList = Stream.of( new SiteData("/", 34.0, OS.WINDOWS), new SiteData("/sign", 15.0, OS.ANDROID), new SiteData("/login", 12.0, OS.WINDOWS), new SiteData("/", 39.0, OS.LINUX) ).collect(toList()); //显示来自某个系统的平均访问时间 System.out.println(averageDuration(siteDataList, OS.WINDOWS)); //Windows系统 System.out.println(averageDuration(siteDataList, OS.ANDROID)); }
我们实现了这个功能,将数据集合、要查询的系统 传入averageDuration()函数中就完成了。
但是,调用者并不满意这个函数:我现在还想知道访问"/login"路径的Android系统的平均访问时间。。。
这样的话,我们需要知道查询条件中的路径信息和操作系统信息,需要在增加一个String path参数
同时修改averageDuration():
.filter(siteData -> os.equals(siteData.os) && path.equals(siteData.getPath()))
调用者又说了:你这是满足俩个条件的啊,我只传一个条件不就查不出来了。。。
显然,这个函数不够"高阶",它的条件太"硬编码"了
我们期望这个函数能够计算满足任意条件的平均时间,将查询条件作为一个抽象,而不是通过硬编码的方式分别展开计算。
我们可以使用一个参数表示任何条件:
private static double averageDuration(List<SiteData> siteDataList, Predicate<SiteData> siteDataPredicate) { return siteDataList.stream() .filter(siteDataPredicate) .collect(Collectors.averagingDouble(SiteData::getDuration)); }
这里使用了Java中的Predicate接口作为查询条件,它是一个函数式接口,它的抽象方法返回一个布尔值。
filter方法中接收这个参数
现在再来调用这个averageDuration方法:
System.out.println(averageDuration(siteDataList, siteData ->
"/login".equals(siteData.getPath()) && OS.ANDROID.equals(siteData.getOs())
));
现在这个方法的条件参数变得灵活了,我们做了什么:
将原来的指定类型的函数参数,抽象为Predicate类型,通过传入一个返回值为boolean类型的lambda表达式,消除了硬编码,调用者可以更灵活的传入查询条件。
我们演示了lambda表达式作为参数的高阶函数,而lambda表达式作为返回结果的高阶函数不太常用,但仍然有用,比如:
定义一个函数,这个函数用来选择恰当的逻辑,并将这个逻辑条件返回(Predicate类型)。
标准库中的函数式接口
现在我们来看一下,部分java提供的函数式接口:
(较完整的接口列表可以参考 https://www.runoob.com/java/java8-functional-interfaces.html)
(1)判断条件
Predicate<T>
首先是刚才的Predicate<T>接口,它提供的抽象方法是 boolean test(T t);
就像刚才使用的那样,可以作为查询条件参数。它还提供了其他默认的接口方法,暂不详细介绍。
(2)比较大小
Comparator<T>
int compare(T o1, T o2);
(3)生产和消费
Supplier<T>
T get(); 产生T类型
Consumer<T>
void accept(T t); 消费T类型
(4)回调函数
Callback<P,R>
R call(P param); 根据P产生R
(5)接受一个参数并返回结果
Function<T,R>
R apply(T t); 根据T产生R
与Callback不同的是,它还提供了compose()、andThen()、identity()三种默认方法
你可以在合适的情况,使用这些接口,并且不再需要使用匿名内部类这种样板代码写法了,只需要使用lambda表达式作为实现类传入。
那么这些标准库函数式接口的适用场景
以及如何自己设计函数式接口,编写更抽象更自然的代码,本篇就先不讨论了。
参考:Java8函数式编程,kotlin in action