[译]函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式 - 响应式编程 [Android RxJava 2](这到底是什么)第三部分

1,646 阅读20分钟
原文链接: github.com

太棒了,我们又来到新的一天。这一次,我们要学一些新的东西让今天变得有意思起来。

大家好,希望你们都过得不错。这是我们的 RxJava2 Android 系列的第三篇文章.

在这篇文章中,我们将讨论函数式的接口,函数式编程,Lambda 表达式以及与 Java 8 的相关的其它内容。这对每个人近期都是有帮助的。

动机:

动机和我在分享第一部分时一致。Lambda 表达式、函数式编程、高阶函数等等总是让我在使用 Java 时很痛苦,因为大家都知道,Java 是面向对象编程的。所以,Java 怎么可能支持函数式编程。那么,在函数式编程里,Lambda 表达式的角色是什么呢?为了让所有问题变得简单明了,我会从函数式接口开始。重要的是,我向你们保证,只要你们 100% 看完这部分,你们将会对最近我们听到的所有名字都感觉自在很多。函数式接口,默认方法,纯函数,函数的副作用,高阶函数,可变的与不可变的,函数式编程与 Lambda 表达式。我觉得很多人最近都在使用 Lambda 表达式,但或许在读完这篇文章后,他们会更了解 Lambda 表达式。攻克难题的时刻到了。

修改:

第一部分,我们讨论了 Rx 最重要、最基础也最核心的概念,那就是观察者模式。在第二部分,我们讨论了拉模式和推模式,以及命令式和响应式编程。

介绍:

今天我们将会弄清楚所有关于函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程以及 Lambda 表达式的所有困惑。所以为了方便理解 Lambda 表达式的概念,我要先解释什么是函数式接口。

函数式接口:

一言以蔽之,函数式接口是有且只有一个抽象方法的接口。换言之,任何拥有唯一抽象方法的接口都可以被称为函数式接口。这里我想分享一些背景知识,这些知识不属于这个系列,但是对你面试尤其有用。如果你读过我的定义。我用了关键词抽象,众所周知的是接口里的方法都是抽象的,但那是 Java 8 出现之前的情况。在 Java 8 里,我们可以在接口中定义一个包含方法体的方法,这个方法叫默认方法,正如下面所示。

public interface Account {

   void name();

   default void showTyepOfAccount(){
      System.out.println("Don't know :(" );
   }
}

现在我们要回顾一下定义。函数式接口是个拥有一个抽象方法的接口。

所以现在,如果我问你上面的接口是不是一个函数式接口,你的答案是什么?根据定义,答案应该是:不是。但那却是一个有效的函数式接口,为什么呢……

现在,如果接口定义默认方法或者继承并重写 java.lang.Object 类里的任何方法。那个接口还是函数式接口,这是因为 java.lang.Object 方法并不算数。正如我在下面展示给你的真正的函数式接口。

public interface Add {
    void add(int a, int b);

    @Override
    String toString();

    @Override
    boolean equals(Object o);
}

所以,任何有多于一个抽象方法的接口不能被称为函数式接口,正如下面所示。

public interface Do {

    void why();

    void sorry();
}

我相信你已经理解了函数式接口的概念。这也是 Lambda 表达式重要的核心概念,一定要好好记住。

一些我们现在日常开发使用的函数式接口的例子:

public interface Runnable {
    public abstract void run();
}

public interface OnClickListener {
    void onClick(View v);
}

现在是时候向你展示 Java 7 和 8 的 Comparator 接口了。它们都是有效的函数式接口。

Java 7 的比较器:

在 Java 8 里:

可别搞混了。它们都是有效的函数式接口。只要记住函数式接口的三点原则。

只有一个抽象方法 - 可以有默认方法 - 可以使用 java.lang.Object 方法。

如果任何接口满足这三点,那就一定是有效的函数式接口,反之则不是。

在 Java 8 里有一个新的工具包 java.util.function。在这个包里,所有的接口都是函数式接口。当我们需要用到流(Stream) API 时,这个工具包很有用。当我们开始学习 Rx Android 的时候,这个包会让我们学到更多。

