noteless 头像

[一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念

 
本文是针对于java8引入函数式编程概念以及stream流相关的一些简单介绍

什么是函数式编程?

 
java程序员第一反应可能会理解成类的成员方法一类的东西
此处并不是这个含义,更接近是数学上的函数
看一下百度百科中关于函数的说明
函数的定义:
给定一个数集A,假设其中的元素为x。
现对A中的元素x施加对应法则f,记作f(x),得到另一数集B。假设B中的元素为y。
则y与x之间的等量关系可以用y=f(x)表示。
我们把这个关系式就叫函数关系式,简称函数。
函数概念含有三个要素:定义域A、值域C和对应法则f。
其中核心是对应法则f,它是函数关系的本质特征。
 
image_5b790d18_713d_thumb[1]
 
对应于编程来说,当然不是完全的数学上的函数定义
所谓函数式编程我们可以理解为:
通过对应法则f(x) 对指定的x 进行处理,映射成另外一个值
而且不会对x本身产生变动
所谓不会对x产生变动,你可以理解为无副作用,或者说副作用不会被察觉
副作用你可以理解为解题过程中对数据的修改
说起来好像很啰嗦,但是如果有人告诉你 通过sin(x) 计算后, x的值被改变了,你不会觉得异常奇怪么
函数式编程就是把函数的一些特性应用于编程语言之中
 
注意:
函数式编程不是某一种语言,也不是某个API
他是一种方法论,是一种编程范式,有它自有的一些特性和规定
语言中引入函数式编程,也就是用语言本身定义了函数式编程的一些特性和规定
 
函数式编程最重要的基础是λ演算,而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。
它一套用于研究函数定义、函数应用和递归的形式系统
我们只需要知道λ演算是一种形式的匿名函数,并且接收一个参数作为输入 (可以柯里化进行参数转换多参数函数转换为单参数)
有兴趣的可以去探究下λ演算
 

函数式编程有下列特性

 
闭包和高阶函数
闭包就是能够读取其他函数内部变量的函数,是个不太好理解的概念
此处我们仅仅理解成 函数可以当做值进行传递并且可以使用变量保存 是"第一等公民"
一等公民或者一等类型的含义就是指可以跟值一样的地位,作为参数传递或者存储于变量中 
高阶函数是指可以用另一个函数(间接地,用一个表达式) 作为其输入参数,比如 f(g(x))=g(x)+1 的形式
 
惰性计算
表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算
你可以理解为流水线上每一个节点都只是做了一系列的设置,并没有立刻去计算数值
 
没有副作用
副作用是指在运算过程中,修改了函数内部局部变量以外的其他变量的状态,比如你修改了类成员变量
没有副作用也就意味着不产生运算以外的其他结果,不修改系统的变量
 
引用透明性
如果提供同样的输入,那么函数总是返回同样的结果
也就是说表达式的值不依赖于可以改变值的全局状态,比如不依赖成员变量的值
 

为什么要使用函数式编程?

 
关注做什么 更接近于自然语言
任务分解后你的解题思路将是如何调用各个不同的函数,要做什么
不在关注于函数内部的细节本身去思考怎么做
 
 
假设有这么一组Student学生类型的List数据,学生有性别男女
如果在Java代码中,你会如何解题?
伪代码:
List<Student> 男List ;
for(int i=0;i<studentList.length;i++){//studentList 为学生列表,其中有男有女
if(studentList[i].性别 == 男){
    男List.add(List[i])
}

 

你循环遍历列表,找到符合条件的学生,然后把他加入另外一个列表,这可能是一种常见的解题思路
 
假设有个Student 学生表,每条记录都有一个性别字段值为男女
如果是在数据库中查询呢,一种可能的解法是这样子的
select
*
from
student
where
sex='';

 

他们的主要区别是什么?
一个最直观的差别就是:
java代码中是你自己去循环数据项,你自己处理每一项数据,找出符合你要求的数据
SQL查询中,你只是传入通知条件where  sex='男';  ,数据库在自己内部进行了循环,帮我们找出来符合要求的数据
这就是外部循环和内部循环,这是一种思维方式的转变
外部循环,需要程序员自己去关注每一个数据项
内部循环,程序员只需要关注结果
内部循环以及函数调用 也将我们从如何做中解放出来,让我们不再关注数据项循环的细节本身,仅仅关注于此次调用的结果
 
不管是什么方式进行思考编程,你都会将你的任务进行分解
划分为更小的子任务
但是不同的是:
如何做的思维下,你还需要思考在每个子任务中,每一个细节是怎么处理的,比如循环中进行条件判断
这其实还是往计算机的思维倾斜的一种思考方式,这是指令式或者命令式的编程模式
 
做什么的思维下,你不在关注每个子任务的内部细节,只在乎结果也就是"做什么"
每个子任务内部的细节是函数自己内部的事情,这更加符合人的思维习惯
 
内部循环不也是函数式编程的一种表现形式么
函数本身如同一个黑盒一般,有输入有输出,我们不关心内部的实现细节,仅仅在乎输入和输出
内部循环也是如此,我们告诉他我们想要的结果行为,他返回给我们结果
比如SQL中
where   sex='男';  这就是对我们行为的描述(不要把它理解成筛选条件)
我们将行为像参数一样传递给了数据库软件,数据库执行查询操作,根据的是我们给定的行为
 
这就是行为参数化的魅力所在
行为参数化也是一种思维模式,只要能把行为像参数一样进行传递  就是行为参数化
有人可能已经想到了匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println();
}
}).start();

 

