Skip to content

《On Java8》 13. 函数式编程 #8

@funnycoding

Description

@funnycoding

第十三章 函数式编程

函数式编程语言操纵代码片段就像操作数据一样容易。虽然 Java 不是函数式语言,但是 Java8 Lambda 表达式和 方法引用(Method Reference) 允许你以函数式编程

【Java8 的最主要特性之一,如果本书写的不够详细的话可以看《实战Java8》那本书,讲的非常细,例子也多】

【关于函数式编程,我的理解是将行为传入方法中,让方法展示动态性】

OO(object oriented 面向对象)是抽象数据
FP(functional programming,函数式编程) 是抽象行为

纯粹的函数式语言在安全性上更强,它们增加了强制的额外约束所有数据必须是不可变的设置一次,永不改变。将值传递给函数,该函数生成新的值,但是不修改自身外部的任何数据(包括其参数或该函数范围之外的元素)。

当强制执行此操作时,你就能知道错误在函数体内,因为该函数仅创建并返回结果,而不是其他任何错误。【因为该函数不能修改外部变量

不可变对象无副作用范式解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。

这是可变共享状态的问题。这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。

纯函数语言:函数永远不会修改现有值,只生成新值,不会对内存产生争用。

因此,经常提出纯函数语言作为并行编程的解决方案。(同时还有其他可行的解决方案)

函数式语言的背后有很多动机,包括:

  • 为并行编程【提高效率】
  • 增强代码可靠性【增加健壮性】
  • 代码创建和库的复用 【增加复用性】

关于函数式编程能高效创建更健壮的代码 **这一观点仍然存在争议。**虽然已有一些好的范例,但是还不足以证明纯函数语言就是解决编程问题的最佳方法。

FP 思想值得融入非 FP语言,比如 Python,以及Java 8中的特性。

【作者在前言中主要介绍了FP思想的简单定义,以及其优点和解决的痛点,同时作为 Java8 的主要特性,下面将会详细描述 函数式编程】

新旧对比

通常,方法根据入参不同,返回的结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?

结论是:**只要能够将代码传递给方法,我们就可以控制方法的行为。**此前我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。

下面将分别展示 传统形式 和 Java 8的方法引用,Lambda 表达式来完成这个操作

// functional/Strategy.java
interface Strategy {
    String approach(String msg);
}

class Soft implements Strategy {
    @Override
    public String approach(String msg) {
        return msg.toLowerCase() + "?";
    }
}

// 该类的方法签名与返回值与接口相同的方法,但是并没有实现接口,方法名称也不同
class Unrelated {
    static String twice(String msg) {
        return msg + " " + msg;
    }
}

public class Strategize {
    // 持有接口引用 用来指向其实现类
    Strategy strategy;
    String msg;

    // 初始化默认策略
    public Strategize(String msg) {
        strategy = new Soft(); // 默认策略是 Soft 对 Strategy 接口的是实现
        this.msg = msg;
    }

    // 打印 approach() 函数的输出
    void communicate() {
        System.out.println(strategy.approach(msg));
    }

    // 给策略引用重新赋值对象,实现策略的切换
    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public static void main(String[] args) {
        // 初始化一个元素是策略对象的数组,第一个元素是一个实现 Strategy 接口的匿名内部类
        // 第二个元素是 使用 Lambda 语法实现的匿名内部类
        // 第三个是 使用方法引用,将 Unrelated 类的 twice 方法引用赋值给 接口 因为 twice 方法的方法签名与返回值与 接口定义的 approach 方法相同
        Strategy[] strategies = {
                new Strategy() {
                    @Override
                    public String approach(String msg) {
                        return msg.toUpperCase();
                    }

                    @Override
                    public String toString() {
                        return "匿名对象策略";
                    }
                }, // 第一个元素: 实现了 Strategy 接口的匿名内部类
                msg -> msg.substring(0, 5), // 第二种 Lambda表达式,入参是 msg ,对入参的处理是截取 [0,5] 的一个子串
                // 第三种 方法引用,将 Unrelated 的方法引用 赋值给 Strategy 接口,只要方法的返回值与方法签名与接口中的抽象方法一直,就可以进行方法引用的赋值,实现行为的绑定
                Unrelated::twice,

        };

        Strategize s = new Strategize("Hello there");
        // 输出默认Strategy -> Soft 中的实现
        s.communicate();
        // 遍历策略数组,分别打印不同策略实现的 approach 方法
        for (Strategy newStrategy : strategies) {
            System.out.println("开始更改策略对象,当前策略对象" + newStrategy);
            s.changeStrategy(newStrategy);
            s.communicate();
        }
    }
}

Strategy 接口提供了单一行为 approach() 方法来承载函数式功能。通过创建不同的 Strategy 对象我们可以创建不同的行为。

最传统的实现方法:我们创建一个单独的类来实现接口的功能比如 Soft类

[1] 在 Strategize 中,Soft 作为默认策略,在构造函数中进行赋值。

[2]一种略显简单且更自发的方法是 匿名内部类 ,但是代码还是显得比较冗长

[3] Java 8 的 Lambda 表达式 由 箭头 “->” 分隔参数和函数体,箭头左边参数,右边是从 Lambda 返回的表达式,即函数体。 这实现了与定义类、匿名内部类相同的效果,但代码少的多。

[4] Java 8 的方法引用, 由 “::” 区分,在 :: 左边的是类或对象的名称右边的是方法返回,但是没有参数列表。

[5] 在使用默认 Soft Strategy 之后,我们逐步遍历数组中的所有 Strategy,并使用 cahngeStrategy() 方法将 每个 Strategy 赋值给 s

[6] 现在,每次调用 communicate() 都会产生不同的行为,具体取决于此刻正在使用的策略对象,我们传递的是行为,并非仅是数据。

个人小结:

  • Lambda 和 方法引用是 对匿名内部类的语法层面的优化,打印对象的话你会发现本质在 JVM 层生成的还是类但是语言层面你可以认为这两种语法生成的都是方法,代表着一种动作
  • 虽然这2个新特性可以写出更简化的代码,但是有利必有弊,如果你需要覆盖 Object 的类的方法(比如 toString() )那么就无法使用这两种语法
  • 这种语法的优化可以在 idea 中直接由编译器推荐优化,所以如果你习惯写 匿名内部类那也没啥,我觉得匿名内部类虽然看着冗长,但是可读性还是要比 lambda 和 方法引用要强,可能是因为自己没看习惯。

Lambda表达式

Lambda 表达式是使用 最小可能 语法编写的函数定义:

  1. Lambda 表达式产生 函数,而不是类。在 JVM 上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数——但作为程序员,你可以高兴地假装它们只是函数。 【也就是将 Lambda 看作行为,一种动词】
  2. Lambda 的语法尽可能的少,为了易于编写和使用,不用写冗长的内部类了。

[1]【这句话的意思是,Lambda的本质还是类,刚才截图也可以看到,其本质是个类。但是我们可以屏蔽掉其 JVM 层的实现,在语法层认为 Lambda 只是一个函数/方法,只代表行为。】

我们在 Strategy.java 中看到了一个 Lambda 表达式,但是还有其他语法变体

// LambdaExpressions.java
// 定义了3个接口,其中分别有 无参函数,一个参数的函数,两个参数的函数
// 分别对应不同的 Lambda 示例
interface Description {
    String brief();
}

interface Body {
    String detailed(String head);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {

    // 两个单参数的 Lambda 表达式,对应接口Body。 单个参数的表达式 入参可以括起来也可以不括
    static Body bod = h -> h + " No Parens!";
    static Body bod2 = (h) -> h + "More Details";

    // 无参 Lambda 表达式 对应接口 Description
    static Description desc = () -> "Short info";
    // 无参 Lambda 表达式,多行方法体的时候需要用花括号括起来
    static Description moreLines = () -> {
        System.out.println("More Lines");
        return "from MoreLines";
    };

    // 双参数接口 Lambda 表达式 对应接口 Multi
    static Multi multi = (h, n) -> h + n;


    public static void main(String[] args) {
        System.out.println(bod.detailed("Oh!"));
        System.out.println(bod2.detailed("Hi!"));
        System.out.println(desc.brief());
        System.out.println(multi.twoArg("Pi!", 3.1415926));
        System.out.println(moreLines.brief());
    }
}

从三个接口开始分析代码,每个接口都一个单独的方法(很快就会理解重要性),每个方法都有不同数量的参数,用来演示 Lambda 语法。

任何 Lambda 表达式的基本语法是:

