Java Java8的函数式编程

1、概述

在计算机科学中,函数式编程是一种编程范式,一种构建计算机程序结构和元素的方式,类似于比较熟悉的面向对象编程。将计算视为数学函数的处理并尽量避免改变状态和可变数据。它是一种声明性编程范例,这意味着使用表达式或声明而不是语句来完成编程。在功能代码中,函数的输出值仅取决于传递给函数的参数,因此用参数x的相同值调用函数f两次会产生相同的结果f(x)。这与依赖于本地或全局状态的过程形成对比,当使用相同的参数但使用不同的程序状态时,有可能在不同的时间产生不同的结果。不依赖于函数输入的状态变化,就可以使得更容易理解和预测程序的行为,这是开发函数式编程的关键动机之一。

和Java没有任何关系的语言JavaScript就是一个函数编程语言。Java最初被设计出来是作为一门面向对象编程语言来设计的。但是随着时代的变化,异步处理在编程中变得越来越频繁。在现代编程语言中,任何可并行操作希望是一个简单的动词,开发者不仅希望能处理并发问题,并且必须能够使用这些动词而无需任何复杂的实现。对象都可以根据并发动词进行演变。所以在Java8中带有用于异步任务的动词,例如CompletableFuture和用于并行处理的CoolJoinPool等结构。异步编程需要隔离的编程单元,使用隔离的编程单元,可以保证结果,这种隔离的编程单元称为“纯函数”。如果在程序中使用纯函数,那么可以理解为在进行函数式编程。而异步处理的这种需求,正好非常适合用函数编程来解决,这也促使来Java 需要支持函数式编程。Java8 正好做了相关的支持。在Android开发中经常使用的Rx系列也采取了函数式编程的理念。

2、相关前置概念讨论

2.1 纯函数

纯函数不依赖于任何其他对象实例或类定义。在Java中,如果对象实例是实例函数,则该函数必须存在于对象实例中,如果它是静态的,则必须存在于类定义上。所以说,严格来讲Java里面其实是没有纯函数的。同时纯函数的输出可以是一个函数。

2.2 函数程序外观

Java代码下面处理一个CSV文件数据。每行通过转换数据或搜索,过滤,排序数据来输出新的结果集。


try(Stream stream = Files.lines(Paths.get(fileName)) ){

    stream

        .limit(10)

        .map(t->t.split(","))

        .flatMap(Arrays::stream)

        .collect(Collectors.groupingBy(Function.identity(),Collectors.counting()))

        .entrySet().stream()

        .filter(t -> t.getValue()>10)

        .forEach(t->System.out.println(t.getKey()+" "+t.getValue()));

}catch(IOException e ){}

上面的代码过滤了一些大于10的数据。里面的语法用到了很多Java里面的新语法,之后会进行讨论,可以认为大于10的数据会被某些东西过滤掉。这里没有如何过滤数据的操作,只是按照需求一步步的进行操作。在传统的程序中,可能需要遍历列表,将每个项目与某些项目进行比较,如果正确,则将其放入目标列表。但这看到这样的东西吗,因为这里不使用命令式编程,而是使用声明性编程。此外,在此代码中看不到任何for循环。因为函数式编程不使用迭代和迭代器,而使用递归。

3、对象不可变

相同引用可以指向的不同对象是面向对象编程的基本能力之一。但在函数式编程中,对象必须是不可变的,即使有权访问对象,也必须无法根据需要对其进行修改。因为可变性是多线程环境中出现错误的主要原因。任何线程都可能意外地更改对象的状态,从而导致程序出错。为了防止这种情况,使用synchronized关键字,Barrier类,互斥体,信号量,锁等。但所有这些实现都很麻烦,甚至可能会导致死锁,线程匮乏等问题。因此,如果为了同步而导致这些问题并且解决起来如此费劲,为什么不能直接消除其可变性,来一劳永逸。这就是在函数式编程中所做的。

在下面的代码中,创建了一个健身房会员对象,会员开始日期与会员数据一起存储。这里,printEndDate()和printStartDate()方法都具有访问和修改startDate值的能力。它在不考虑任何其他方法的情况下能修改原始日期对象。


