OnJava学习笔记_函数式编程

onJava – 函数式编程 (2022.12.22)

概论

  • 函数式编程的意义:通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此我们会得到更可靠的代码,而且实现起来更快。(这个理论看起来是成立的,至少在某些情况下如此)
  • 可以这样认为:面向对象编程抽象数据,而函数式编程抽象行为
  • 纯函数式语言规定了额外的约束条件:所有的数据必须是不可变的:设置一次,永不改变。函数会接受值,然后产生新值,但是绝对不会修改自身之外的任何东西(包括其参数或该函数作用域之外的元素)。有了这一保证,可以知道不会再有任何所谓的 “副作用” 引起的bug,因为函数只是创建并返回了一个结果,其他的什么都没做 (纯函数式语言经常被当作并行编程问题的解决方案,当然也有其他可行的解决方案)

Lambda表达式

  • Lambda 表达式 是使用尽可能少的语法编写的函数定义

  • Lambda 表达式产生的是函数,而不是类在Java虚拟机(JVM)上,一切都是类,所以幕后会有各种各样的操作让Lambda 看起来像函数。但是作为程序员,我们可以开心地假装他们 “就是函数”

  • 基本语法:

    1. 参数;
    2. 后面跟 -> ,你可以将其读作 “产生” (produces);
    3. -> 后面的都是方法体。
  • 规则:

    1. 只有一个参数,可以只写这个参数,不写括号。不过这是一种特殊情况
    2. 通常情况是用括号将参数包裹起来。为了一致性,在单个参数时也可以使用括号,尽管这并不常见
    3. 在没有参数的情况下,必须使用括号来指示空的参数列表
    4. 在有多个参数的情况下,将它们放在括号包裹起来的参数列表内
    5. 如果方法体只有一行,方法体中表达式的结果会自动成为Lambda表达式的返回值,这时使用 return 关键字是不合法的。而当方法体有多行时,则必须将这些代码行放到花括号中,并需要使用 return 关键字 从 Lambda 表达式中生成一个值。
  • 代码示例

    package closure;
    
    /**
     * @title: LambdaExpressions
     * @description: Lambda 表达式
     * @author: sunwenlong
     * @date 2022/12/22 22:00
     */
    
    // 定义三个接口,每个接口中都有且只有一个方法(后续就能理解其意义了),为了演示Lambda表达式的语法,每个方法的参数数量不同
    interface ZeroArg {
        String zeroArg();
    }
    
    interface OneArg {
        String oneArg(String str);
    }
    
    interface TwoArgs {
        String twoArgs(String str, Double num);
    }
    
    public class LambdaExpressions {
        // 规则3
        static ZeroArg zero = () -> "zero arg";
    
        // 规则2
        static OneArg one = (h) -> h + "one arg";
        // 规则1
        static OneArg one1 = h -> h + "Arg";
    
        // 规则4
        static TwoArgs two = (h, n) -> h + n;
    
        // 规则5
        static ZeroArg moreLines = () -> {
            System.out.println("More lines");
            return "from moreLines()";
        };
    
        public static void main(String[] args) {
            System.out.println(zero.zeroArg());     // 输出:zero arg
    
            System.out.println(one.oneArg("Hi, "));     // 输出:Hi, one arg
            System.out.println(one1.oneArg("Ho, "));    // 输出:Ho, Arg
    
            System.out.println(two.twoArgs("Pi = ", 3.141592));     // 输出:Pi = 3.141592
    
            System.out.println(moreLines.zeroArg());      // 输出:More lines \n from moreLines()
        }
    }
    
  • 递归

    • 可以编写递归的 Lambda 表达式,但是有一点要注意:这个 Lambda 表达式必须被赋值给一个静态变量或实例变量,否则会出现编译错误。

      /**
       * @title: IntCall
       * @description: 两个用 Lambda 表达式 写递归示例的通用接口
       * @author: sunwenlong
       * @date 2022/12/22 22:27
       */
      public interface IntCall {
          int call (int arg);
      }
      
      /**
       * @title: RecursiveFactorial
       * @description: 用 Lambda 表达式 写递归 -- 阶乘
       * @author: sunwenlong
       * @date 2022/12/22 22:28
       */
      // 示例1:赋值给静态变量
      public class RecursiveFactorial {
      
          // 注意,不能在定义的时候像这样初始化 fact
          // 尽管这样的期望非常合理,但是对于Java编译器而言处理起来太复杂了,所以会产生编译错误
      //    static IntCall fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
      
          static IntCall fact;
      
          public static void main(String[] args) {
              fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
      
              for (int i = 0; i <= 10; i++) {
                  System.out.println(fact.call(i));
              }
          }
      }
      
      /**
       * @title: RecursiveFibonacci
       * @description: 用 Lambda 表达式 写递归 -- 斐波那契数列
       * @author: sunwenlong
       * @date 2022/12/22 22:35
       */
      // 示例2:赋值给实例变量
      public class RecursiveFibonacci {
          IntCall fib;
      
          RecursiveFibonacci() {
              fib = n -> n == 0 ? 0 :
                         n == 1 ? 1 :
                         fib.call(n - 1) + fib.call(n - 2);
          }
      
          int fibonacci(int n) { return fib.call(n); }
      
          public static void main(String[] args) {
              RecursiveFibonacci rf = new RecursiveFibonacci();
              for (int i = 0; i <= 10; i++) {
                  System.out.println(rf.fibonacci(i));
              }
          }
      }
      