  1. 左边是入参
  2. 接着 -> 可以看做是 产出
  3. -> 之后的都是方法体
    1. 当只用一个参数时,可以不需要括号 ()
    2. 正常情况使用 括号() 包裹参数,为了一致性,也可以包裹单个参数,虽然这种情况并不常见
    3. 如果没有参数,则必须使用括号 () 代表空参数列表
    4. 对于多个参数,将参数列表放在括号 () 中

目前为止,所有 Lambda 表达式方法体都是单行。该表达式的结果自动成为 Lambda 表达式的返回值在此处使用 return 关键字是非法的。这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。

如果需要多行,则需要使用 {将方法体括起来,同时使用 return 关键字。}

Lambda 表达式通常比匿名内部类更容易阅读,下面的代码示例将尽可能使用这种形式

小结:

本章介绍了Lambda 的基本语法,给了两个简单示例,可以看到 Lambda 的语法总体来说还是挺简单的。

递归

递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但是要注意,递归方法必须是 实例变量静态变量。 否则会出现编译时错误

下面的两个例子都需要接受一个 int 型入参并生成 int 的接口

// functional/IntCall.java
interface IntCall {
  int call(int arg);
}


public class RecursiveFactorial {
    // 声明一个刚才创建的接口的引用
    static IntCall fact;

    public static void main(String[] args) {
        // Lambda 中嵌套了递归
        fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
        
        for (int i = 0; i < 10; i++) {
            System.out.println(fact.call(i));
        }
    }
}
// 输出

1
1
2
6
24
120
720
5040
40320
362880

这里 fact 是一个静态变量。注意三元操作符,递归函数将一直调用自己,直到i == 0。

所有递归函数都必须存在终止条件,否则将成为无限递归。

我们可以将 斐波那契数列 改为使用 递归 Lambda 表达式来实现,这次使用实例变量演示:

// RecursiveFibonacci.java
public class RecursiveFibonacci {
    // 入参是int,出参也是int的函数式接口。
    IntCall fib;

    // 类的构造函数,使用 Lambda 表达式实现 IntCall 的call()函数
    public RecursiveFibonacci() {
     /*   fib = n -> n == 0 ? 0 :
                n == 1 ? 1 :
                        fib.call(n - 1) + fib.call(n - 2);*/

        // if - else 版 斐波那契数列生成函数
        fib = n -> {
            if (n == 0) {
                return 0;
            } else if (n == 1) {
                return 1;
            } else {
                return fib.call(n - 1) + fib.call(n - 2);
            }
        };

    }

    // 调用生成斐波那契数列的函数 ,这层封装只是为了更强的语义性,直接调用 fib.call 也是一样的效果
    int generatorFibonacci(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.generatorFibonacci(i));
            //System.out.println(rf.fib.call(i)); // 效果和 rf.generatorFibonacci(i) 一样
        }
    }
}



/**
输出
0
1
1
2
3
5
8
13
21
34
55
*/

这里 使用 Lambda 实现的递归函数将一直调用自己,直到 i == 0。

所以递归函数都有结束条件,否则会形成无限递归的死循环。

小结:

本章使用算是 Lambda 的更具体的使用场景,实现了一个 Lambda 版本的 斐波那契数列算法的函数。

  • 递归的基本知识
    • 递归需要有结束条件
    • Lambda 递归必须使用实例变量或者 静态变量

方法引用

Java 8 中的方法引用没有历史包袱。 方法引用组成:类名或对象名,后面跟 [::] 然后跟 方法名称。

// MethodReferences.java
// 一个方法引用的简单说明例子
interface Callable { // [1] 函数式接口,入参 String,没有返回值
    void call(String s);
}

class Describe {
    void show(String msg) { // [2]
        System.out.println(msg);
    }
}

public class MethodReferences {
    static void hello(String name) { // [3]
        System.out.println("Hello, " + name);
    }

    // 静态内部类/嵌套类 1
    static class Description {
        String about;

        public Description(String about) {
            this.about = about;
        }

        void help(String msg) { //[4]
            System.out.println(about + " " + msg);
        }
    }

    // 静态内部类/嵌套类 2
    static class Helper {
        static void assist(String msg) { // [5]
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        Describe d = new Describe();

        //方法引用语法
        // Describe 的 show() 方法与 Callable 接口的 call() 方法的 方法签名与返回值都一致
        // 这里赋值是先构造的 Describe 对象,然后使用实例::方法名 的形式将方法引用赋值给 Callable 接口
        Callable c = d::show; //[6]
        c.call("call"); // [7]  所以这里 Callable 引用 c  调用的 call() 实际调用的是 Describe De show()

        // 同上, MethodReferences 的 hello() 方法与 Call()方法也一致
        // 这里静态方法的引用赋值就不需要构造方法所在对象的实例,可以直接 类名::方法名 进行赋值
        c = MethodReferences::hello; //[8]
        c.call("Bob");

        // 这里是将静态内部类 Descripetion 的 非静态方法 hel() 的引用 赋值给 Callable 是[6] 赋值的一步完成版本,构造类实例—方法引用赋值
        c = new Description("valueable")::help; // [9]
        c.call("information");

        c = Helper::assist; // [10]  静态方法的引用赋值不需要实例对象
        c.call("Help!");
    }
}

【第一遍真没太弄明白,又看了两编大概懂了,直接看原书的讲解吧】

[1] 从单一方法接口开始【这种接口被称为函数式接口】

[2] show() 签名 (参数类型和返回类型) 符合 Callable 的 call() 签名。

【方法引用的赋值是根据方法的返回值与方法签名是否与函数式接口中的方法一致,如果一致就可以进行方法赋值】

[3] hello() 也符合 call() 签名。

[4] help() 也符合,它是静态内部类中的非静态方法

[5] assist() 是静态内部类中的 静态方法。

[6] 我们将 Describe 对象的方法因复制给 Callable,这个接口它没有 show() 方法,而是 call() 方法,然而Java 接受了这个奇怪的赋值,因为Describe的 show(String msg )方法引用符合 Callable 的 call(String s) 方法的签名。

【奇怪的赋值 这个描述挺贴切,这玩意看的我一脸懵逼,按方法引用这个说法,只要是单函数接口且方法签名与返回类型一致,就可以这么玩?】

[7] 我们可以通过调用 call() 来调用 show() ,因为 Java 将 call() 映射到 show() 。

这个真的很神奇,代码运行出来的时候看到这我就懵了】

[8] 这是一个 静态方法引用。【可以看到静态方法引用的赋值不需要构造类的实例对象】

[9] 这是6的另一个版本:对已经实例化对象的方法的引用,有时称为:绑定方法引用。

[10] 最后,获取静态内部类的方法引用的操作与 [8] 中外部类的方式一致。

上面例子是简短介绍,很快就能看到方法引用的全部变化。

个人小结:

【方法签名引用的赋值只要目标方法与函数式接口中的方法返回值与签名一致就可以进行赋值,非常的方便。

不需要实现接口,也不需要方法名称与接口中的一致,这样就可以更好的对方法进行命名。】

Runnable接口

Runnable 接口自 1.0 版本一直在 Java中,因此不需要导入。

它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。

因此,我们可以使用 Lambda 表达式和 方法引用来实现 Runnable:

【方法引用和Runnable接口的结合使用】

// RunnableMethodReference.java
// 一个使用匿名内部了/Lambda/方法引用 三种形式 生成实现 Runnable 实例的例子
class Go {
    static void go() {
        System.out.println("Method Reference 实现接口");
    }
}

public class RunnableMethodReference {
    public static void main(String[] args) {

        // 匿名内部类实现 Runnable 接口
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类");
            }
        }).start();

        // Lambda
        new Thread(
                () -> System.out.println("Lambda 实现 Runnable")
        ).start();

        // Method Reference
        new Thread(Go::go).start();
    }
}

【如果实现了一个以上方法的匿名内部类,比如我重写了 toString() ,那么就无法使用 Lambda 或者方法引用】

【本来有点懵逼,现在明白多了,实际上就是三种不同的语法,但是底层JVM生成的类是一致的,简化了书写而已。】

未绑定的方法引用

未绑定的方法引用:指没有关联对象的普通方法,也就是非静态方法

使用未绑定引用之前,必须先有对象才能调用。