public class GymMember {

        String name;

        Integer id;

        Date startDate;

        public GymMember(String name, Integer id, Date date) {

                super();

                this.name = name;

                this.id = id;

                this.startDate = date;

        }

        public static void main(String[] args) {

                GymMember object = new GymMember("talha", 1, new Date());

                object.printEndDate();

                object.printStartDate();

        }

        private void printEndDate() {    

                Calendar calendar = Calendar.getInstance();

                calendar.setTime(startDate);

                calendar.add(Calendar.MONTH,1);

                startDate = calendar.getTime(); 

                System.out.println(startDate);

        }

        private void printStartDate() {

                System.out.println(startDate);

        }

}

为了防止上面的随意修改,可以将startDate属性设置为final


final Date startDate;

当这时候在在其中一个方法中更改startDate时,编译器将报错。但这样设置并不能完全将startDate设置为不可变。final仅阻止修改引用,使之无法更改此引用引用的地址。但仍然可以更改引用指向的对象的属性。仍然可以更改startDate的属性值,即使它被final修饰。所以在并行环境中使用它仍然可能不安全。为此Java8 中对于Data类提供了一个不可变类LocalDate。同时为了防止GymMember类通过子类注入的方式改变其属性,比如如下:


public class VIPGymMember extends GymMember {

        public VIPGymMember(String name, Integer id, LocalDate date) {

                super(name, id, date);

                id = id + 1000000;

        }

}

需要将类设置为final可以阻止这类事情的发生,下面是一个最终版的:


public final class GymMember {

        final String name;

        final Integer id;

        final LocalDate startDate;

        private void printEndDate() {

                Calendar calendar = Calendar.getInstance();

                calendar.add(Calendar.MONTH,1);

                System.out.println(startDate);

        }    

        public GymMember(String name, Integer id, LocalDate date) {

                super();

                this.name = name;

                this.id = id;

                this.startDate = date;    

        }

        private void printStartDate() {

                System.out.println(startDate);

        }

}

如上面的Data,Java8 中提供了许多不可变的对象,例如JDK架构师为每个集合提供不可变版本。可以使用这些不可变集合类型并使用Collections实用程序类转换可变集合,如下所示:


public static void main(String[] args) {

        final Map myMap = new HashMap<>();

        Map myUnmodifiableMap = Collections.unmodifiableMap(myMap);

        myMap.put(2, "talha");

        addElement(myUnmodifiableMap); // 会报错

}

public static void addElement(Map m) {

        m.put(3, "john");

}

如上所示,使用Collections的 unmodifiableMap方法使Map不可变。可以再次使用Map引用此方法的结果。但是此集合初始化后,永远不允许修改。

综上所述,主要讨论了使用final关键字使自定义对象不可变。final关键字用于;

① 防止对象的地址更改

② 防止对象实例的属性的地址更改

③ 防止类被子类继承

同时还讨论了JDK集合和Date API是可变的。但是在在函数式编程中这是不可接受的。于是在Java8中出现了LocalDate类来替换Date类并将可变Map,List,Set接口用不可变类替换。可以通过使用java.util.Collections类的unmodifiableMap(),unmodifiableCollection()和unModifiableSet()等方法来获取不可变集合。

4、引用函数

函数是函数式编程中的一等公民。这句话可以说是函数式编程中最关键的一句话。但比较遗憾的是在Java中函数并不是一等公民,类或者说对象才是一等公民。Java中的函数是不能脱离类或者对象而存在的。但是在Java8中通过一些巧妙的方法将函数变成了伪一等公民。而在比较新的一门语言Kotlin中函数就正式成为了一等公民。

函数不能作为参数传递给Java 8之前的其他函数。在Java8函数可以作为一种伪独立对象调用,它们可以作为伪参数传递给其他函数。它其实是一个只有一个接口函数的接口类。在Java8中增加了一些语法糖,使它看上去像一个函数对象,这会在下面进行讨论。

同时在Java8中可以用“ ::” 来表示引用此函数,可以通过此表示法访问类或实例的构造函数和方法。同时可以用Supplier和Function接口来引用对应的构造函数和方法,以上面的GymMember类为例。


