1⃣️ 概念
- 函数式编程是种编程方式,它将电脑运算视为函数的计算。将业务逻辑细化,抽象,封装成一个个功能函数,并借助语言自带的高阶函数api,将整个业务流程转化为函数之间的相互调用,这就是函数式编程。
2⃣️ 为什么要使用函数式编程
- 与命令式编程相比,函数式编程更加强调做什么,在函数式编程中我们只需要关注做什么样的事情,不需要关注具体的实现细节;这么说可能有一点抽象,接下来我们根据例子说明一下:查找一个数组中的最小值;
两种编程方式实现对比
package lambda;
import java.util.stream.IntStream;
/**
* Created by wb-yxk397023 on 2018/7/18.
*/
public class MinDemo {
public static void main(String[] args){
int[] nums = {33,55,-55,90,-666,90};
// 命令式编程
int min = Integer.MAX_VALUE;
for (int i : nums){
if (min > i){
min = I;
}
}
System.out.println("命令式编程实现:" + min);
// 函数式编程实现
int asInt = IntStream.of(nums).min().getAsInt();
System.out.println("函数式编程实现:" + asInt);
}
}
在这里例子中我们使用命令式编程,需要用程序告诉计算机我们每一步需要做什么最终才能实现我们想要的结果;如果我们使用函数式编程我们只需要关注我们最终要实现的结果即可,而且从代码量的角度来看,也简洁了不少;
3⃣️ 初识Lambda
- Lambda简介
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
- 语法
(parameters) -> expression
或
(parameters) ->{ statements; }
- 特征
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
- Lambda 表达式实例
public class Java8Tester {
public static void main(String args[]){
Java8Tester tester = new Java8Tester();
// 类型声明
MathOperation addition = (int a, int b) -> a + b;
// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;
// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
执行以上代码输出的结果
10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google
- 需要注意的点
Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
4⃣️ JDK8的接口新特性
- @FunctionalInterface
标注为@FunctionalInterface的接口被称为函数式接口,该接口只能有一个自定义方法,但是可以包括从object类继承而来的方法。如果一个接口只有一个方法,则编译器会认为这就是一个函数式接口。
- 需要注意的点
1⃣️ 该注解只能标记在”有且仅有一个抽象方法”的接口上。
2⃣️ JDK8接口中的静态方法和默认方法,都不算是抽象方法。
3⃣️ 接口默认继承java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法。
4⃣️ 该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。
5⃣️ 在一个接口中定义两个自定义的方法,就会产生Invalid ‘@FunctionalInterface’ annotation; FunctionalInterfaceTest is not a functional interface错误.
- 使用时的一些建议
1⃣️ 一个接口中只定义一个方法,也叫做单一责任制;
2⃣️ 通过接口的多继承来实现相对比较复杂的功能;
5⃣️JDK8中新增的函数接口
Predicate接口
Predicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):Function 接口
Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):Supplier 接口
Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数Consumer 接口
Consumer 接口表示执行在单个参数上的操作。Comparator 接口
Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法:Optional 接口
Optional 不是函数是接口,这是个用来防止NullPointerException异常的辅助类型,这是下一届中将要用到的重要概念,现在先简单的看看这个接口能干什么:
Optional 被定义为一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java 8中,不推荐你返回null而是返回Optional。Stream 接口
java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set, Map不支持。Stream的操作可以串行执行或者并行执行。
首先看看Stream是怎么用,首先创建实例代码的用到的数据List:Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。
Filter 过滤
过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。Sort 排序
排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:⚠️ 其实这也是函数式编程的一个好处:不会改变对象状态,每次都会创建一个新对象。
Map 映射
中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来讲对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。Match 匹配
Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。Count 计数
计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。Reduce 规约
这是一个最终操作,允许通过指定的函数来讲stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:并行Streams
前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。下面的例子展示了是如何通过并行Stream来提升性能:
首先我们创建一个没有重复元素的大表:然后我们计算一下排序这个Stream要耗时多久,
串行排序:// 串行耗时: 899 ms
并行排序:// 并行排序耗时: 472 ms
上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。
Map
前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。以上代码很容易理解, putIfAbsent 不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。
下面的例子展示了map上的其他有用的函数:接下来展示如何在Map里删除一个键值全都匹配的项:另外一个有用的方法:对Map的元素做合并也变得很容易了:Merge做的事情是如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。
6⃣️ 方法引用
- 好处
使用方法引用可以让我们的代码更加的简洁;
2.方法引用的种类
1⃣️静态方法引用:ClassName::methodName;对于静态方法引用,我们需要在类名和方法名之间加入::分隔符,例如:Integer::sum;
2⃣️实例上的实例方法引用:instanceReference::methodName;对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符;
3⃣️超类上的实例方法引用:super::methodName;
4⃣️类型上的实例方法引用:ClassName::methodName;
5⃣️构造方法引用:Class::new;
6⃣️数组构造方法引用:TypeName[]::new;
7⃣️ 类型推断
package lambda;
@FunctionalInterface
interface IMath {
int add(int x, int y);
}
@FunctionalInterface
interface IMath2 {
int sub(int x, int y);
}
public class TypeDemo {
public static void main(String[] args) {
// 变量类型定义
IMath lambda = (x, y) -> x + y;
// 数组里
IMath[] lambdas = { (x, y) -> x + y };
// 强转
Object lambda2 = (IMath) (x, y) -> x + y;
// 通过返回类型
IMath createLambda = createLambda();
TypeDemo demo = new TypeDemo();
// 当有二义性的时候,使用强转对应的接口解决
demo.test( (IMath2)(x, y) -> x + y);
}
public void test(IMath math) {}
public void test(IMath2 math) {}
public static IMath createLambda() {
return (x, y) -> x + y;
}
}
8⃣️ 变量引用
package lambda;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* 变量引用
*/
public class VarDemo {
public static void main(String[] args) {
final List<String> list = new ArrayList<>();
Consumer<String> consumer = s -> System.out.println(s + list);
consumer.accept("1211");
}
}
这里需要说明一下,为什么变量要定义为final的,在1.8中如果不定义为final的也是可以用的,只要后边不改变变量的值即可;因为我们在穿变量的时候实际上传的是值,为了避免结果的二义性变量需要定义为final的;
9⃣️ 级联表达式和柯里化
什么是级联表达式?
有多个箭头的Lambd表达式就是级联表达式,例如
什么是柯里化?
把多个参数的函数转换成只有一个参数的函数,例如
柯里化的意义。
1⃣️ 把函数标准化;把函数标准话以后,每个函数都只有一个参数,调用起来就非常的灵活,还可以执行其他的特性比如把函数组合起来;例如js中就有这样的操作:var a = add(2)(3)(4);
2⃣️ 高阶函数:返回函数的函数