很重要的一点。当我们要开始使用 Rx Android 时,我们会使用很多这样的函数式接口。基本上,在安卓平台中,我们依赖于 Rx Java 和 Rx Android。现在,我将要给你看一看 Rx Java 1.0 和 2.0 包里的函数式接口。没有必要去记住这个,也没有必要紧张,这只是通用知识。只要试着记得函数式接口的概念就可以了。当你开始使用 Rx,这些你都会在潜移默化中记住的。

RxJava 1:

RxJava2:

哇哦!我们该庆祝一下我们已经知道什么是函数式接口,以及在 Java 8 里什么是默认方法。我在介绍一栏中写到的本章需要探讨的概念,已经解释完两个了。函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

函数式编程:

说实话,我的大多数工作都是用 Java 和 C++ 完成的,而这两种语言都是命令式的而非纯函数式的。所以我打算尽力解决所有的我面对的困惑。如果我有什么地方弄错了,请不要介意。不过务必在回复里提醒我,这样我就可以修正我的文章了。

在进入无聊的定义之前,我打算回顾我们在学校里学习到的理论。这对接下来阐释剩下模棱两可的名词是很有帮助的。

每个做开发的人都知道函数。但是现在请试着忘记我们学习过的所有编程知识,重回学校。

好孩子。

数学概念上的函数是什么?[现在请忘掉你所知道的 Java 或者 C++ 等任何编程语言关于函数的所有知识。]

什么是函数?一个根据输入决定输出的方程式。挺无聊的,好的,那忘记这个。

有多少人听过下面的句子。

f(x) = x+3

如果 x = 2,答案是什么。

f(x) 等于 y。

y = x+3

x = 2

y = 2+3

y = 5

所以,f(x) = x+3 是一个函数。当你给同一个输入,会给你同样的输出。

再来一个例子。

有多少人记得 Sin(x) [ 三角函数 ]

我们当然记得。在学校的时候,对一个 45° 的角取正弦,我会得到 1/2 的答案,如下所示。

y = Sin(45deg)

y = 1/2

后来我在大学时代里也用过相同意义上的“函数”。对于给定的输入值,会得到唯一的结果。这就叫纯函数。我会在接下来解释。

我们回顾了在大学里常用的一些函数。现在,当我们在编程中用同样的思想,这就叫函数式编程。不要紧张,我马上就会解释。我们从儿时的回忆里回来看看。

首先,我们要讨论一些困惑。比如当我们刚开始写程序的时候,都写过一个计算圆面积的函数。

public double areaOfACircle(int radius){
    return radius*radius*3.14;
}

很好。随着我变得更专业,我对函数认识也不同了。比如,写一个美元转巴基斯坦卢比的汇率计算器。

public float convertUSDIntoPKR(int USD){
    return USD*getTodayPKRValueFromAPI();
}

在编程中,上面的是一个函数。但是在数学中,这就有问题了。因为在数学中,我们总是说,同一个输入对应同样的输出。但是编程中的函数给同样的输入可以有不同的输出,因为它依赖于其它数值。所以这里,我们又要介绍一个名词,叫纯函数。在数学概念里,我们知道每个函数都是纯函数,如 Sin(),但是,在我们的编程语言里,我们有很多函数给我们不同的数值。所以,这就是我们要介绍的,编程语言里的纯函数。 纯函数的返回值由它的输入值决定,而且没有明显可见的副作用。

下一个名词,副作用。任何不纯的函数叫非纯函数,它可能产生副作用。或者一些函数本身是纯函数(指对于给定的输入值可以得出相同的输出值),但是如果它在产生结果的时候与外界发生了数据交换,那么我们就不能说这是一个纯函数。

第一类非纯函数的典型就是 Random 函数。对于给定的一个输入值,它总是返回不同的结果。

第二类副作用的典型是 println() ,它是一个非纯的函数。因为它将输出值转去了输入输出设备(而不是作为函数返回值输出),所以产生了副作用。任何纯函数一旦用 println() 来注释打印,那它就不再是纯函数了。

一些例子:

纯函数:

public int squre(int x){
    return x*x;
}

因为副作用而非纯的的函数;

public int squre(int x){
    System.out.println(x*x);
    return x*x;
}

非纯函数:

public void login(String username, String password, Callback c){
    API.login(username, password, callback);
}

现在我们又理解了两个名词。纯函数和副作用。

函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

接下来,我们准备讨论可变的不可变的。在数学中,我们记得,当我给函数一个值,我总能获得新的值,而我原来的值还是一样的。但是,在编程中,那个概念就变了。这时为什么我们有两种不同的定义。可变的和不可变的。在面向对象中,我们几乎无时不刻不在破坏不可变性。这可能导致很多问题,但是函数式编程总是利用不可变性。正如每个人都知道在 Java 里,String 是不可变的。

String s = "Hello";
s = "World";

这里,我们本来的字符串从未改变。虽然第二行我们创建了新的字符串并且把它赋给我的 s 对象。

所以,什么是可变的?给你一个例子。

int array []= {1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
    array[i] = array[i] * 2;
}

在 Java 或者命令式编程中,我认为上面的代码基本上是可变的。它改变了原本的数组值。但是在函数式编程里,如果我做了同样的事情,我总是获得与 2 相乘后的数值组成的新数组,而我原来的数据仍然保持不变。

Integer array []= {1,2,3,4,5};
Arrays.stream(array).map(v->v*2).forEach(i-> System.out.print(i+" "));
System.out.println();
for (int i = 0; i < array.length; i++) {
    System.out.print(array[i]+ " ");
}
Output:
2 4 6 8 10
1 2 3 4 5

上面的例子是用 Java 8 写的,但是那跟之后讲 Rx 是一样的。举出这个例子,只是为了帮助你理解可变和不可变的概念。正如你所看到的,所输出的原本的数组值并没有改变。

现在可能你在想这样的好处是什么。我这里用另外一个例子来解释。如果我知道我所有的函数都是纯的并且是不可变的,我可以做很多事情而不用管我数据的状态。例如,我要使用线程。

public class FunctionalLambda {

    public static void main(String[] args) {

        Integer array []= {1,2,3,4,5};
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < array.length; i++) {
                    array[i] = array[i]+1;
                }
            }
        }).start();
        for (int i = 0; i < array.length; i++) {
            System.out.println(square(array[i]));
        }
    }
    public static int square(int a){
        return a*a;
    }
}

在这个例子里,基本上我用到了线程。子线程让数组里的每一个数据 + 1,而主线程或者其他子线程则对数组中的数据做平方运算。作为一个开发者,我期望数值应如下所示。

1
4
9
16
25

但是,当我执行这段代码时,得到的结果如下。

4
9
16
25
36

结果和期望并不相同,因为我没有管数据可变性。现在我准备写一个合适的函数式程序,对数据的不可变性进行严格控制。

public class FunctionalLambda {

    public static void main(String[] args) {

        Integer array []= {1,2,3,4,5};
        new Thread(new Runnable() {
            @Override
            public void run() {
                Observable.from(array)
                        .map(integer -> integer+1)
                        .subscribe(integer -> {});
            }
        }).start();

        Observable.from(array)
                .map(integer -> square(integer))
                .subscribe(integer -> System.out.println(integer));

    }

    public static int square(int a){
        return a*a;
    }
}

注意:如果要运行上面的例子,你需要下载 rxjava 的 jar 包

运行完这段例子后,我所期望的和实际输出的是一致的,因为我的程序没有对数组做直接改变,而是拷贝了我的数据。这就是为什么我可以说我的数组是不可变的。对不起,我也用 Rx 了。但是从现在开始,我会加一点 Rx 到我的例子里。我会在接下来的文章中解释清楚。但是,请相信我,那是一个函数式程序。在程序里,我有一个纯函数做平方运算,并且我的数组不改变,因为我将使用函数式范式。

函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

是时候解释清楚高阶函数 (HOF) 的含义了。

拥有至少一个函数类型为参数的函数,或着返回一个函数的函数叫做高阶函数。