public GymMember() {

        this.name = "gpj";

        this.id = 0;

        startDate =  LocalDate.MIN;

    }

public Date getNextDay(Date date) {

        Calendar calendar = Calendar.getInstance();

        calendar.setTime(date);

        calendar.add(Calendar.DAY_OF_YEAR, 1);    

        Date dateNew = calendar.getTime();

        return dateNew;

}

public static void main(String[] args) {

        Supplier memberCreator = GymMember::new;

        GymMember mGymMember = memberCreator.get();

        Function nextDayCalculator = mGymMember::getNextDay;

        nextDayCalculator.apply(new Date());

}

在上面的main函数中第一行用了GymMember::new来获取GymMember的构造函数,左侧是一个特殊的SAM(单一抽象方法),下面将讨论,并用于引用一个没有输入但返回一个对象的函数。将其引用赋值给memberCreator引用。第二行通过这个引用来构造对象。第三行获取了GymMember的getNextDay 赋值给Function类型nextDayCalculator。这里的Function 第一个泛型是函数输入类型,第二个是输出参数类型。Function这个类型只有一个输入,这边对于这个设计可以给的一个解释是 纯函数的性质和形式定义来讲,必须有且只有一个输入参数。第四行使用apply方法来独立调用一个函数并独立地提供输入参数,可以理解为此时该函数是剥离于对象的。

4.1 单一抽象方法接口(SAM)

前面已经多次提到了这个概念,在Java中实际上是不存在脱离于对象的函数的。于是为了满足函数式编程,设计了单一抽象方法接口来进行模拟。其实在Java8之前就有许多单一抽象方法接口,比如说Runnable,Callable,Comparator和ActionListener等。他们的相似之处在于,当实现其中任何一个时,只需处理此接口的一种方法。其他方法对开发者来说并不重要。在实现此特定方法(此方法默认情况下未实现,即abstract)之后,将传递实现这些接口的类的新实例。

在Java 8中,几个SAM接口和SAM接口的新表示(Lambda表达式)被添加到JDK中。使用lambda表达式(后面会讨论),不再需要创建一个类来包装所需的方法并调用它。因为从开发者层面讲可以引用方法并独立调用它。所有这些SAM接口都可以在Stream API中使用。以下是Java8中新的SAM接口:

接口 输入 输出 目标
Function 一个任意类型 一个任意类型 实现或改变一个逻辑功能
BiFunction 两个任意类型 一个任意类型 实现或改变一个逻辑功能
Predicate 一个或多个任意类型 boolean 测试值是否符合逻辑
Consumer 一个或多个任意类型 使用数据并产出一些副效果
Supplier 一个任意类型 创建所需类型的对象

下面会分别对这些类进行讨论。

4.2 Function

Java Java8的函数式编程_第1张图片
Function可视化

这个函数用于基于给定输入和逻辑创建输出对象,并可能与其他函数链接。可以重写其apply()方法。也是需要实施的唯一方法。

方法 功能
R apply(T t) 调用该方法,其传入参数类型是T,返回值类型是R
andThen(Function after) 先调用其自己的apply()方法,再调用传入的Function的apply()方法
compose(Function before) 先调用其传入的Function的apply()方法,再调用自己的apply()方法
identity() 静态函数,返回一个输入输出类型一样的Function

4.3 Predicate

测试数据是否符合某些值或逻辑。其test()是需要实现的单一抽象接口。已经实现了其他方法。

Java Java8的函数式编程_第2张图片
Predicate可视化
方法 功能
boolean test(T t) 测试值是否符合逻辑
Predicate and(Predicate other) 和传入的Predicate进行与操作作为一个合的Predicate返回
Predicate or(Predicate other) 和传入的Predicate进行或操作作为一个合的Predicate返回
Predicate negate() 将自身取反作为一个合的Predicate返回

4.4 Consumer

获取一个对象,使用但不返回。它可能有副效果,例如更改对象的值或将某些输出写入某处。

Java Java8的函数式编程_第3张图片
Consumer 可视化
方法 功能
void accept(T t) 使用实例t,必要时修改它而不返回输出
void andThen(Consumer other) 形成消费链,调用完自己的accept()后 调用传入的Consumer 的accept()

