函数式编程(一):labmda表达式与函数式接口,以及高阶函数

函数式编程能够更好地实现:抽象、复用、组合,提供更加清晰的面向对象编程方式

 

从替换匿名内部类开始

下面的代码为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来说明函数式编程的优点可能不够明显,我们更想知道要怎样去使用函数式接口实现抽象

 

在此之前,我们先引入高阶函数这个概念。

高阶函数

它的定义很简单,使用也很广泛:
  只要函数的参数列表包含函数接口 或者 函数返回一个函数接口,那么该函数就是高阶函数
  也就是说,函数的参数列表包含lambda表达式 或者 函数返回了一个lambda表达式,那么该函数就是高阶函数
 

引用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

 

posted @ 2019-10-24 16:24  _Gradually  阅读(366)  评论(0编辑  收藏  举报