那简直太简单了,并且我们在 Rx 编程中用了很多这个概念。在 Java 8 之前,展示 HOF 还是有点困难的,但是我们使用匿名类作为 HOF。我们大多在 C++ 中使用这个概念,把函数作为一个参数。在安卓中,这就类似于添加一个匿名类为点击事件监听者。所以你可以说,这是 HOF 的一个例子。我会在介绍 Rx 的文章中更详细地解释这个。

函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

现在,如果我们使用这些概念,在任何语言中,我们所讨论的纯函数,HOF,不可变的都是接下来的函数式范式。那就是函数式编程。在面向对象编程时我们经常要管理对象的状态,但是在函数式程序里,我们有数据,管理好了不可变性,我们可以大胆地做运算。

函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

加油呀!我们已经弄清楚了很多关于函数式编程模棱两可的概念。现在我们要用学习 Lambda 表达式来结束这篇文章。

在进入 Lambda 的章节前,我想复习一下前面的内容。

函数式接口 - 有且仅有一个抽象方法的接口。

默认方法 - 在 Java 8 里,我们可以在接口中定义有方法体的方法,这些叫默认方法。

纯函数 - 一个函数的返回值仅由输入值决定,没有明显可见的副作用。

Lambda 表达式:

在计算机编程中,lambda 表达式,也叫匿名函数,是指一类无需定义标识符(函数名)的函数或子程序。”(Wiki)

首先,RxJava 并不依赖于 Lambda 表达式。实际上,函数式编程与 Lambda 表达式没有关系,正如你在我以上的例子中看到的那样,我从来没有说过我用了 lambda。只是 IDE 在某些地方可能把我的代码转换成了 lambda 表达式,但我可以不用它来写代码。那么,问题是,为什么在每一篇关于 Rx 或者函数式编程的博客里,我们看到 lambda 表达式总是核心内容。在我看来,你可以把它们理解为简洁高效的匿名函数语法。

在我详细介绍 Lambda 表达式前,有个先决条件。我们已经知道 Java 是一个静态类型语言。它意味着所有的 java 程序对象和变量总是在编译时间里知道数据类型,如下面的例子所示。

int i = 1;
float j = 3;
Person person = new Person();
String s = "Hello";

同样的,在 Java 7 之前,我们准备用 Collections 来写一个完整的 List 对象初始化,如下所示。

List<String > list = new ArrayList<String >();

但在 Java 7,我们有类型引用的概念。使用这个概念,我们可以写出如下简洁的代码。

List<String > list = new ArrayList<>();

所以现在,编译器在编译时根据上下文决定数据类型。这样,我们就节省了很多时间。

再一次,数据类型引用非常重要。所以我们要关注这个。在 Lambda 表达式中,我们要用到很多次,但是大家因为缺少这个概念而感到困惑。

我们继续用另外一个例子来描述同一个概念。

我写了一个方法,整数作为参数传入,而这个方法将不改变任何东西,返回同样的数值给我,如下所示。

public static void main(String [] args){
    System.out.println(giveMeBack(1));
}

public static int  giveMeBack(int a){
    return a;
}

这是简单的例子。现在我想传个 3.14 给这个方法,有没有人告诉我,会发生什么呢?

是的,你的程序将无法编译。我已经说过了,这个方法只能传入整数。我的下一个要求是,我要使得这个方法适用于所有数据类型。作为一个开发者,我是一个懒人。我不想写重复的代码。这里我想利用 Java 的引用。

public static<T> T  giveMeBack(T a){
    return a;
}

这也叫泛型。利用泛型,我节省了很多时间。这个方法可以适用于任何数据类型,如下图所示。

现在我从 Java 引用中获得了好处。怎么样获得的呢?我的编译器,编译我的程序,为我的所有数据类型生成了代码。现在,编译器可以很容易地从我的参数的数据类型做决定。这里没有什么神奇的地方。每当我没有提到数据结构,我的编译器就从上下文中提取并且赋予其数据类型,因为 Java 是一个静态类型语言。

再重复一遍,Java 是一个静态类型语言。所以如果你觉得你在 IDE 中写的代码没有任何类型。你可能会认为你使用的是一个动态类型语言。你错了,你只是在利用 Java 类型推断而已。