4.5 Supplier

通过构造函数或其他方式(构建器,工厂等)提供给定类型的实例。

Java Java8的函数式编程_第4张图片
Supplier 可视化
方法 功能
T get() 提供一个实例

5、Lambda表达式

在上一节中,讨论了Functional Interface。由于这些实际上都是接口类,所以实现起来需要实现多行代码,这个函数式编程里面的单行编程理念所违背。于是在Java8中也同时推出了Lambda表达式用来简化需要编写的代码。以上一节的Function为例,原始的实现方法如下:


Function findWordCount = new Function(){

        @Overide

        public Integer apply(String t){

                return t.split(" ").length;

        }

}

使用Lambda表达式实现上述接口类


Function findWordCount  = (String t)->{  return t.split(" ").length; };

上面的Lambda表达式删除了匿名类外壳,只留下了剩余的部分。Lambda表达式一般由如下部分组成:

① 在括号内输入参数

② - > lambda签名

③ 返回单个结果的方法体

如果输入列表只有一个项目,则可以删除括号


Function findWordCount  = t ->{ return t.split(" ").length; };

下面是一些常见的lambda表达式语法:

表达式 例子
(parameters)->expression (List list )->list.isEmpty()
(parameters)->{return ... ;} (int a, int b )->{return a+b;}
(parameters)->statements (int a, int b )->a+b

lambda表达式的右侧是方法体。如果更喜欢使用return关键字,则必须遵循方法体语法并使用分号和花括号。但是如果喜欢语法糖,可以删除花括号和return关键字,如第三个表达式所示,但这种情况下必须是方法体里面只有一句代码。

上面的例子在复制号右侧编写了一个方法体。JDK会自动检查方法的输入和输出参数,并使用适当的Functional Interface类型包装此方法。比如如下lambda表达式:


t - > t%2 == 0

这个lambda表达式是符合上一节所说的Predicate接口的,所以可以用Predicate来引用该表达式。


Predicate predicate = t -> t % 2 == 0;

boolean isEven = predicate.test(5);

6、Stream API

在前面第二节举的关于函数式编程的例子里面用的就是Stream API,这个API也是在Java8新出的。这是一个非常经典的用于函数式编程的思想来进行处理的API,主要用来处理集合结构。这个API可以理解为将数据倒入到一个管道中,在整个管道中一些过滤器过滤掉不正确的项目,一些处理器转换项目,一些根据某些逻辑对它们进行分组,一些数据在整个管道中流式传输时计算一个值。

Java Java8的函数式编程_第5张图片
Stream 模拟

类似上图所示在流开始处,获取一些塑料球作为数据源,过滤红色球,将其融化并将其转换为随机三角形。之后过滤器过滤掉小三角形。最后计算出总周长。

使用常规Collection执行此操作意味着多次迭代,多次函数调用。使用常规Collection,无法并行化该过程。并且只能处理有限数量的对象。如果数据源是无限流式传输的,则在Java8之前就无法很好的进行处理。而采用Stream API就能很好的解决这些问题,其优点在于:

① 可以处理来自源的无限数量的对象

② 具有函数式编程风格

③ 能使用前面所说的Functional Interface

在Stream中,可以依次连接几个处理器。最后一个用于产生结果。此结果可能是示例中的标量值,也可能是全新的集合,例如List或Map。最后一个操作称为终端操作,因为它终止了流进程。其他处理器称为中间操作。终端操作后,无法再次使用该流,也无法再使用Stream的其他中间操作。

以下是中间操作:

Stream 操作 作用 输入
filter 根据给定的Predicate过滤对象 Predicate
map 根据给定对象对其进行相应的转换 Function
limit 限制集合个数 int
sorted 给对象进行排序 Comparator
distinct 根据对象的equals()过滤掉重复的对象
flatMap 将多个流合并成一个流 Function,Stream

以下是终端操作

Stream 操作 作用 输入
forEach 遍历每个对象,并执行传入的Consumer的操作 Consumer
count 输出对象个数
collect 将最终过滤的数据输出

你可能感兴趣的:(Java Java8的函数式编程)