的确这也是一种行为参数化,但是显然,这段代码还不够简洁纯粹,因为方法的外层还套了一层对象
Java8中的行为参数化,传递的将是更加纯粹的行为,而不再需要借助一个匿名对象的形式,而且,Lambda表达式不会像内部类一样生成一个类
传递的是方法本身,方法中的代码本身
那么行为参数化,不也就是函数式编程中的闭包特性么
 
 
更加易于并发编程
函数式编程的准则是没有副作用不依赖外部的数据,也不改变外部数据的值。
我们知道线程安全的根本在于共享数据,如果没有任何的数据共享,那么很多的并发/线程安全问题都将迎刃而解
所以说这一特性正好满足了多核并行程序设计的需求,所以很显然能够简化并行程序的开发
 
 
函数式编程代码简洁
函数式编程大量使用函数,减少了代码的重复,就如同你调用别人的方法一样不是么,一行就得到了结果

Java8 对于函数式编程的支持

 
编程语言把函数式编程的概念引入,也就是使自身支持函数式编程的特性,换句话说也就是
在语言内部可以使用一系列的类型或者关键字或者符号组合等进行表示
 
Java主要涉及这三个核心概念
  • 函数接口(FunctionalInterface)
  • 流(Stream)
  • 收集器(Collector)
 
函数接口
 
既然函数式编程要求函数可以是同值一样的一等公民用于参数化传递,那么必须要有表示函数的类型
先说一下函数式接口的注解
 
注解@FunctionalInterface   描述了什么是一个函数式接口
public @interface FunctionalInterface {}
image_5b790d18_4458_thumb[1]
 
上面的注释也就相当于是函数式接口的定义:
一个函数式接口只能有一个抽象方法,default方法有实现,所以不是抽象方法 
如果一个接口声明了一个覆盖Object  public公有方法的抽象方法,也不算是抽象方法
所以说:函数式接口,有且仅有一个抽象方法,覆盖Object的public方法不计算在内(如果是覆盖Object的protected那么会计数的) 
比如
java.lang.Runnable、java.util.Comparator是典型的函数式接口
 
函数接口是一个接口,有且只有一个唯一的抽象方法
 
接口上定义了函数的类型参数
抽象方法的方法签名限定了函数(函数式接口的抽象方法的签名称为函数描述符)
所以说一个函数接口,只能描述一种类型的函数
 
比如
Function<T, R>      这个函数接口
image_5b790d18_5ba3_thumb[1]
他表示形如
R function(T){
    ....
  return R
}
他的类型参数是T  R,调用方法apply 输入为T   输出为R
作用为转换一个对象为不同类型的对象
所有这种形式的函数都是这个函数接口类型
比如
public static void main(String[] args){
Function<String,Boolean> function = (String x)->x.equals("true");
System.out.println(function.apply("1"));
System.out.println(function.apply("true"));
}

 

image_5b790d18_906_thumb[1]
 
 
至此,Java中已经有了用于表示函数的类型了,也就是可以定义一个函数或者返回一个函数,或者把函数当做一个参数值进行传递了
以赋值运算符的形式来类比的话就是
比如
int i = 1;
等号左边的类型已经有了就是函数接口
但是右边,也就是行为参数化这个行为到底如何表示呢?也即是上面的1 的位置
在java中可以使用
  • Lambda表达式((String x)->x.equals("true"))
  • 方法引用(String::length)
两种形式进行表示
比如
public static void main(String[] args){
Function<String,Integer> function = String::length;
System.out.println(function.apply("1"));
System.out.println(function.apply("true"));
}

 

image_5b790d18_316d_thumb[1]
 
既然每一种函数类型都需要存在指定形式的函数接口,想要使用Lambda-匿名函数或者方法引用,自然需要定义函数接口
函数类型的说法可能不太准确,函数式接口的抽象方法的签名称为函数描述符 其实说的也都还是方法签名  方法签名唯一的标识了一个函数
 
Java8 也已经给我们预置了一些常用的函数接口类型  
已经定义一套能够描述常见函数描述符的函数接口
比如上面提到的
function  就是其中一种
另外还有其他一些,后面再说,我们已经可以在Java中表示一个函数,并且对函数进行调用
 