也就是如果想怼非静态方法实现方法引用的绑定必须先构造对应类的实例。

// UnboundMethodReference.java
class X {
    String f() {
        return "X::f()";
    }
}

interface MakeString {
    String make();
}

interface TransformX {
    // 这里方法的参数是一个 X 类型的对象,后面就可以看到这个入参的作用。
    String transform(X x);
}

public class UnboundMethodReference {
    public static void main(String[] args) {
       // MakeString ms = X::f; // 编译器报错,该方法不是静态方法,不能直接使用 类名::方法进行绑定
        MakeString ms = new X()::f; // 针对上面代码的修改。非静态方法引用的绑定,先构造对象,然后将方法引用赋值给接口

        // 这里的方法引用赋值如果是第一次的话会很懵逼
        // 方法签名既不一致,而且 f() 也不是静态方法,为什么可以使用 类::方法的形式
        // 这里因为接口的方法的第一个参数是对应类,所以就存在了一个隐式 this 的赋值过程,方法绑定的时候会将对象赋值给第一个指定对象的类型参数,实现与构造对象相同的效果
        TransformX sp = X::f;

        X x = new X();
        // 这三个语句效果一样,都是调用 X 类的 f() 函数
        System.out.println(sp.transform(x));
        System.out.println(x.f()); 
        System.out.println(ms.make()); 
    }
}

写完这个代码我又迷茫了... 这方法签名不一样咋还用上方法引用了呢,而且看报错信息,这里面说非静态方法不能从静态上下文中被引用,除了 main() 是静态的,其他都不存在静态,那么问题就出在 main() 方法里了。

还是先看看书上怎么说把:】

目前我们已经明白了与接口方法签名一致的方法引用的赋值,在[1]中,会尝试将 X 的 f() 方法,赋值给 MakeingString ,虽然这俩方法签名一致,但是编译器仍然告诉你 invalid method reference(无效的方法引用)

因为这里还有一个隐藏参数 this你不能在没有 X 对象的前提下调用 f() ,因此 X::f 表示未绑定的方法的引用,因为它尚未绑定到对象。

要解决这个问题,我们需要一个 X 对象,所以我们的接口实际上需要一个额外的 参数的接口,如上例中的 TransformX。 如果将 X::f 赋值给 TransfromX,这在 Java中是允许的。

使用未绑定的引用时,函数方法的签名(接口中的单方法)不再与方法引用的签名完全匹配。理由是:你需要一个对象来调用方法。

[2] 的结果有点像脑筋急转弯【确实】,我接受未绑定的引用并对其调用 transform() ,将其传递给 X,并以某种方式导致对 x.f() 的调用。

Java 知道它必须采用第一个参数,实际上就是 this,并在其上调用方法。

【总结:如果是非静态方法,实现类名::方法名这种绑定形式的话,需要接口中第一个参数为对应类的类型参数方便,作为隐式 this 的赋值,因为非静态方法必须有一个对象来调用】

// 未绑定方法与多参数接口的结合
// MultiUnbound.java 这里我把接口 和 This类的顺序调换了一下,我感觉更好理解
// 这里接口的第一个入参都是 This 类型,因为未绑定方法的引用赋值需要隐式this来调用

// 这三个接口的第一个参数都是 类 This,用处是 类This将非静态方法绑定到接口的时候不需要构造对象。
interface TwoArgs {
    void call2(This athisRef, int i, double d);
}

interface ThreeArgs {
    void call3(This athis, int i, double d, String s);
}

interface FourArgs {
    void call4(
            This athis, int i, double d, String s, char c);
}

// 分别对应接口的 2,3,4 入参的函数

class This {
    void two(int i, double d) {

    }

    void three(int i, double d, String s) {

    }

    void four(int i, double d, String s, char c) {

    }
}


public class MultiUnbound {
    public static void main(String[] args) {
        // 方法绑定 这里可以看到 非静态方法直接使用 类名::方法名进行了绑定
        TwoArgs twoArgs = This::two;
        ThreeArgs threeArgs = This::three;
        FourArgs fourArgs = This::four;
        // 真正调用之前还是需要构造对应类的实例,将该实例传入接口中,实现对象的赋值,然后调用刚才绑定的方法
        This aThis = new This();
        twoArgs.call2(aThis, 11, 3.14);
        threeArgs.call3(aThis, 11, 3.14, "Three");
        fourArgs.call4(aThis, 11, 3.14, "Four", 'Z');
    }
}

构造函数引用

还可以捕获构造函数的引用,然后通过引用调用该构造函数

【这个操作想想就有点意思啊。】

//CtorReference.java
// 三个构造函数对应上面的三个接口
class Dog {
    String name;
    int age = -1;


    public Dog() {
        name = "default Dog Name";
    }

    public Dog(String name) {
        this.name = name;
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", Dog.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("age=" + age)
                .toString();
    }
}

// 三个接口都是构造 Dog 实例的方法 对应 dog 的三个构造函数
interface MakeNoArgs {
    Dog make();
}

interface Make1Args {
    Dog make(String name);
}

interface Make2Args {
    Dog make(String nm, int age);
}


public class CtorReference {
    public static void main(String[] args) {
        // 分别将三个构造器的方法引用赋给了3个对象的接口
        // 这里我还观察到了一个现象,就是构造函数的赋值形式和静态方法是一样的
        // 书里一直有个观念 构造函数是隐式静态方法,我之前一直存怀疑态度,但是在这里
        // 至少语法上是一致的
        MakeNoArgs mna = Dog::new; //[1]
        Make1Args m1a = Dog::new; //[2]
        Make2Args m2a = Dog::new; //[2]

        Dog dn = mna.make();
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
        System.out.println(dn);
        System.out.println(d1);
        System.out.println(d2);
    }
}

Dog 有三个构造函数,有三个对应构造函数方法签名的接口(make() 方法的名称可以不同)

注意对 [1] [2] [3] 的引用,每个都使用了 Dog::new ,每种情况下都赋值给了不同的接口。

编译器可以检测并知道这是调用哪个构造函数

编译器能识别并调用你的构造函数 (本例中是 make() 改成别的名字也完全没有问题)

总结:

【构造函数的方法赋值和静态函数一样,更多详细的在代码注释中有说明。】


【从这里可以认为是一个小的分割了,因为上面都是最基本的语法的介绍,下面的内容开始设计 JDK 自带的函数式接口,也就是配合 Lambda 和 方法引用使用的 Java8 新加的内置接口,所以有的例子会比上面的复杂很多。】

总结:

这一部分讲的都是Lambda 和 方法引用的基础知识,但是也很重要,如果看着不习惯的话,就多敲几遍例子也就理解了。

layout: post
title: 《On Java8》第十三章——函数式编程(2) java.util.function 的内置函数式接口
date: 2020-02-18 01:26:52
tags:

  • 读书笔记
    categories:
  • 《On Java8》

构造函数引用

还可以捕获构造函数的引用,然后通过引用调用该构造函数

【这个操作想想就有点意思啊。】

//CtorReference.java
// 三个构造函数对应上面的三个接口
class Dog {
    String name;
    int age = -1;


    public Dog() {
        name = "default Dog Name";
    }

    public Dog(String name) {
        this.name = name;
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", Dog.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("age=" + age)
                .toString();
    }
}

// 三个接口都是构造 Dog 实例的方法 对应 dog 的三个构造函数
interface MakeNoArgs {
    Dog make();
}

interface Make1Args {
    Dog make(String name);
}

interface Make2Args {
    Dog make(String nm, int age);
}


public class CtorReference {
    public static void main(String[] args) {
        // 分别将三个构造器的方法引用赋给了3个对象的接口
        // 这里我还观察到了一个现象,就是构造函数的赋值形式和静态方法是一样的
        // 书里一直有个观念 构造函数是隐式静态方法,我之前一直存怀疑态度,但是在这里
        // 至少语法上是一致的
        MakeNoArgs mna = Dog::new; //[1]
        Make1Args m1a = Dog::new; //[2]
        Make2Args m2a = Dog::new; //[2]

        Dog dn = mna.make();
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
        System.out.println(dn);
        System.out.println(d1);
        System.out.println(d2);
    }
}

Dog 有三个构造函数,有三个对应构造函数方法签名的接口(make() 方法的名称可以不同)

注意对 [1] [2] [3] 的引用,每个都使用了 Dog::new ,每种情况下都赋值给了不同的接口。