方法引用

  • Java8 方法引用指向的是方法。方法引用是用类名或对象名,后面跟上 :: ,然后跟方法名

  • 注意,比如:Describe :: show 是产生一个引用,而不是调用该方法

  • 个人理解:方法引用好比于new 对象,比如

    Person p = new Person();
    
    // 方法引用产生该方法的一个引用,然后将该引用赋值给了 Callable 中的call() 方法,
    // 这样,通过调用call() 方法就能调用show() 方法了
    // 对象引用要能赋值给一个引用变量,需要类型相同或是继承关系,
    // 同样的,要把show() 方法的引用赋值给Callble,需要 show() 方法的签名 (参数列表和返回值类型)和Callable中的call() 一致
    Callable c = Describe::show
    
  • 示例:

    // 注意,这里的示例方法的签名都和 Callable中的 call() 方法是一致的
    interface Callable {
        void call(String str);
    }
    
    class Describe {
        void show(String msg) {
            System.out.println(msg + "!!!");
        }
    }
    
    public class MethodReferences {
        static void hello(String name) {
            System.out.println("Hello, " + name);
        }
    
        static class Description {
            String about;
            Description(String desc) {
                about = desc;
            }
    
            void help(String msg) {
                System.out.println(about + " " + msg);
            }
        }
    
        static class Helper {
            static void assist(String msg) {
                System.out.println(msg);
            }
        }
    
        public static void main(String[] args) {
            // 将Describe对象的一个方法引用赋值给一个Callable,Callable中没有show() 方法,只有一个call() 方法。
            // 然而,Java 似乎对这种看似奇怪的赋值并没有意见
            // 因为,这个方法引用的签名(参数类型和返回值类型)和Callable中的call() 方法一致
            Describe d = new Describe();
            // Java 将call() 映射到了show() 上
            Callable c = d::show;
            c.call("call()");       // 调用的show() 方法。 输出: call()!!!
    
            c = MethodReferences::hello;
            c.call("Bob");
    
            // 这是 Callable c = d::show 的另一个版本——绑定方法引用
            c = new Description("valuable")::help;
            c.call("information");
    
            c = Helper::assist;
            c.call("Help!");
        }
    }
    
  • Runable

    • Runable 接口在java.lang 包中,所以不需要import。它也遵从特殊的单方法接口格式:其run() 方法没有参数,也没有返回值。所以可以将Lambda 表达式或方法引用作Runable:

      class Go {
          static void go() {
              System.out.println("Go::go()");
          }
      }
      
      public class RunableMethodReference {
          public static void main(String[] args) {
              // 匿名内部类 方式
              // 三种方式,只有匿名内部类需要提供名为run()的方法
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      System.out.println("内部类Runable");
                  }
              }).start();
      
              // Lambda 表达式 方式
              new Thread(
                      () -> System.out.println("Lambda 表达式")
              ).start();
      
              // 方法引用 方式
              new Thread(Go::go).start();
          }
      }
      