现在,我们可以开始写 Lambda 表达式了。目前,Lambda 表达式只支持 Java 8。在安卓中,如果我们想用它,我们可以用 Retrolambda 库。现在们来解释一下 lambda 表达式。

在安卓中,我想要一个可监听点击事件的按钮,如下面代码所示。

Button button = new Button(this);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Click
    }
});

这里我们传入了一个 OnClickLisetener 的匿名对象。当用户点击,onClick 方法就会被调用。现在我们要用 Lambda 表达式改变这个匿名的,恶心的,复杂的代码。

Button button = new Button(this);
button.setOnClickListener((View v)->{
    // Click
});

通过使用 Lambda 表达式,我的代码可读性更强了。我准备再重构一下上面的例子。

button.setOnClickListener(v -> /* Click */);

我真的很喜欢写类似上面的代码,但是在开始的时候,我真的很困惑,编译器是如何知道我这里在做什么。首先,我利用了 Java 引用。就像编译时,Java 自动知道‘v'是一个 View,因为我们用的是函数式接口。这个接口只有一个抽象方法,它的参数是一个 view,如下所示。

/**
 * Interface definition for a callback to be invoked when a view is clicked.
 */
public interface OnClickListener {
    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    void **onClick**(View v);
}

还记得接口函数的概念吗?现在所有的线索都被串起来了。我们已经讨论了函数式接口。它意味着任何以函数式接口为参数的方法,我就可以写成 Lambda 表达式。这意味着,Lambda 表达式是一个语法糖。我觉得你们现在已经知道 Lambda 表达式是个什么东西了。这就是为什么我要关注函数式接口和其它名词了。

再来一个例子。

Without Lambda:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // Without Lambda
    }
});
thread.start();

使用 Lambda 表达式:

Thread thread = new Thread(()->{});
thread.start();

在 Java 8 或者 Rx Java 中,我们会使用很多函数式接口,因为我们想写出简单明了的代码,并且寥寥数语就可以完成一个大功能。现在我觉得所有的困惑都已经清晰了。这里有一些关于 Lambda 表达式更重要的点。

如果当按钮被按下时,我想写一行代码,我可以写成下面这样。

button.setOnClickListener(v -> System.out.println());

但如果我想写不止一行,那么我需要把它们写进花括号里,如下所示。

button.setOnClickListener(v -> {
    System.out.println();
    doSomething();
});

我可以明确提及数据类型,如下所示。

button.setOnClickListener((View v) -> System.out.println());

现在,如何返回 Lambda 表达式类型呢?再给你一个例子。

public interface Add{
    int add(int a, int b);
}

private Add add= new Add() {
    @Override
    public int add(int a, int b) {
        return a+b;
    }
};

int sum = add.add(1,2);

现在我使用 Lambda 表达式来表现同一个例子。

public interface Add{
    int add(int a, int b);
}

private Add add = (a, b) -> a+b;

int sum = add.add(1,2);

现在可以看到我写的代码有多简洁了。它们的功能是一样的。我没有提及任何返回的数据类型,因为 Java 的类型引用自动帮我决定了这是一个整型。现在,如果我想添加更多的代码到 add 方法的实现中,只需要像下面那样写就行了。

public interface Add{
    int add(int a, int b);
}

private Add add = (a, b) -> {
    System.out.println();
    return a+b;
};

int sum = add.add(1,2);

现在我们知道函数式接口、默认方法、纯函数、函数的副作用、阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。

结论:

大家都太棒了。今天我们到达了一个 Rx 学习中的里程碑。下一篇文章是 War against Learning Curve of Rx Java 2 + Java 8 Stream [ Android RxJava2 ] ( What the hell is this ) Part4。到现在为止,我们了解了观察者模式、拉模式与推模式、响应式与命令式、函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。我认为,如果你都了解了上述名词,Rx 的学习将会越来越简单。现在我感觉你们都已经了解了,所以接下来 Rx 的学习对于我们都会更简单。

祝你们有个愉快的周末。让我们下周再见吧。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划