编译器可以检测并知道这是调用哪个构造函数

编译器能识别并调用你的构造函数 (本例中是 make() 改成别的名字也完全没有问题)

总结:

【构造函数的方法赋值和静态函数一样,更多详细的在代码注释中有说明。】


【从这里可以认为是一个小的分割了,因为上面都是最基本的语法的介绍,下面的内容开始设计 JDK 自带的函数式接口,也就是配合 Lambda 和 方法引用使用的 Java8 新加的内置接口,所以有的例子会比上面的复杂很多。】

函数式接口

方法引用 和 Lambda 表达式必须被赋值,同时编译器需要识别类型信息以确保类型正确。 Lambda表达式特别引入了新的要求:

x -> x.toString();	

这里清楚的看到 返回值 是String类型,那么 x 入参是什么类型呢?

【之前我就在嘀咕这个问题了】

Lambda 表达式包含 类型推导(编译器自动推导出类型声明,避免了需要开发者显示声明) 编译器必须能够以某种方式推导出 x 的类型

下面是第二个代码示例:

(x,y) -> x + y;	

这里 x,y 可以是任何 支持 + 运算符连接的数据类型。

可以是两个数值或者一个 String 加一个可以被转换为 String 的数据类型

但是,当 Lambda 表达式被赋值时,编译器必须确定 x 和 y 的确切类型以生成正确的代码。

【lambda 表达式的类型推导在编译时由编译器完成,必须能够确切推导出参数的类型信息】

该问题也适用于方法引用,假设要传递 System.out :: prinln 到你正在编写的方法,你怎么知道传递给方法的参数的类型

为了解决这个问题, Java 8 引入了 **java.util.function** ,包含了一组函数式接口每个接口只包含一个抽象方法,称为函数式方法。

【那么问题来了,这个类型推导与集合中的泛型菱形语法一样吗?】

【这里我找到了另外的资料解答了我的疑惑,并且说的挺详细挺好,跟我有意义疑惑的同学可以拉倒最后的额外资料去看那篇文章】

【也就是专门为了 Lambda 和 方法引用添加的接口呗】

在编写接口时, 可以使用 @FunctionalInterface 注解强制执行 函数式方法模式

// FunctionalAnnotation.java
// 加了这个注解接口就只能存在1个抽象方法
@FunctionalInterface
interface Functional {
    String goodbye(String arg);
}

// 不加注解也没事,注解只是一个强制校验 当抽象方法的数量超过一个的时候会报错,而不加注解则该接口就成为非函数式接口
interface FunctionalNoAnn {
    String goodbye(String arg);
}

/*
以下是一个使用 @FunctionalInterface注解标示为函数式接口但是存在多个抽象方法的例子,编译器会报错。

@FunctionalInterface
interface NotFunctional {
  String goodbye(String arg);
  String hello(String arg);
}
产生错误信息:
NotFunctional is not a functional interface
multiple non-overriding abstract methods
found in interface NotFunctional
 因为如果使用了 @FunctionalInterface 注解,那么接口中只能存在一个抽象函数
*/


public class FunctionalAnnotation {
    // 与接口中返回值与签名一样的未绑定方法
    public String classGoodBye(String arg) {
        return "Good bye" + arg;
    }

    public static void main(String[] args) {
        FunctionalAnnotation fa = new FunctionalAnnotation();
        // 方法引用赋值
        Functional f = fa::classGoodBye;
        FunctionalNoAnn fna = fa::classGoodBye;
        // 对象的实例赋值给接口的引用,因为没有实现
        //Functional fac = fa; // 编译器报错

        // 使用 Lambda形式实现 Function接口的函数 这里编译器会自动推导出返回值为 String类型
        Functional fl = a -> "GoodBye Lambda," + a;
        Functional fnal = a -> "GoodBye Lambda With No Annotation," + a;
    }
}

函数式接口标记注解 @FunctionaInterface可选的,如果接口中的抽象函数大于一个时编译器会报错。

定义 f 和 fna 时, Functional 和 FunctionaNoAnn 定义的是接口,然而被赋值的是方法 classGoodbye()。

首先这只是一个方法,而不是类;其次,它甚至不是事先了该接口的类中的方法。

Java 8 在这里添加了一点小魔法:如果 将方法引用Lambda 表达式赋值给函数式接口(类型需要匹配)Java 会将你的赋值与目标接口进行适配,编译器会自动包装方法引用 或 Lambda 表达式到实现目标接口的类的实例。

尽管 FunctionalAnnotation 确实适合 Functional 模型,因为方法签名和返回值都可以匹配上,但是 Java 不允许我们将 FunctionalAnnotation 像 fac 那样直接赋值给 Functional ,因为该类没有明确地实现 Functional 接口。

令人惊奇的是, Java 8 允许我们以简便的语法为接口赋值函数。

【这个例子其实挺简单的,就是说明被@FunctionalInterface 修饰的接口只能存在一个抽象函数,否则编译器会报错。至于上面说的 方法赋值给接口,之前已经生成了对应类的实例,所以是正常的。】

java.util.function 包的目的是创建一组完整的目标接口。让我们在一般情况下不需要自定义自己的接口。

这主要是因为基本类型会产生一小部分接口。如果你了解命名模式,就可以见名知意,知道特定接口的作用。

下面是基本命名准则:

  1. 如果只是处理对象而非基本类

    ,名称为 Function,Consumer,Predicate等。参数类型通过泛型添加。

  2. 如果接受的参数是基本类型,则由名称的第一部分表示。如 LongConsumer,DoubleFunction,IntPredicate等,但基本 Supplier 类型除外。

  3. 如果返回值是基本类型,则用 To 表示, 如 ToLongFunction《T》

  4. 如果返回值与参数类型一致,则是一个运算符:单个参数使用 UnaryOperator ,两个参数使用 BinaryOperator

  5. 如果接受两个参数且返回值为布尔值,则是一个谓词(Predicate)

  6. 如果接收的两个参数类型不同,则名称中有一个 Bi。

【总结,函数式接口是为了更好地见名知意,让不同的接口有不同的用途,其实方法赋值的话很多都是可以通用的。】

下表描述了 java.util.function 中的目标类型(包括例外情况)

【可以看到,很多都是数字来回操作,不太明白这个意义和具体用处以及例子,这块需要自己去多找找了】

此表提供了常规方案,通过上表你应该能或多或少自行推导出更多行的函数式接口

【我有点懵逼,推导不出来啊】

Update:只要定义一个入参与返回值与函数接口中方法一致的就可以定义新的通用的函数式接口,标准非常宽松

可以看出,创建 java.util.function 时,设计者们做了选择:

例如 为什么没有 IntComparatorLongComparator,和DoublerComparator呢?

BooleanSupplier 却没有其他表示 Boolean 的接口。

有通用的 BiConsumer 却没有 用于 int,long,和double 的 BiConsumer 的变体

【至于这些选择是怎样考虑的,作者没说,我更想知道选择背后的决策】

你还可以看到基本类型给 Java 添加了很多复杂性,为了缓和效率问题,该语言的第一版就包含了基本类型。现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。

【这里关于Java8新增的内置函数的具体使用建议看最底下的额外参考资料,有更详细的说明,因为这块也是我昨天学习的时候比较疑惑的点,就是这些内置函数接口我到底要怎么在实际开发中去使用

下面列举了 Lambda 表达式所有的 Function变体的示例:

// FunctionVariants.java
// 一个使用内置函数式接口进行各种基础操作的例子
class Foo {
}

class Bar {
    // 持有一个Foo对象引用
    Foo f;

    public Bar(Foo f) {
        this.f = f;
    }
}

class IBaz {
    int i;
    public IBaz(int i) {
        this.i = i;
    }
}

class LBaz {
    long l;

    public LBaz(long l) {
        this.l = l;
    }
}

class DBaz {
    double d;

    public DBaz(double d) {
        this.d = d;
    }
}


public class FunctionVariants {
    // 使用内置接口,这里 Bar/ IBaz / LBaz / DBaz 与对应接口的函数方法签名和返回值一致
    static Function<Foo, Bar> f1 = f -> new Bar(f); // 传入一个 f 返回一个 Bar 符合 Function 的 apply() 函数
    static IntFunction<IBaz> f2 = i -> new IBaz(i); // 返回和入参相同类型的方法 apply()
    static LongFunction<LBaz> f3 = l -> new LBaz(l);
    static DoubleFunction<DBaz> f4 = d -> new DBaz(d);