未绑定的方法引用

  • 绑定方法引用:对某个活跃对象上的方法的方法引用,叫作 “绑定方法引用”,如上述方法引用示例中的 c = new Describe(“valuable”)::help

  • 未绑定方法引用:尚未关联到某个对象的普通方法(非静态方法)。对于未绑定引用,必须先提供对象,然后才能使用。

  • 示例

    class X {
        String f() { return "X::f()"; }
    }
    
    interface MakeString {
        String make();
    }
    
    interface TransformX {
        String transform(X x);
    }
    
    public class UnboundMethodReference {
        public static void main(String[] args) {
    //        MakeString ms = X::f;
            TransformX sp = X::f;
            X x = new X();
            System.out.println(sp.transform(x));
            System.out.println(x.f());
        }
    }
    
  • 上面的示例中的 MakeString ms = X::f ,编译器会报错,提示 “无效方法引用”,即使make() 和 f() 的签名相同。问题在于,这里事实上还涉及了另一个(隐藏的)参数:this。如果没有一个可供附着的 X 对象,就无法调用 f() 。因此,X::f 代表一个未绑定的引用,因为它没 “绑定到” 某个对象。

  • 为解决这个问题,我们需要一个 X 对象,所以我们的接口事实上还需要一个额外的参数,如 TransformX 所示。如果将 X::f 赋值给一个TransformX ,Java 会很开心地接受。我们必须再做一次心理调节:在未绑定引用的情况下,函数式方法(接口只有一个方法,那个单一的方法就是函数式方法)的签名与方法引用的签名不再完全匹配(函数式方法要多一个参数,第一个参数的类型必须是方法引用所属的类),因为我们需要一个对象,让方法在其上调用。

  • 示例

    class Args {
        void two(int i, double d) {}
        void three(int i, double d, String s) {}
        void four(int i, double d, String s, char c) {}
    }
    
    interface Two {
        // 需要比对应的Args 中的two() 多一个参数:Args args,并且必须是第一个参数
        void call2(Args args, int i, double d);
    }
    
    interface Three {
        void call3(Args args, int i, double d, String s);
    }
    
    interface Four{
        void call4(Args args, int i, double d, String s, char c);
    }
    
    public class MultiUnbound {
        public static void main(String[] args) {
            Two two = Args::two;
            Three three = Args::three;
            Four four = Args::four;
            Args arg = new Args();
            two.call2(arg, 11, 14);
        }
    }
    

构造器方法引用

  • 我们也可以捕获对某个构造器的引用,之后通过该引用来调用那个构造器

  • 构造器引用的写法是:类名::new

  • 示例

    class Dog {
        String name;
        int age;
        Dog() {name = "stray";}
        Dog(String name) { this.name = name; }
        Dog(String name, int age) {this.name = name; this.age = age;}
    }
    
    interface MakeNoArgs {
        Dog make();
    }
    
    interface Make1Arg {
        Dog make(String name);
    }
    
    interface Make2Args {
        Dog make(String name, int age);
    }
    
    public class CtorReference {
        public static void main(String[] args) {
            MakeNoArgs mna = Dog::new;
            Make1Arg m1a = Dog::new;
            Make2Args m2a = Dog::new;
    
            Dog dn = mna.make();
            Dog d1 = m1a.make("Comet");
            Dog d2 = m2a.make("Ralph", 4);
        }
    }
    
  • 所有这3个构造器都只有一个名字 ::new,但是在每种情况下,构造器引用被赋值给了不同的接口,编译器可以从接口来推断使用哪个构造器

