函数式编程

从 JDK 8 开始,使用了函数式编程,函数式编程的引入,大大加强了 Java 的开发能力和一直为外界所诟病的“不简洁”的毛病

概念

函数式编程时一种相对于命令式编程的一种编**程范式,不是一种具体的技术,而是一种方法论**,是一种如何搭建应用程序的方法论

为什么要使用函数式编程

函数式编程与命令式编程的区别

函数式编程 命令式编程
关注点 做什么?实现什么功能? 如何做?步骤是什么?
  1. public static void main(String[] args) {
  2. int[] nums = {33, 44, 55, -14, 0, 92, -123, 19};
  3. // 命令式编程:告诉程序该怎么做
  4. int min = Integer.MAX_VALUE;
  5. for (int num : nums) {
  6. if (num < min) {
  7. min = num;
  8. }
  9. }
  10. System.out.println("min = " + min);
  11. // 函数式编程:告诉我要什么
  12. int asInt = IntStream.of(nums).min().getAsInt();
  13. System.out.println("min = " + asInt);
  14. }

Lambda 表达式

lambda 表达式所可以调用的结构,必须只有一个方法

  1. @FunctionalInterface
  2. interface Interface1 {
  3. int doubleNum(int i);
  4. }
  5. public class LambdaDemo01 {
  6. public static void main(String[] args) {
  7. Interface1 i1 = (i) -> i * 2;
  8. Interface1 i2 = i -> i * 2;
  9. Interface1 i3 = (int i) -> i * 2;
  10. Interface1 i4 = (int i) -> {
  11. return i * 2;
  12. };
  13. }
  14. }

**

@FunctionalInterface

@FunctionalInterface 注解,说明的是下面的接口是一个函数式接口有且只能提供一个方法,当提供多个方法的时候,编译器会报错

  1. @FunctionalInterface
  2. interface Interface1 {
  3. int doubleNum(int i);
  4. }

接口默认实现方法 default

JDK 8 以前,接口是不可以有方法实现的,JDK 8 提供了要给接口中的关键字 default,可以有默认实现方法

  1. @FunctionalInterface
  2. interface Interface1 {
  3. int doubleNum(int i);
  4. default int add(int x, int y) {
  5. return x + y;
  6. }
  7. }

函数接口

Demo

先看一个函数接口的案例,我们输入一个 int 类型的数值,然后给对这个数值进行格式化,返回 String

  1. interface IMoneyFormat {
  2. String format(int i);
  3. }
  4. class Money {
  5. private int money;
  6. public Money(int money) {
  7. this.money = money;
  8. }
  9. public void printMoney(IMoneyFormat format) {
  10. System.out.println("存款: " + format.format(this.money));
  11. }
  12. }
  13. public class MemoryDemo {
  14. public static void main(String[] args) {
  15. Money myMoney = new Money(99999999);
  16. // lambda 表达式并不关心接口是什么,方法名是什么,只关心入参和返回值
  17. // 那么这么一说,其实定义接口就显得不必要了
  18. myMoney.printMoney(i -> new DecimalFormat("#,###").format(i));
  19. }
  20. }

lambda 表达式并不关心接口是什么,方法名是什么,只关心入参和返回值,那么这么一说,其实定义接口就显得不必要了,所以 JDK 8 提供了一个函数接口 Function

  1. class Money {
  2. private int money;
  3. public Money(int money) {
  4. this.money = money;
  5. }
  6. public void printMoney(Function<Integer, String> format) {
  7. System.out.println("存款: " + format.apply(this.money));
  8. }
  9. }
  10. public class MemoryDemo {
  11. public static void main(String[] args) {
  12. Money myMoney = new Money(99999999);
  13. myMoney.printMoney(i -> new DecimalFormat("#,###").format(i));
  14. }
  15. }

使用函数接口 Function 除了可以帮我们简化代码之外,还可以提供链式编程,比如,我们还需要再格式化数字之前加上货币前缀,如:¥99,999,999

  1. public class MemoryDemo {
  2. public static void main(String[] args) {
  3. Money myMoney = new Money(99999999);
  4. Function<Integer, String> moneyFormatFunc = i -> new DecimalFormat("#,###").format(i);
  5. // 函数接口链式操作
  6. myMoney.printMoney(moneyFormatFunc.andThen(s -> "¥ " + s));
  7. }
  8. }