    static ToIntFunction<IBaz> f5 = ib -> ib.i; // 返回 int
    static ToLongFunction<LBaz> f6 = lb -> lb.l; // 返回 long
    static ToDoubleFunction<DBaz> f7 = db -> db.d; // 返回 double

    static IntToLongFunction f8 = i -> i; // 入参 int 出参 long 入参类型比出参小,不用进行数据类型强转
    static IntToDoubleFunction f9 = i -> i;  // 入参 int 出参 double 入参类型比出参小,不用进行数据类型强转
    static LongToIntFunction f10 = l -> (int) l; // Long 转 int ,因为出参类型比入参小,需要在方法中进行强转
    static LongToDoubleFunction f11 = l -> l;  // Long 转 double ,不用进行数据类型强转
    static DoubleToIntFunction f12 = d -> (int) d; // double 转 int ,向下转换,需要强转
    static DoubleToLongFunction f13 = d -> (long) d; // double 转 long,强转

    public static void main(String[] args) {
        // Function 的 apply() 函数就是 根据入参生成出参两种类型变量。 这里就是根据 Foo对象构造 Bar对象
        Bar b = f1.apply(new Foo());
        // 对象的构建,根据对应的入参
        IBaz ib = f2.apply(11);
        LBaz lb = f3.apply(11);
        DBaz dBaz = f4.apply(11);

        // 获取 对象中的 int
        int i = f5.applyAsInt(ib);
        // 获取对象中的 long
        long l = f6.applyAsLong(lb);
        // 获取对象中的 double
        double d = f7.applyAsDouble(dBaz);

        // int 转 long
        l = f8.applyAsLong(12);
        // int 转 double
        d = f9.applyAsDouble(12);
        // long 转 int
        i = f10.applyAsInt(12);

        // long 转 double
        d = f11.applyAsDouble(12);
        // double 转 int
        i = f12.applyAsInt(13.0);
        // double 转 long
        l = f13.applyAsLong(13.0);
    }
}

这些 Lambda 表达式尝试生成适合函数签名的最简代码。

在某些情况下有必要进行强制类型转换,否则编译器会报错。

主方法中的每个测试都显示了 Function 接口中不同类型的 apply() 方法。每个都产生一个与其关联的 Lambda 表达式的调用

【实际上这个例子就是内置函数接口的一个使用样例,就是看着有点繁琐】

方法调用有自己的小魔法:

// MethodConversion.java
class In1 {}
class In2 {}


public class MethodConversion {
    static void accept(In1 i1, In2 in2) {
        System.out.println("accept()");
    }

    static void someOtherName(In1 i1, In2 in2) {
        System.out.println("Some Other Name");
    }

    public static void main(String[] args) {
        // 去看看 BiConsumer 的源码就会发现,这是一个 有2个类型参数的泛型类
        // 接口抽象函数 accept() 入参是 2个类型参数的类 这里对应 静态方法 accept() 和 someOtherName()
        BiConsumer<In1, In2> bic;
        // 调用 accept
        bic = MethodConversion::accept;
        bic.accept(new In1(), new In2());
        
        // 调用 someOtherName()
        bic  = MethodConversion::someOtherName;
        bic.accept(new In1(), new In2());
    }
}

/**
输出,可以看到对应的 accept 和 someOtherName都被调用了,因为分别进行了方法绑定,绑定到 BiConsumer的 accpt() 函数上
accept()
someOtherName()
*/

【第一次的时候确实很懵逼,隔天再看其实就很清晰了,就是对应的内置函数的使用】

这里可以看到,只要参类型、返回类型与接口中的抽象方法能对应,方法名可以使用不同的名称 比如 someOtherName() 。

因此在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。

Java 会将你的方法映射到接口方法,要调用方法,可以调用接口的函数式方法名(在本例中是accpt() ) 而不是你的方法名。

【也就是调用过的时候用接口中的名字就好了,自己起的名字不需要用】

下面看看所有基于函数式、应用于方法引用(即那些不涉及基本类型的函数)

下面创建一个最简单的函数式签名

// functional/ClassFunctionals.java
// 内置函数接口的比较多的使用例子,但是没有本质性的区别
// 这里我加了2个输出,可以看到方法绑定赋值之后调用接口内的方法实际调用的就是未绑定方法赋值过去引用所指向的方法,也就是你自己实现的这个

class AA {
}

class BB {
}

class CC {
}


public class ClassFunctionals {
    static AA f1() {
        return new AA();
    }

    static int f2(AA a1, AA a2) {
        return 1;
    }

       static void f3(AA aa) {
        System.out.println("f3被调用了");
    }

    static void f4(AA aa, BB bb) {

    }

    static CC f5(AA aa) {
        System.out.println("f5被调用了,入参AA,返回值CC");
        return new CC();
    }


    static CC f6(AA aa, BB bb) {
        return new CC();
    }

    static boolean f7(AA aa) {
        return true;
    }

    static boolean f8(AA aa, BB bb) {
        return true;
    }

    static AA f9(AA aa) {
        return new AA();
    }

    static AA f10(AA aa1, AA aa2) {
        return new AA();
    }
    // 这10个方法基本是这几个类作为参数来回换

    public static void main(String[] args) {
        Supplier<AA> s = ClassFunctionals::f1;
        s.get(); // x相当于调用 AA的 f1函数,创建一个 AA 对象
        Comparator<AA> comparator = ClassFunctionals::f2;
        comparator.compare(new AA(), new AA());

        Consumer<AA> cons = ClassFunctionals::f3;
        cons.accept(new AA());

        BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
        bicons.accept(new AA(), new BB());

        Function<AA, CC> f = ClassFunctionals::f5;
        CC apply = f.apply(new AA());

        BiFunction<AA, BB, CC> bif = ClassFunctionals::f6;
        bif.apply(new AA(), new BB());

        Predicate<AA> p = ClassFunctionals::f7;
        boolean result = p.test(new AA());

        BiPredicate<AA, BB> bip = ClassFunctionals::f8;
        result = bip.test(new AA(), new BB());

        UnaryOperator<AA> uo = ClassFunctionals::f9;
        AA aa = uo.apply(new AA());
        BinaryOperator<AA> bo = ClassFunctionals::f10;
        aa = bo.apply(new AA(), new AA());

    }
}
/**
输出
f3被调用了
f5被调用了,入参AA,返回值CC
*/

请注意,每个方法的名称都是随意的,一旦将方法引用赋值给函数接口,我们就可以使用与该接口关联的函数方法,其具体方法体就是未绑定方法的实现。

在例子中具体对应的就是 get()compare()accept()apply()test()

多参数函数接口

·java.util.functional中的接口是有限的。比如有了 BiFunction,但它不能变化,如果需要三个参数的函数接口怎么办?

可以查看 Java 库源代码并自行创建:

// TriFunction.java
// 自己创建一个三个参数的函数式接口
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

// functional/TriFunctionTest.java

public class TriFunctionTest {
		
  	// 对应函数式接口的方法
    static int testMethod(int i, long l, double d) {
        return 99;
    }

    public static void main(String[] args) {
        // 将方法引用赋值给接口
        TriFunction<Integer, Long, Double, Integer> tf = TriFunctionTest::testMethod;
      	// Lambda 实现接口
        tf = (i, l, d) -> 12;
      	// 我又加了个匿名内部类的实现做对比
         TriFunction<Integer, Long, Double, Integer> triFunction = new TriFunction<Integer, Long, Double, Integer>() {
            @Override
            public Integer apply(Integer integer, Long aLong, Double aDouble) {
                return 99;
            }
        };

    }
}

缺少基本类型的函数

重温 BiConsumer ,创建缺少 int, long 和 double 的各种排列

【对 BiConsumer 这个接口是干啥的,我还是没有建立起直观的印象,那么看源码吧】

【可以看到,这个接口有一个 accept方法,是一个无返回值,两个参数的方法,就是对这两个翻出进行处理,至于咋处理就由你来定义了,只要方法符合这个返回值和入参就可以赋值给这个接口,也就可以说你那个类实现了这个接口】