流,流动,流水,java中早就已经有了IO流,形象的表达了数据在程序中的处理与流动
Java8中的Stream流则更倾向于流水线的含义
每个节点有各自独立的功能目的,根据你的目的(做什么),将各个独立的功能目的节点拼接成一整个的完整的流水线
数据在此流水线上进行加工处理,最终得出结果
通过告知Stream "做什么" 来进行数据操作和处理
你不在需要关注内部的细节,Stream通过内部迭代进行数据项的筛选查找,找到符合条件的数据 
 
流(Stream)是Java8对函数式编程的重要支撑。大部分函数式工具都围绕Stream展开
也可以说Stream类是Java8 关于函数式编程定义的一些列函数集合
由此可以看得出来,Stream的重要性  
想要使用Java进行函数式编程,仅仅使用Lambda表达式是不够的,必须有足够的函数,Lambda表达式只有跟stream一起使用才能显示其真实的威力
 
集合是一种数据结构用于存储数据
Stream不是一种数据结构,是对于数据的一种新的视图,用于数据的计算,提供了一系列的API用于调用
 
概括的说
Stream就是函数式编程中编程语言提供出来的库方法集合,而参数基本上都是函数
所以才说,Lambda表达式只有跟stream一起使用才能显示其真实的威力
image_5b790d18_3c54_thumb[1]
 
常用的Stream调用流程
image_5b790d18_5d3e_thumb[1]
 
 
1.获得Stream
想要使用Stream的一些特性,显然你必须把你的数据集转换生成为Stream,这没有Stream何谈使用?
 
2.设置行为类型 也就是操作类型
这句话有些模糊不清,其实就是你需要设置想干什么
到底是筛选数据?转换数据?求和还是怎样?
你可以类比为SQL查询中到底是SELECT 还是UPDATE 或者DELETE? 这就是行为的类型
为了更快理解的话,你可以片面的理解为调用Stream类的方法
我们举例说明
比如你经常让同学帮你买东西
买东西就是行为类型,是去买东西,既不是帮你开车也不是陪你看电影
这就是行为的类型
Stream中有一系列的API可以帮助我们达到这个目的
比如 filter  map等等
3. 确定行为参数 也就是操作内容
行为参数也就是基于已经设置的行为类型下,你具体要以什么样子的行为去执行
你筛选数据筛选什么样子的数据?
转换数据,转换为什么形式?
类比为SQL查询中就是查询条件,查询  男生?查询 女生? 这就是行为的具体方式
还是刚才的例子,你经常让同学帮你买东西,那到底买什么?买矿泉水还是买面包?这就是确定行为参数
Java8中使用方法引用或者Lambda-匿名函数  或者方法引用来表示行为参数
4.行为的属性
既然是流水线式的工作方式,那么当前的工作结束后或许结束了或许是进入到流水线的下一环节
当然最终他肯定还是会结束掉的
这就又涉及到绑定行为方法的属性种类  到底是中间的操作(可以继续传递给流水线下一步)  还是结束的终端操作
中间操作的返回结果还是一个Stream  你仍旧可以对他进行上述类似的过程
终端操作则一般会将流进行收集整理成指定的数据结构
这基本上是一个常用的Stream使用流程 
 
流程处理虽然很简单,但是强大之处在于中间操作处理后仍旧是流
这就意味着你可以按照需要进行无数的变换组合以达到你想要的效果

收集器

 
Stream结合Lambda表达式可以对于数据进行各种各样的操作
但是Stream 终归是Stream ,它并不是一种数据结构,不管经过了多少处理,他终归是再次返回到代码中具体的其他数据类型中
把Stream类比做数据项处理的流水线的话
中间操作就是流水线上的一个个的功能操作节点
而收集器就是在某些结束操作中用于将数据进行转换的工具
在Java中关于收集器有几个关键的概念
1. Stream中的collect 方法是收集器的调用者
<R, A> R collect(Collector<? super T, A, R> collector);
2. Collector 接口 定义了收集器
public interface Collector<T, A, R> {
3. 收集器工厂Collectors  用于预置一些收集器
public final class Collectors
比如   .....collect(Collectors.toList());  就是把一个处理后的流转换为List
 
 
总结:
 
Java8 构建了三个主要概念,函数接口,流,收集器
有了函数接口  函数拥有了类型也就是可以像值一样作为参数进行传递,作为返回值,或者使用变量进行表示
使用Lambda-匿名函数或者方法引用来表示行为参数  也就是函数的值
Stream是Java8 提供的函数式编程的"库函数" 预定了一些常用的操作模式,通过Lambda表达式结合使用
收集器用于把Stream处理后的数据进行打包整理成你需要的数据结构
 
 
posted @ 2018-08-19 14:32 noteless 阅读(...) 评论(...) 编辑 收藏