函数式接口

  • 方法引用和Lambda 表达式都必须赋值,而这些赋值都需要类型信息,让编译器确保类型的正确性。考虑如下代码: (x, y) -> x + y

    因为Lambda 表达式包含了某种方式的类型推断(编译器推断出类型的某些信息,而不需要程序员显示指定),所以编译器必须能够以某种方式推断出 x,y 的类型。

    现在 x 和 y 可以是支持 + 操作符的任何类型,如两个数值类型,或者一个String 和某个能够自动转换为String的其他类型。但是在Lambda 表达式被赋值之后,编译器就必须确定 x 和 y 的精确类型并生成正确的代码了。同样的情况也适用于方法引用。

  • 为解决这个问题,Java 8 引入了包含一组接口的 java.util.function,这些接口是Lambda 表达式和方法引用的目标类型。每个接口都值包含一个抽象方法,叫做函数式方法

  • 当编写接口时,这种 “函数式方法” 模式可以使用 @FunctionalInterface 注解来强制实施

    @FunctionalInterface
    interface Functional {
        String goodbye(String arg);
    }
    
    // @FunctionalInterface 注解是可选的
    // @FunctionalInterface 的价值:如果接口中的方法多于一个,则会产生一条编译错误信息
    interface FunctionalNoAnn {
        String goodbye(String arg);
    }
    
    public class FunctionalAnnotation {
        public String goodbye(String arg) {
            return "Goodbye" + arg;
        }
    
        public static void main(String[] args) {
            FunctionalAnnotation fa = new FunctionalAnnotation();
            
            Functional f = fa::goodbye;
            FunctionalNoAnn fna = fa::goodbye;
            Functional fl = a -> "Goodbye" + a;
            FunctionalNoAnn fnal = a -> "Goodbye" + a;
        }
    }
    
  • 仔细看一下 f 和 fna 的定义中发生了什么。Functional 和 FunctionalNoAnn 定义了接口。然而被赋值给它们的只是方法googbye。首先,goodbye 只是一个方法,而不是类。其次,它甚至不是实现了这里定义的某个接口的类中的方法。这是Java 8 增加的一个小魔法:如果我们将一个方法引用或Lambda 表达式赋值给某个函数式接口(而且类型可以匹配),那么Java 会调整这个赋值,使其匹配目标接口。而在底层,Java编译器会创建一个实现了目标接口的类的实例,并将我们的方法引用或Lambda 表达式包裹在其中

  • java.util.function 旨在创建一套足够完备的目标接口,这样一般情况下我们就不需要定义自己的接口了。不详细讲述,具体见《on Java》基础卷 356 页

高阶函数

  • 高阶函数:只是一个能接受函数作为参数或能把函数当返回值的函数

  • 示例

    // 使用继承,可以轻松地为专门的接口创建一个别名
    // Function<> 是 java.util.function 提供的一个基本接口
    // Function<String, String> 代表该接口的唯一的方法的签名是参数列表只有一个String类型参数,返回值是String型(最后一个类型是返回值类型)    
    interface FuncSS extends Function<String, String> {}
    
    // 把函数当返回值
    public class ProduceFunction {
        static FuncSS produce() {
            return s -> s.toLowerCase();
        }
    
        public static void main(String[] args) {
            FuncSS f = produce();
            System.out.println(f.apply("HELLO, JAVA"));      // 输出:hello, java
        }
    }
    
    class One {}
    class Two {}
    
    // 接受函数作为参数
    public class ConsumeFunction {
        // Function<A, B, C> 中,最后一个也就是C,是 apply() 方法的返回值
        static Two consume(Function<One, Two> oneTwo) {
            return oneTwo.apply(new One());
        }
    
        public static void main(String[] args) {
            Two two = consume(one -> new Two());
        }
    }
    
    