// BiConsumerPermutations.java
// 分别输出 int,double / double ,int / int /long
public class BiConsumerPermutations {
    static BiConsumer<Integer, Double> bicid = (i, d)
            -> System.out.format("%d,%f %n", i, d);
    static BiConsumer<Double, Integer> bicdi = (d, i)
            -> System.out.format("%d,%f %n", i, d);
    static BiConsumer<Integer, Long> bicil = (i, l) ->
            System.out.format("%d,%d %n", i, l);

    public static void main(String[] args) {
        bicid.accept(47, 11.34);
        bicdi.accept(22.45, 92);
        bicil.accept(1, 11l);
    }
}

这里使用 System.out.format() 来显示。 它类似于 System.out.println() 但提供了更多的显示选项。

%f 表示将 n 作为浮点数输出,

%d 表示 n 是一个整数值。

其中可以包含空格, %n 表示换行,使用 \n 也可以,但是 %n 自动跨平台

上面例子使用了包装类型,装箱和拆箱用于在基本类型之间的相互转换。也可以使用包装类型,如 Function,而不是预定义的基本类型,代码如下:

// FunctionWithWrapped.java
// 当使用 Function 的时候,涉及到对象的装箱和拆箱,这时候选择使用对应的专门类型可以省略这个步骤,提高效率
public class FunctionWithWrapped {
    public static double returnD(int i) {
        return i;
    }

    public static int returnI(double d) {
        return (int) d;
    }


    public static void main(String[] args) {
        // 这里使用 Function接口的话,实现的时候需要进行显示的类型强转
        Function<Integer, Double> fid = i -> (double) i;
        // 使用专门的转换类就不需要
        IntToDoubleFunction fid2 = i -> i;

        // 这里我加了个 使用 double 转 int的内置函数接口,把 returnI 也用上了
        DoubleToIntFunction d = FunctionWithWrapped::returnI;
        double v = returnD(10);
    }
}

如果这里不使用强制转换则编译器会抛出异常信息 "Integer cannot be converted to Double"。而使用 IntToDoubleFunction 就没此类问题

为什么 IntTODoubleFunction不需要呢?看源码:

@FunctionalInterface
public interface IntToDoubleFunction {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    double applyAsDouble(int value);
}

这里显示的指明了返回值是double ,精度比入参 int 高,所以不需要强转。

而 Function 这个 R apply(T) ,你并不知道哪个精度更高,所以编译器需要让你显示的进行转换。

之所以我们可以简单地编写 Function< Integer,Double> 并返回合适的结果,很明显是为了性能。

使用基本类型可以防止传递参数和返回结果过程中的自动装箱和拆箱。

似乎考虑到使用频率,某些函数类型并没有预定义。

总结

如果简单编写,可以使用Function,但是涉及到性能的话建议使用专门的转换接口。

或者自己编写接口也可以,明白规则之后编写函数式接口非常简单。


layout: post
title: 《On Java8》第十三章——函数式编程(3)高阶函数、闭包与柯里化
date: 2020-02-18 01:26:52
tags:

  • 读书笔记
    categories:
  • 《On Java8》

高阶函数

高阶函数(Higher-order Function) 指 消费或产生函数的函数。

【也就是一个方法的返回值是另一个方法。】

来一个产生函数的例子:

// ProduceFunction.java
// 继承了 Function 接口,返回值和入参类型相同
interface MyFunction extends Function<String, String> {
} // [1]

public class ProduceFunction {
    // 高阶函数,返回了将入参字符串转为小写这个动作
    static MyFunction produce() {
        return s -> s.toLowerCase(); // [2]
    }

    public static void main(String[] args) {
        MyFunction f = produce();
        System.out.println(f.apply("YELLING"));
    }
}

// 输出
yelling

这里 produce()高阶函数

[1] 使用继承可以轻松为预定义好的专用函数式接口来创建别名

[2] 使用 Lambda 表达式轻松在方法中创建和返回一个函数

要消费一个函数,消费函数需要在参数列表正确地描述函数类型:

其实本质是:【返回了一个实现接口的虚拟内部类的实例】

// ConsumeFunction.java
class One {
}

class Two {
}

// 我自己又加了个根据方法引用生成 Two 的
class Three {
    static Two test(One o) {
        return new Two();
    }
}

public class ConsumeFunction {
    // 入参是一个两个参数的Function,根据参数位置可以看到入参是One,返回值是 Two,与这个方法的返回值一致。
    static Two consume(Function<One, Two> oneTwo) {
        System.out.println("入参是 Function,根据 Function.apply 返回 Two对象的方法被调用了");
        return oneTwo.apply(new One());
    }

    public static void main(String[] args) {
        // 实现与 consume 相同效果的 Lambda 表达式
        Function<One, Two> f = one -> new Two();
        //  f.apply(new One) 直接可以生成一个 Two 的实例
        Two apply = f.apply(new One());
        System.out.println("使用Lambda表达式生成的对象:" + apply);

        Two consume = consume(f);
        System.out.println("使用 Consume 生成的对象:" + consume);

        Two two = consume(one -> new Two());
        System.out.println("将Lambda表达式传入 consume 生成的对象: " + two);

        // 把 Three类的test方法赋值给 Function,传入consume,也可以生成 Two 对象
        Two three = consume(Three::test);
    }
}

/**
输出
使用Lambda表达式生成的对象:functional.Two@7b23ec81
入参是 Function,根据 Function.apply 返回 Two对象的方法被调用了
使用 Consume 生成的对象:functional.Two@6acbcfc0
入参是 Function,根据 Function.apply 返回 Two对象的方法被调用了
将Lambda表达式传入 consume 生成的对象: functional.Two@3feba861
*/

【可以看到这里我发散了一下,自己加了个方法引用的例子,大家也可以多魔改一下样例代码,试试自己的理解到底对不对。找出自己的薄弱项】

消费函数的同时生成一个新的函数,事情就变得相当有趣了

class I {
    @Override
    public String toString() {
        return "IIII";
    }
}

class O {
    public O() {
        System.out.println("O的构造函数被调用了");
    }

    @Override
    public String toString() {
        return "OOO";
    }
}


public class TransformFunction {
    // 这个方法的入参也是一个 Function 可以看做是一个函数 一种行为
    // 入参是一种行为 返回值也是一种行为,消费行为,生成行为
    // 这里我觉得如果刚开始看不太明白的话,直接通过IDE的功能将 lambda 展开为 匿名内部类 就会看得非常清楚了
    // 这里实现了一个 Function 的匿名内部类,类中的 apply 方法 打印入参 然后不做处理直接返回

    /**
     * 将 Lambda 展开为匿名内部类形式
     * static Function<I, O> transform(Function<I, O> in) {
     * return in.andThen(new Function<O, O>() {
     *
     * @Override public O apply(O o) {
     * System.out.println(o);
     * return o;
     * }
     * });
     * }
     */

    // Lambda
    // 然后在看一下 andThen 函数的定义
    static Function<I, O> transform(Function<I, O> in) {
        System.out.println("transform 被调用了");
        return in.andThen(o -> {
            System.out.println("transform 被调用才进来的");
            System.out.println("当前对象:"+o + "apply()");
            return o;
        });
    }


    public static void main(String[] args) {
        System.out.println("第一个方法引用:myApply");
        Function<I, O> ioFunction = // 这里使用我自己实现的 apply方法赋值给 Function
                TransformFunction::myApply;
        System.out.println("开始调用 transform(), 入参是一个 Lambda 表达式, 该表达式的入参是 i,输出i,返回 O");
        Function<I,O> f2 = transform(i -> {
            System.out.println(i);
            return new O();
        });
        f2.apply(new I());
        System.out.println("transform 调用结束");

        System.out.println("开始调用 iOFunction 的 apply()"); // 这里实际调用的是我下面定义的 myApply()函数
        System.out.println(ioFunction.apply(new I())); // 这里打印出来的 OOO 是 ioFunction.apply(new I()) 这里生成的对象 O
        System.out.println("-------------");
    }

    private static O myApply(I i) {
        System.out.println("这里是打印i的上一行");
        System.out.println("打印I: " + i );
        System.out.println("这里是return O 的上一行");
        // 为什么这个返回这个对象 或者说调用 O 的构造函数,toString 方法被调用了
        System.out.println("开始构造 O 对象");
        O o = new O();
        System.out.println("打印O对象 "+o);
        return o;
    }
}