相关函数接口表

接口 入参 返回类型 说明
Predicate T boolean 断言
Function T R 输入 T,输出 R 的函数
Consumer T void 消费一个数据(消费者)
Supplier - T 提供一个数据(生产者)
UnaryOperator T T 一元函数,输入输出类型相同
BinaryOperator T, T T 二元函数,输入输出类型相同
BiFunction T, U R 2 个输出的函数
  1. public class FunctionDemo {
  2. public static void main(String[] args) {
  3. // 断言函数
  4. Predicate<Integer> predicate = i -> i > 0;
  5. System.out.println(predicate.test(-10));
  6. // 消费函数
  7. Consumer<String> consumer = s -> System.out.println(s);
  8. consumer.accept("hello was consumer");
  9. // 生产函数
  10. Supplier<String> supplier = ()-> "has been produced";
  11. System.out.println(supplier.get());
  12. }
  13. }

方法引用

箭头函数的左边**入参右边是执行体**,当执行体中只有一个函数的调用,且调用的入参和左边的入参是相同的时候,就可以使用方法引用

  1. public static void main(String[] args) {
  2. // Consumer<String> consumer = s -> System.out.println(s);
  3. Consumer<String> consumer = System.out::println;
  4. consumer.accept("test");
  5. }

方法引用总体分为三种类型,静态方法引用非静态方法引用构造函数引用

  1. class Dog {
  2. private String name;
  3. private int food = 10;
  4. public Dog() {
  5. this.name = "匿名";
  6. }
  7. public Dog(String name) {
  8. this.name = name;
  9. }
  10. public static void bark(Dog dog) {
  11. System.out.println(dog.name + "-汪汪汪");
  12. }
  13. // 非静态方法前面隐藏了一个默认参数,就是对象实例,这个参数名为 this
  14. // 所以方法中可以使用 this 关键字
  15. public int eat(int num) {
  16. System.out.println(name + " 吃了 " + num + " 斤狗粮");
  17. this.food -= num;
  18. return food;
  19. }
  20. @Override
  21. public String toString() {
  22. return "Dog{" + "name='" + name + '}';
  23. }
  24. }

**

静态方法引用:类名::方法名

  1. // 静态方法的引用,类名::方法名
  2. // Consumer<Dog> consumer2 = dog -> Dog.bark(dog);
  3. Consumer<Dog> consumer2 = Dog::bark;
  4. Dog dog = new Dog("哮天犬");
  5. consumer2.accept(dog);

非静态方法引用:实例对象::方法名 或 类名::方法名

  1. // 非静态方法的引用,对象实例::方法名
  2. // Function<Integer, Integer> function = num -> dog.eat(num);
  3. // Function<Integer, Integer> function = dog::eat;
  4. UnaryOperator<Integer> function = dog::eat;
  5. System.out.println("剩余狗粮: " + function.apply(3) + " 斤");
  6. // BiFunction<Dog, Integer, Integer> biFunction = (d, i) -> d.eat(i);
  7. BiFunction<Dog, Integer, Integer> biFunction = Dog::eat;
  8. System.out.println("剩余狗粮: " + biFunction.apply(dog, 2) + " 斤");

实例对象::方法名,这种情况就不描述了,我们要说一下,为什么非静态方法也可以使用 类名::方法名 这种模式呢?

首先,我们要知道非静态方法中间是可以使用 this 关键字的,而 this 关键字,我们都知道是代表当前实例对象,那么 this 是从哪里来的呢?

其实 JDK 编译的过程中,会自动给方法的加上一个参数,这个参数的类型是当前类,参数名是 this,而且一定是再第一位,是不是豁然开朗,我们用代码证明一下

  1. public int eat(Dog this, int num) {
  2. System.out.println(name + " 吃了 " + num + " 斤狗粮");
  3. this.food -= num;
  4. return food;
  5. }

