在Java8中引入的众多特性中,lambda表达式是最值得提的;lambda的出现给Java语言带来了更多的可能性
到底是什么机会或者什么好处
使Java语言更有表达能,更简洁
虽然Java8已经发布很长,但是还是没有很好掌握他们,所以我们再次梳理总结;
函数式编程是一个编程风格或者模式
那这有事什么意思呢?简单理解就是对现实世界中问题抽象的方法或者工具,面向对象就将我们要解决的问题,都抽象成各种的
类
或者对象
,但是在函数式编程中,我们基础模块是函数
。都是我们认识或者理解实际问题的工具,可以互相补充。面向对象更加的容易,因为抽象中涉及的概念更贴近生活,但是函数在实际生活中似乎没有,更不用说,高阶等其他的数学特性。
从面向对象转向函数式编程,犹如从知道天圆地方到地球是圆的。
介绍
什么是Functional programming
他是一种源自lambda calculus
编程范式,注重声明式。而不是我们通常使用的命令式表达。
也就是他注重定义应该做什么,而不是定义怎么样做或者以什么顺序做
这句话很迷惑,没有上下文,但是到处都在说,只要是涉及函数式编程
t: 这里怎样做,主要是指通过什么线程模型等等完成,需要做的内容。
下面我们通过几个例子来理解,什么是命令式编程,什么是声明式编程。
声明式,从更高的抽象或者另一个角度,抽象问题
我们通过一个从一个数组中返回大于某个数的数组,我们先看看命令式是怎么实现的
public static Integer[] filterGreaterThan(int threshold, int[] array) {
List<Integer> greaterThanNumbers = new ArrayList<>();
for (int number : array) {
if (number > threshold) {
greaterThanNumbers.add(number);
}
}
return greaterThanNumbers.toArray(new Integer[greaterThanNumbers.size()]);
}
这个方法中我们指定了获取大于一个数的数组的每一步。也就是指定了具体怎么做
int[] numbers = new int[]{1, 23, 13, 9, 26, 90, 3, 8};
final Integer[] greaterThan = filterGreaterThan(10, numbers);
System.out.println(Arrays.toString(greaterThan));
结果是 [23, 13, 26, 90]
结果没有错,就是代码太冗余,我们看看函数式怎么实现
final int[] greaterThanTen = Arrays.
stream(numbers).
filter(number -> number > 10).toArray();
System.out.println(Arrays.toString(greaterThan));
在这个例子中我们使用Java中的stream处理数组,是用Predicate告诉stream怎样过滤元素
在stream 中通过 operator声明式的表达要做什么,通过function告诉具体怎么做 stream是什么,就是函数式编程吗?
Predicate【函数式接口,后面会讲】是一个接受一个参数,返回布尔值的Function,他只是众多Java8 提供的Function中的一个
都在Java.util.function包中中
输出的结果也是: [23, 13, 26, 90],但是实现的方式不一样。
和上面的方法比较有什么不同呢?
- 我们指定的需要做什么,而不是怎样做
- 更简洁
- 减少重复代码
- 容易阅读和理解
- 天生的不可变性
这个对于并发编程很重要
- 通过lambda不能修改状态
对于并发操作非常重要,将会再其他的文章中介绍
- 不会再函数中修改参数状态
- 惰性求值Lazy evaluation
我们声明需要做什么,但是函数不会执行,直到stream形成或者函数被调用
好的,上面简单介绍完函数式编程之后,我们现在来学习,最重要的内容Java中的函数
Functions 函数
首先,我们需要了解一个重要的概念,这个对我们理解函数式编程很重要
- 纯函数 pure function
任何时间,同样的参数返回同样的结果;而且没有副作用,就是不影响外界
- 非纯函数 impure function
和纯函数相反,不能总是返回相同的结果,而且有副作用
- 高阶函数 Higher-order-functions
首先它是一个函数,高阶函数可以接受函数或者返回函数的函数
int number = 9;
final Function<Integer, Integer> impureTimesFunction = times -> number * times;
System.out.println(impureTimesFunction.apply(2));
这个函数不纯,就是应该他依赖外部的变量;
所他的输出不能总是一样,例如我们将number修改为8,输出就为16,但是我们输入并没有变
那什么是纯函数呢?我们将上面的函数改造成一个纯函数,结果如下:
final BiFunction<Integer,Integer,Integer> pureTimesFunction = (numb,times) -> numb * times;
System.out.println(pureTimesFunction.apply(9,2));
上面的函数比之前的多了一个参数,这样我们就能保证这个函数输出始终是一样的,不收外界的影响,也不影响外界,没有副作用。
这就是纯函数
纯函数真的有这么重要吗?并发编程中对不变性和非共享状态有需求,而纯函数就提供了这样的支持,这个对于并发编程太重要了
为了能更形象的理解纯函数,我们将纯函数想象成pipe,管道,pipe就是输入通过的地方,安全而且封闭,其他线程不能访问到。
这个概念对于我们写出简单、安全的并发程序非常重要
现在我们对函数式编程核心概念有了一些 的了解,接下来,我们看看Java是怎么整合这些概念以。
Java中的函数式编程
主要接口
怎么说着函数,说到了接口
从接口有说到了函数式接口,哪到底什么是函数,接口和函数有啥区别
最主要就是下面三个接口
- Supplier
- Consumer
- Function
还有这个三个函数的变种,理解和掌握着三个接口,其他的接口就能很快掌握
下面我们看看,在Java中着三个接口是怎么使用的
lambda 表达式和匿名类
我们可以使用lambda表达式表达上面的接口。
lambda表达式可以让我们脱离类的存在定义函数,就是函数的定义不在依附于类了
function自己做为一个对象,可以保存和作为参数传递
lambda 表达式的结构如下
(argument1, argument2) -> expression
举例之前我们讨论一下匿名类,匿名类是我们创建一个接口示例,而不创建实现类的方法
因此我们内联定义和实例化,但是不给名字,这就是他的名字的由来
知道了这个,我们现在实例化一个Supplier,我们同时使用匿名类和lambda表达看看
final Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "hello world";
}
};
这个实现没错,但是太烦了;看看lambda表达式怎么实现
final Supplier<String> supplier1 = () -> "hello world";
如上所示,使用lambda表达式,表达函数更加的简洁
下面看看其他的接口使用lambda表达式怎么表示 ?
final Supplier<String> supplier2 = () -> "hello world";
final Consumer<String> consumer = element -> System.out.println("This is next element: " + element);
final Function<String,String> upCase = word -> word.toLowerCase();
final Function<String,String> upperCaseUsingMethodReference = String::toUpperCase;
最后一行我们看到一个**::**操作,这是方法应用,如果一个函数只是调用另一个函数,我们就可以将这个方法引用赋值给这个函数。
函数接口
除了这些已经存在的接口,任何只有一个方法的接口都被认为是一个函数式接口
我么可以在这些类上添加 @FunctionalInterface 注解,但是这个注解知识给编译器使用的信息,并不会改变接口本身
编译器可以推断函数式接口的类型,只要函数的参数个数和返回值相同,因此我们可以自己定义函数式接口,并且应用在已有的方法上。
我们可以使用之前的例如做个试验,我们定义一个 GreaterThan
interface GreaterThan {
boolean apply(int number);
}
因为这个函数也是接受一个int返回一个boolean,我们可以代替之前示例中的predicate,
private static int[] elementsGreaterThan(int[] number, GreaterThan greaterThan) {
return Arrays.stream(number)
.filter(greaterThan::apply)
.toArray();
}
在上面的方法中 filter的参数预期是一个Predicate,但是GreaterThan和Predicate有相同的返回值类型和参数个数,所以编译器和自己推断成功
int[] numbers = new int[]{1, 23, 13, 9, 26, 90, 3, 8};
final int[] greaterThanTen1 = elementsGreaterThan(numbers,n -> n >10);
System.out.println(Arrays.toString(greaterThanTen1));
这个例子依然是解释Java中的函数式接口
Java Stream
Java8 中新添加的Stream特性,让我们可以定义一个操作或者方法的pipeline,处理一个元素的列(集合)。
Stream的特性之一就是延迟计算,就是stream中的方法或者操作不直接执行,直到遇到terminal 操作或者终止操作。 终止操作就是那些想获取值,例如 forEach, collect, sum等等
还有一个特性就是一个stream只能被消费一次。
如上所示,我们只是,声明了我们需要这个stream需要施加的操作,从string 到 int,但是没有终止操作。
如上所示,我们在之前的基础上增加了,终止操作sum。
总结,stream可以让我们使用简单的方法定义一个整个集合的操作pipeline。没有终结操作时不执行,有终结操作开始执行。
试想如果没有stream如果我们想实现一个操作集合施加到集合上,应该怎么实现 ??
常见的stream的方法
- filter
- map
- flatMap
- reduce
其他的特性支持
现在我们看看除了函数式特性,还增加了那些特性
CompletableFuture
异步编程工具,在其中大量使用function功能。使用function功能使定义回调和串联多个CompletableFuture变得的简单
Optional
NPE 工具
这些一个处理NPE的工具,可以是我们的代码简洁。
Collection.forEach
还是和函数式编程功能相关,可以通过给forEach方法提供函数,这些就可以针对每个元素执行这个函数。
collection.forEach(element -> System.out.println(element));
还有Iterator.forEachRemaining 或者 Collection.removeIf.