/**
第一个方法引用:myApply
开始调用 transform(), 入参是一个 Lambda 表达式, 该表达式的入参是 i,输出i,返回 O
transform 被调用了
IIII
O的构造函数被调用了
transform 被调用才进来的
当前对象:OOOapply()
transform 调用结束
开始调用 iOFunction 的 apply()
这里是打印i的上一行
打印I: IIII
这里是return O 的上一行
开始构造 O 对象
O的构造函数被调用了
打印O对象 OOO
returnO的上一句
OOO
-------------


*/

【这里我把这个例子改的复杂了点,但是一个例子就加深了不少掌握程度,下面是原例子】:

// functional/TransformFunction.java

import java.util.function.*;

class I {
  @Override
  public String toString() { return "I"; }
}

class O {
  @Override
  public String toString() { return "O"; }
}

public class TransformFunction {
  static Function<I,O> transform(Function<I,O> in) {
    return in.andThen(o -> {
      System.out.println(o);
      return o;
    });
  }
  public static void main(String[] args) {
    Function<I,O> f2 = transform(i -> {
      System.out.println(i);
      return new O();
    });
    O o = f2.apply(new I());
  }
}
/**
输出结果
I
O
*/

这里 transform() 生成一个与传入函数具有相同签名的函数,但是可以生成任意你想要的类型。

这里调用的 andThen 是 Function 中的默认方法,专门用于操作函数。

顾名思义,在调用 in 函数之后调用 toThen() 。要附加一个 andThen() 函数,只需要将函数作为参数传递。

transform() 产生的是一个新函数,它将 in 的动作与 andThen() 函数的参数结合起来

【可以看到 andThen 的入参是一个 Function ,例子中传入的是一个 Lambda ,该 Lambda 的入参和返回值都是 o,符合 andThen 的入参要求。】

闭包

【之前在匿名内部类章节就中讲到了闭包这个行为】

在上一节的 ProduceFunction.java 例子中,我们从方法中返回 Lambda 函数。

虽然过程简单,但还是有些问题必须回过头探讨一下

【这就对了啊,感觉很多东西都没有说的太清楚太细,是不是默认读者都已经完全明白了啊?】

闭包(Closure) 一词总结了这些问题。它非常重要,利用闭包可以轻松生成函数。

考虑一个更复杂的 Lambda,它使函数作用于 域之外的变量。返回该函数会发生什么?

也就是说,当你调用函数时,它对那些”外部“变量引用做了什么?

如果语言不能自动解决这个问题,那将变的非常有挑战性。

能够解决这个问题的语言被称为 支持闭包 或者叫 在词法上限定范围(或使用术语 — 变量捕获

Java8 提供了有限但合理的闭包支持,下面用一些简单的例子说明:

【之前说过,Lambda 或者 函数式的好处就是不改变函数外的变量,但是一旦 Lambda 涉及到函数外的变量就可能引起很多问题, 闭包指的是能够解决 Lambda 引用函数外变量的情况下可能发生的问题。这个看起来确实很重要,直接敲代码吧。】

下面例子的函数中,方法返回访问对象字段和方法参数

但是仔细考虑一下, i 的这种用法并非大难题。

因为对象很可能在调用 makeFun() 之后就存在了。

实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[5]
如果你对同一个对象多次调用 makeFun(),你最终会得到多个函数,它们共享 i 的存储空间:

可以看到,每次调用 getAsInt() i 都会增加,表明其存储是共享的。

如果 i 是 makeFun() 方法的局部变量怎么办?

正常情况下,方法执行结束,局部变量消失,但是如果你这么写仍然可以通过编译:

// functional/Closure2.java
public class Closure2 {
    IntSupplier makeFun(int x) {
        int i = 0;
        return () -> x + i;
    }
}

由 makeFun() 返回的 IntSupplier "关闭" i 和 x ,因此当你调用返回的函数时两者仍然有效。

但请注意,没有像 Closure1 那样对 i 进行递增,因为会产生编译时的错误,看代码:

【这里说 Lambda 表达式中的变量必须是不可变的】

从 Lmbda 表达式引用的局部变量必须是 final 或者等同 final效果的。

如果使用 final 修饰则通过编译,也代表 i 不可变。

// functional/Closure3.java

// {WillNotCompile}
import java.util.function.*;

public class Closure3 {
    IntSupplier makeFun(int x) {
       final int i = 0;
        //return () -> x++ + i++;   // x++ 和 i++ 都会报错: 因为 Lambda 中局部变量必须为 等效final的,也就是不能够修改
        return () -> x + i;
    }
}

【说到底还是因为匿名内部类的入参必须是 final,引申到语法不同但是核心机制应该相同的 Lambda/方法引用上也是一样的道理】

x 和 i 的操作犯了同样的错误,:从 Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的。

// functional/Closure4.java

import java.util.function.*;

public class Closure4 {
  IntSupplier makeFun(final int x) {
    final int i = 0;
    return () -> x + i;
  }
}

那么为啥 Closure2里面没有显示的定义 i 为final 但是同样没有报错呢?

这叫做同等 final 效果(Effectively Final) 。这个术语是 Java8 才开始出现的。表示虽然没有明确地声明变量为 final,但是因为变量没有被改变过而实际有了 final 的同等效果。

如果局部变量的初始值永远不会改变,那么它实际上就是 final的。

如果 x 和 i 的值在方法中的其他位置发生了改变(但不在返回的函数内部),则编译器仍将其视为错误。

每个递增操作会分别产生错误信息,代码如下:

/ functional/Closure5.java

// {无法编译成功}
import java.util.function.*;

public class Closure5 {
  IntSupplier makeFun(int x) {
    int i = 0;
    i++;
    x++;  // x 和 i 都必须为等效 final 的变量
    return () -> x + i;
  }
}

等同final效果 意味着可以在变量声明前加上 final 关键字而不用更改任何其余代码。

通过在闭包中使用 final 关键字提前修饰变量 x 和 i ,我们解决了 Closure5 中的问题:

意思就是这里 x 和 i 都是 只增加1的常量,只要在使用之前用 final 修饰并赋值给另一个引用就ok。

其实这里也不用使用 final 修饰符,因为 将 i 赋值给 iFinal 之后 该变量未发生改变。

// functional/Closure6.java

import java.util.function.*;

public class Closure6 {
  IntSupplier makeFun(int x) {
    int i = 0;
    i++;
    x++; // 这里虽然 i和x的值改变了,但是之前要返回之前将其定义为 final即可正常运行
    final int iFinal = i;
    final int xFinal = x;
    return () -> xFinal + iFinal;
  }
}

如果这里使用的是包装类型,需要把 Int 改为 Integer:

// functional/Closure7.java

// {无法编译成功}
import java.util.function.*;

public class Closure7 {
  IntSupplier makeFun(int x) {
    Integer i = 0;
    i = i + 1;  // Integer 的值发生了改变,导致非final的产生
    return () -> x + i;
  }
}

就算使用包装器类型编译器仍然能够识别该变量被更改过

接下来尝试一下 List:

// functional/Closure8.java

import java.util.*;
import java.util.function.*;

public class Closure8 {
  Supplier<List<Integer>> makeFun() {
    // 每次调用 makeFun 返回的都是新的内含一个参数 "1"的不可变的 ArrayList
    final List<Integer> ai = new ArrayList<>();
    ai.add(1);
    return () -> ai;
  }
  public static void main(String[] args) {
    Closure8 c7 = new Closure8();
    List<Integer>
      l1 = c7.makeFun().get(),
      l2 = c7.makeFun().get();
    System.out.println(l1);
    System.out.println(l2);
    l1.add(42);
    l2.add(96);
    System.out.println(l1);
    System.out.println(l2);
  }
}
/**
输出
[1]
[1]
[1, 42]
[1, 96]
*/

【可以看到就算重复调用 makeFun() 方法,生成的list 的元素也都是1】

这次一切正常,哪怕我们改变了 List 的值也没有产生编译时错误。

通过观察输出发现,这看起来非常安全,因为每次调用 makeFun() 都会创建并返回一个全新的 ArrayList

也就是说,每个闭包内都有自己独立的 ArrayList,它们之间互不干扰。

例子中的 list 被声明为是 final的,尽管在这个例子中去掉 final 也能获得同样的结果。

应用于引用对象的final 仅仅代表不能重新赋值引用,而改变对象本身的值是被允许的。【final 章节中的基础知识】

下面看看 Closure7 和 Closure8 之间的区别: 在 Closure7.java 中变量 i 有过重新赋值。 也许这就是等同 final 效果错误消息的触发点。

下面看看给 List重新赋值所导致的编译错误:

// functional/Closure9.java

// {无法编译成功}
import java.util.*;
import java.util.function.*;

public class Closure9 {
  Supplier<List<Integer>> makeFun() {
    List<Integer> ai = new ArrayList<>();
    ai = new ArrayList<>(); // 这里给返回的数组重新进行了赋值,打破了等同 final效果
    return () -> ai;
  }
}

现在回顾一下 Closure1.java 中的 i,为什么那里面的 i 被修改了编译器却并没有报错?

因为 i 是外部类的成员,所以这样做肯定是安全的除非你正在创建共享可变内存的多个函数)。