我们给 eat 方法加上了一个参数 Dog this,当加上这个参数是,其实后面对这个方法的调用,并不需要传递两个参数,就好像并没有加入这个参数

  1. Dog dog = new Dog("哮天犬1");
  2. dog.eat(1);

是不是非常的 amazing~~~,为了更具有说服力,我们看一下编译后的字节码文件
image.png
通过查看 eat 方法的本地变量表,我们看到有两个参数,其中 0 代表第一个位置的参数,可以看到类型是 Dog,名称是 this,OK,bingo~

这就解释了为什么可以使用 BiFunction biFunction = Dog::eat; 这样的函数调用方式了

构造函数的方法引用 类名::new

构造函数分为无参构造函数,和含参构造函数

  1. // 构造函数的方法引用,类名::new
  2. // 无参构造函数
  3. Supplier<Dog> supplier = Dog::new;
  4. System.out.println("创建了新的对象: " + supplier.get());
  5. // 含参构造函数
  6. Function<String, Dog> function1 = Dog::new;
  7. System.out.println("创建了新的对象: " + function1.apply("旺财"));

其实选择什么样的函数,主要是要分析入参和返回值类型,这样就可以分析出所需要的函数了

类型推断

  1. @FunctionalInterface
  2. interface IMath {
  3. int add(int x, int y);
  4. }
  5. @FunctionalInterface
  6. interface IMath2 {
  7. int sub(int x, int y);
  8. }
  9. public class TypeDemo {
  10. public static void main(String[] args) {
  11. // 变量类型的定义
  12. IMath lambda = (x, y) -> x + y;
  13. // 数组
  14. IMath[] lambdas = {(x, y) -> x + y};
  15. // 强转
  16. Object lambda1 = (IMath) (x, y) -> x + y;
  17. // 通过返回类型
  18. IMath lambda2 = createLambda();
  19. // 方法重载的调用
  20. TypeDemo demo = new TypeDemo();
  21. demo.test((IMath) (x, y) -> x + y);
  22. demo.test((IMath2) (x, y) -> x + y);
  23. }
  24. public int test(IMath iMath) {
  25. return iMath.add(1, 3);
  26. }
  27. public int test(IMath2 iMath) {
  28. return iMath.sub(4, 2);
  29. }
  30. public static IMath createLambda() {
  31. return (x, y) -> x + y;
  32. }
  33. }

变量的引用

image.png

这里的 list,并没有标记为 final,那么可以说明,lambda 表达式可以不需要变量是 final 类型的了么?其实不是,只是 JDK 8 做了一些优化,让我们可以不用显示的增加 final 关键字

我们知道 Java 的变量传递是值传递,而非引用传递,通过上图,我们可以看出,传递过程是这样的,编号 1 的 list 指向 new ArrayList,当传递给 lambda 表达式的时候,编号 2 的 list 也指向 new ArrayList,所以两个变量 list 都指向同一个对象,所以,如果外部,改变了 list 的指向,那么内部就会造成数据紊乱,那么如果 编号 2 的 list 指向编号 1 的 list,然后 编号 1 的 list 指向 new ArrayList 的话,那么外部可以任意修改指向,这也证明了 Java 值传递,而非引用传递

级联表达式和柯里化

柯里化:把多个参数的函数,转化为只有一个参数的函数
柯里化的目的:把函数标准化,所有函数都只有一个参数,可以组合使用
高阶函数:返回函数的函数

  1. public class CurryDemo {
  2. public static void main(String[] args) {
  3. // 级联表达式
  4. Function<Integer, Function<Integer, Integer>> function = x -> y -> x + y;
  5. Integer result = function.apply(2).apply(3);
  6. System.out.println("result = " + result);
  7. Function<Integer, Function<Integer, Function<Integer, Integer>>> func = x -> y -> z -> x + y + z;
  8. System.out.println(func.apply(2).apply(2).apply(2));
  9. int[] nums = {2, 2, 2};
  10. Function f = func;
  11. for (int num : nums) {
  12. if (f instanceof Function) {
  13. Object object = f.apply(num);
  14. if (object instanceof Function) {
  15. f = (Function) object;
  16. } else {
  17. System.out.println("调用结束: " + object);
  18. }
  19. }
  20. }
  21. }
  22. }