闭包

  • 闭包:是一个可调用的对象,它保留了来自它被创建时所在的作用域的信息

  • Java 8之前,要生成类似闭包的行为,唯一的方法是通过内部类。现在Java 8 中有了Lambda表达式,它也有闭包行为,而且语法更漂亮,更简洁。所以首选Lambda表达式做闭包行为

  • 示例

    public class Closure1 {
        IntSupplier makeFun(int x) {
            int i = 0;
            // 该Lambda 表达式保留了 x,i (来自于被创建时所在的作用域的信息)
            return () -> x + i;		// 尝试去增加 i 或 x,会报错
        }
    }
    
  • 如果尝试去增加 i 或 x,会报错,提示:从lambda 表达式引用的局部变量必须是最终变量或实际上的最终变量

  • 需要注意,闭包中使用的其作用域外的局部变量(注意是局部变量),必须是最终变量(final 修饰的变量)或实际最终变量(该术语是为Java 8 创建的),所谓实际最终变量,意味着我们可以在变量声明前面加上final关键字,而不用修改其余代码。它实际上就是final的,我们只是懒得说而已

  • 对于下面的示例,i 增加 并没有报错

    public class Closure2 {
        int i;
        IntSupplier makeFun(int x) {
            return () -> x + i++;
        }
    }
    

    请注意,上面的规则并不是像说 “任何在Lambda 表达式之外定义的变量都必须是最终变量或实际最终变量” 那么简单。相反,我们必须从被捕获的变量是 “实际上的最终变量” 这个角度考虑。如果它是某个对象中的一个字段,那么它会有独立的生命周期,并不需要任何特殊的捕获,以便在之后这个Lambda 表达式被调用时,变量仍然存在。

函数组合

  • 函数组合:将多个函数结合使用,以创建新的函数。

  • java.util.function 中的一些接口也包含了支持函数组合的方法

    组合方法说明
    andThen(argument)先执行原始操作,再执行参数操作
    compose(argument)先执行参数操作,再执行原始操作
    and(argument)对原始谓词和参数谓词执行短语逻辑与(AND)计算
    or(argument)对原始谓词和参数谓词执行短语逻辑或(OR)计算
    negate()所得谓词为该谓词的逻辑取反
    • 谓词:java.util.function 中的一条命名规则,如果接口接受一个参数并返回boolean,则会被命名为Predicate (谓词)
  • 示例

    public class FunctionComposition {
        static Function<String, String> f1 = s -> {
            System.out.println(s);
            return s.replace('A', '_');
        },
        f2 = s -> s.substring(3),
        f3 = s -> s.toLowerCase(),
        f4 = f1.compose(f2).andThen(f3);
    
        public static void main(String[] args) {
            System.out.println(f4.apply("GO AFTER ALL AMBULANCES"));
        }
    }
    
    public class PredicateComposition {
        static Predicate<String>
            p1 = s -> s.contains("bar"),
            p2 = s -> s.length() < 5,
            p3 = s -> s.contains("foo"),
            p4 = p1.negate().and(p2).or(p3);
    
        public static void main(String[] args) {
            Stream.of("bar", "foobar", "foobaz", "fongopuckey").filter(p4).forEach(System.out::println);
        }
    }
    

柯里化和部分求值

  • 柯里化:将一个接受多个参数的函数转变为一系列只接受一个参数的函数

  • 示例

    // 柯里化:将一个接受多个参数的函数转变成一系列只接受一个参数的函数
    public class Curring {
        // 未柯里化
        static String uncurring(String a, String b) {
            return a + b;
        }
    
        public static void main(String[] args) {
            // 柯里化函数
            Function<String, Function<String, String>> sum = a -> b -> a + b;
    
            System.out.println(uncurring("Hi, ","Ha"));     // 输出:Hi, Ha
    
            Function<String, String> hi = sum.apply("Hi ");
            System.out.println(hi.apply("Ho"));     // 输出:Hi Ho
    
            // 部分应用
            Function<String, String> sumHi = sum.apply("Hup ");
            System.out.println(sumHi.apply("Ho"));      // 输出: Hup Ho
            System.out.println(sumHi.apply("Hey"));     // 输出: Hup Hey
    
            // 接受三个参数的函数进行柯里化
            Function<String,
                    Function<String,
                            Function<String, String>>> three = a -> b -> c -> a + b + c;
    
            Function<String,
                    Function<String, String>> two = three.apply("Hi, ");
            Function<String, String> one = two.apply("Ho, ");
            System.out.println(one.apply("Hup"));       // 输出: Hi, Ho, Hup
        }
    }
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值