你可以说当变量是外部类的成员变量而不是方法内的局不变量时不会发生变量捕获(Variable Capture)。

但可以肯定的是 Closure3.java 的错误信息是专门针对局部变量的

因此**,规则并非只是 在 Lambda 之外定义的任何变量必须是 fina 或者 等同 final 效果那么简单。**

而是你必须考虑捕获的变量是否等同 final 效果。

如果它是对象中的字段,那么它拥有独立的生命周期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda时存在。

【这段话中的信息挺关键,总结一下就是如果变量是方法内的,则需要考虑捕获的是否是 final 变量,如果是类变量,则不需要考虑】

作为闭包的内部类

我们可以使用匿名内部类重写之前的例子:

// functional/AnonymousClosure.java

import java.util.function.*;

public class AnonymousClosure {
  IntSupplier makeFun(int x) {
    int i = 0;
    // 同样规则的应用:
    // i++; // 非等同 final 效果
    // x++; // 同上
    return new IntSupplier() {
      public int getAsInt() { return x + i; }
    };
  }
}

实际上只要有内部类,就会有闭包(Java8 只是简化了语法)。

Java8 之前, 变量 x 和 i 必须被确定的声明为 finalJava8 中,内部类的规则放宽,包括了等同 final 效果。

函数组合

函数组合 (Function Composition) 意味 "多个函数组合成新函数"。

它通常是函数式编程的基本组成部分。

在前面的 TransformFunction.java 类中,有一个使用 andThen() 的函数组合示例。
一些 java.util.function 接口中包含支持函数组合的方法:

下例使用了 Function 里的 compose()andThen()

// FunctionComposition.java
public class FunctionComposition {

    // 这里声明了4个函数 f4 是前三个的组合
    static Function<String, String> f1 = s -> {
        System.out.println("S1获得了字符串: " + s);
        System.out.println(s);
        return s.replace('A', '_');
    },
            f2 = s -> s.substring(3),

    f3 = s -> {
        System.out.println("f3入参:" + s);
        return s.toLowerCase();
    },
            f4 = f1.compose(f2).andThen(f3); // compose 表示 f2的调用该发生在f1之前 然后调用 s3

    public static void main(String[] args) {
        System.out.println(f4.apply("GO AFTER ALL AMBULAMNCES"));
    }
}
/**
输出
S1获得了字符串: AFTER ALL AMBULAMNCES
AFTER ALL AMBULAMNCES
f3入参:_FTER _LL _MBUL_MNCES
_fter _ll _mbul_mnces
*/

这里我们重点看正在创建的新函数 f4。它调用 apply() 的方式与常规几乎无异[^8]。

f1 获得字符串时,它已经被f2 剥离了前三个字符。这是因为 compose(f2) 表示 f2 的调用发生在 f1 之前。

p4 获得了所有动作的组合,并组合成了一个更复杂的动作。

解读: 如果字符串不包含 bar 且长度小于5,或者包含foo,则结果为 true。

正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter() 操作。 filter() 使用 p4 的谓词来确定对象的去留。最后我们使用 forEach()println 方法引用应用在每个留存的对象上。

从输出结果我们可以看到 p4 的工作流程:任何带有 foo 的东西都会留下,即使它的长度大于 5。 fongopuckey 因长度超出和不包含 bar 而被丢弃。

总结:

闭包的核心在于方法内变量的不可变性,而对方法外的类变量则没有要求。

柯里化和部分值:

【将多个入参的函数转化为一组单入参函数】

柯里化(Currying) 的名称来自于其发明者之一 Haskell Curry。他可能是计算机领域唯一名字被命名重要概念的人(另外就是 Haskell语言)

柯里化指的是:将一个多参数的函数,转换为一系列的单参数函数

【重构中也提到了这个概念,降低函数复杂的度的】

// CurryingAndPartials.java
public class CurryingAndPartials {
    // 未柯里化的函数,有多个入参
    static String uncurried(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 柯里化函数
        Function<String, Function<String, String>> sum =
                a -> b -> a + b; //[1] a 的入参是一个函数b。 b 是一个 返回 a+b 的函数

        System.out.println("uncurried:" + uncurried("HI ", "Ho"));

        Function<String, String> hi = sum.apply("HI "); //[2]
        String ho = hi.apply("Ho");
        System.out.println("sum.apply:" + ho);

        // 部分应用
        Function<String, String> sumHi = sum.apply("Hup ");
        System.out.println(sumHi.apply("Ho"));
        System.out.println(sumHi.apply("Hey"));
    }
}
/**
输出
uncurried:HI Ho
sum.apply:HI Ho
Hup Ho
Hup Hey
*/

【看着这一串的箭头是不是感觉有点懵逼?仔细看看,发现就是函数套函数】

第二个参数是另一个函数

[2] 柯里化的目标是通过提供一个参数来创建一个新的函数

所以现在有了一个带参函数和剩下的无参函数你可以从一个双参数函数开始,得到一个单参数的函数。

下面来一个柯里化的三参数函数:

public class Curry3Args {
    public static void main(String[] args) {
        Function<String,
                Function<String,
                        Function<String, String>>> sum =
                a -> b -> c -> a + b + c; // 这里相当于参数的连续传递吗,c -> a + b + c 这里 b和c是哪里来的?

        Function<String, Function<String, String>> hi =
                sum.apply("Hi ");
        Function<String, String> ho = hi.apply("Ho ");
        System.out.println(ho.apply("Hup"));
    }
}

// 输出结果
Hi  Ho Hup

处于每个级别的箭头级联(Arrow-cascading) ,你在类型声明中包裹了另一个 Function

处理基本类型装箱时,请适当使用 Function 接口:

public class CurriedIntAdd {
    public static void main(String[] args) {
        IntFunction<IntUnaryOperator> curriedIntAdd = a -> b -> a + b; // 使用不需要拆装箱的内置Function

        IntUnaryOperator add4 = curriedIntAdd.apply(4);
        System.out.println(add4);
        System.out.println(add4.applyAsInt(5));
    }
}

/**
输出
9
*/

【可以看到输出是4和5进行相加,将两个入参的函数 比如(int a,int b) 柯里化了两个 单参数的函数】

可以在网络上找到更多的柯里化的例子。通常它们是Java 之外的语言实现的。 但是如果你理解了柯里化的基本概念,你可以很轻松地用户 Java 实现它们。

纯函数编程

Java 的函数式编程需要确定变量是 final 的。

同时方法和函数没有副作用【这里的副作用我理解为修改方法之外的变量】

Java 在本质上并非是不可变语言,我们无法通过编译器查错。【这句话是否太片面,需要自己进行求证】

作者更推荐使用 Scala 或者 Clojure 这样的为了保持不变形而设计的语言,如果你必须要使用纯函数式编写,则用这些语言更合适。

不过为什么要用纯函数式呢?看着是很简洁,但是也增加了理解成本和学习成本,不过多学点东西还是好的。

本章小结

Lambda 表达式和方法引用并没有改变 Java 的本质,没有将 Java 转换成函数式语言,只是提供了对函数式编程的支持。

这对 Java 来说增加了语法的支持,如果你喜欢编写更简洁明了,易于理解的代码,可以使用这两种形式。还有下一章中的流式编程,看起来简洁明了。

Lambda 和方法引用 因为 Java 早期的语言包袱会导致也有很多问题,特别是没有泛型的 Lambda。

Lambda 在 Java中并非一等公民,但是它的使用也会让人感觉到沮丧和鸡肋。

【说明有巨大的局限性,但是局限性在哪里作者只给出了结论却没有支撑的数据和论证,让人看着很难受】

总结:

额外参考资料:

[深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)]

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions