函数式编程不是一个新概念;事实上,它很旧了。例如,Lisp是一种函数式语言,是当今第二古老的常用编程语言。
函数程序是使用可重用的纯函数(lambda)的小块构建的。程序逻辑由小的声明步骤和不复杂的算法组成。这是因为函数式程序最小化了状态的使用,这使得命令式程序变得复杂且难以重构/支持。
有了 Java8,Java 世界获得了 lambda 表达式和将函数传递给函数的能力。有了它们,我们可以用一种更实用的方式编写代码,并摆脱大量的样板代码。我们在 Java8 中得到的另一个新东西是流,它非常类似于 RxJava 的可观察对象,但不是异步的。结合这些流和 lambda,我们能够创建更具功能性的程序。
我们将熟悉这些新结构,并了解如何将它们用于 RxJava 的抽象。通过使用 lambdas,我们的程序将更简单、更容易理解,本章介绍的概念将有助于设计应用。
本章包括:
Java8 中最重要的变化是 lambda 表达式的引入。它们可以实现更快、更清晰的编码,并使使用函数式编程成为可能。
Java 早在 20 世纪 90 年代就作为一种面向对象的编程语言诞生了,其思想是一切都应该是对象。当时,面向对象编程是软件开发的主要范式。但是,最近,函数式编程变得越来越流行,因为它非常适合并发和事件驱动编程。这并不意味着我们应该停止使用面向对象语言编写代码。相反,最好的策略是混合面向对象编程和函数式编程的元素。将 lambdas 添加到 java8 与这种思想相联系,Java 是一种面向对象的语言,但是现在它有了 lambdas,我们也可以使用函数式风格进行编码。
让我们详细了解一下这个新特性。
为了引入 lambda 表达式,我们需要查看它们的实际值。这就是为什么本章将从一个未使用 lambda 表达式实现的示例开始,然后使用 lambda 表达式重新实现相同的示例。
还记得Observable
类中的map(Func1)
方法吗?让我们尝试为java.util.List
集合实现类似的功能。当然,Java 不支持向现有类添加方法,因此实现将是一个静态方法,它接受列表和转换,并返回包含转换后元素的新列表。为了将转换传递给方法,我们需要一个接口,其中一个方法表示转换。
让我们看看代码:
interface Mapper<V, M> { // (1)
M map(V value); // (2)
}
// (3)
public static <V, M> List<M> map(List<V> list, Mapper<V, M> mapper) {
List<M> mapped = new ArrayList<M>(list.size()); // (4)
for (V v : list) {
mapped.add(mapper.map(v)); // (5)
}
return mapped; // (6)
}
这里发生了什么?
Mapper
。M map(V)
,该方法接收类型为V
的值并将其转换为类型为M
的值。List<M> map(List<V>, Mapper<V, M>)
采用一个包含V
类型元素和Mapper
实现的列表。在源列表的每个元素上使用此Mapper
实现的map()
方法,它将列表转换为包含转换元素的M
类型的新列表。M
,大小与源列表相同。Mapper
实现进行转换,并添加到新列表中。在这个实现中,每当我们想要通过转换另一个列表来创建一个新列表时,我们都必须使用正确的转换来实现Mapper
接口。在 Java8 之前,将自定义逻辑传递给方法的正确方法与匿名类实例完全相同,实现给定的方法。
但让我们看看我们是如何使用这个List<M> map(List<V>, Mapper<V, M>)
方法的:
List<Integer> mapped = map(numbers, new Mapper<Integer, Integer>() {
@Override
public Integer map(Integer value) {
return value * value; // actual mapping
}
});
为了将映射应用于列表,我们需要编写四行样板代码。实际的映射非常简单,只是其中一条线。这里真正的问题是,我们传递的不是动作,而是对象。这掩盖了该程序传递一个操作的真正意图,该操作从源列表的每个项目产生转换,并在最后获得一个包含应用更改的列表。
下面是使用 Java 8 的新 lambda 语法调用的情况:
List<Integer> mapped = map(numbers, value -> value * value);
很直截了当,不是吗?而且它很有效。我们不是传递一个对象并实现一个接口,而是传递一段代码,一个无名函数。
发生了什么事?我们用一个任意方法定义了一个任意接口,但是我们可以传递这个 lambda 来代替接口的实例。在 Java8 中,如果您仅使用一个抽象方法定义接口,并创建一个接收此类接口参数的方法,则可以传递 lambda。如果接口 single 方法接受两个 string 类型的参数并返回一个整数值,lambda 必须由->
前面的两个参数组成,并且要返回整数,这些参数将被推断为字符串。
这种类型的接口称为功能接口。单一方法必须抽象,而不是默认。Java 8 中的另一个新功能是接口的默认方法:
interface Program {
default String fromChapter() {
return "Two";
}
}
更改现有接口时,默认方法非常有用。当我们向它们添加默认方法时,实现它们的类不会中断。只有一个默认方法的接口不起作用;默认情况下不应使用单个方法。
lambda 充当功能接口的实现。因此,可以按如下方式将它们分配给 interface 类型的变量:
Mapper<Integer, Integer> square = (value) -> value * value;
我们可以重用 square 对象,因为它是Mapper
接口的实现。
也许您已经注意到了,但是在到目前为止的示例中,lambda 表达式的参数没有类型。这是因为类型是推断的。所以这个表达式与前面的表达式完全相同:
Mapper<Integer, Integer> square = (Integer value) -> value * value;
示例中的参数没有类型,这一事实并不神奇。Java 是一种静态类型语言,因此函数接口的单个方法的参数用于类型检查。
lambda 表达式的主体如何?任何地方都没有return
声明。事实证明,这两个例子完全相同:
Mapper<Integer, Integer> square = (value) -> value * value;
// and
Mapper<Integer, Integer> square = (value) -> {
return value * value;
};
第一个表达式只是第二个表达式的一个简短形式。lambda 最好只包含一行代码。但如果 lambda 表达式包含多行,则定义它的唯一方法是使用第二种方法,如下所示:
Mapper<Integer, Integer> square = (value) -> {
System.out.println("Calculating the square of " + value);
return value * value;
};
实际上,lambda 表达式不仅仅是匿名内部类的语法糖。它们的实现是为了在Java 虚拟机(JVM中快速执行,因此如果您的代码设计为仅与 Java 8+兼容,那么您一定要使用它们。他们的主要思想是以与数据传递相同的方式传递行为。这使您的程序更具可读性。
与新语法相关的最后一件事是能够传递给方法并分配给已经定义的函数和方法的变量。让我们定义一个新的功能接口:
interface Action<V> {
void act(V value);
}
我们可以使用它对列表中的每个值执行任意操作;例如,记录列表。以下是使用此接口的方法:
public static <V> void act(List<V> list, Action<V> action) {
for (V v : list) {
action.act(v);
}
}
此方法类似于map()
函数。它遍历列表,并对每个元素调用传递的操作的act()
方法。让我们使用 lambda 调用它,它只记录列表中的元素:
act(list, value -> System.out.println(value));
这非常简单,但不需要,因为println()
方法可以传递给act()
方法。这是按照进行的,如下所示:
act(list, System.out::println);
这在 Java8 中是有效的语法——每个方法都可以成为 lambda,可以分配给变量或传递给方法。所有这些都是有效的:
现在我们已经揭示了 lambda 语法,我们将在 RxJava 示例中使用它,而不是匿名内部类。
Java8 附带了一个特殊的包,其中包含用于常见情况的功能接口。这个包是java.util.function
,在本书中我们不会详细介绍,但会介绍其中一些值得一提的内容:
Consumer<T>
:表示接受参数且不返回任何内容的函数。其抽象方法为void accept(T)
。例如,我们可以使用它将System.out::println
方法分配给变量,如下所示:
Consumer<String> print = System.out::println;
Function<T,R>
:表示一个函数,该函数接受给定类型的一个参数,并返回任意类型的结果。其抽象方法为R accept(T)
,可用于制图。我们根本不需要Mapper
接口!让我们来看看下面的代码片段:
Predicate<T>
:表示只有一个参数返回布尔结果的函数。其抽象方法为boolean test(T)
,可用于滤波。让我们来看看下面的代码:
有很多类似的功能接口;例如,具有两个参数的函数或二进制运算符。这也是一个有两个参数的函数,但都是相同类型的,并且返回的结果也是相同类型的。它们帮助我们在代码中重用 lambda。
好的是 RxJava 与 lambda 兼容。这意味着我们传递给subscribe
方法的操作实际上是功能接口!
RxJava 的功能接口在rx.functions
包中。它们都扩展了一个基本的标记**接口**(没有方法的接口,用于类型检查),称为Function
。此外,还有另一个标记接口,扩展了Function
接口,称为Action
。它用于标记使用者(函数,不返回任何内容)。
RxJava 有十一个Action
接口:
Action0 // Action with no parameters
Action1<T1> // Action with one parameter
Action2<T1,T2> // Action with two parameters
Action9<T1,T2,T3,T4,T5,T6,T7,T8,T9> // Action with nine parameters
ActionN // Action with arbitrary number of parameters
主要用于订阅(Action1
和Action0
)。我们在第 1 章中看到的Observable.OnSubscribe<T>
参数是反应式编程的介绍(用于创建自定义可观察对象)也扩展了Action
接口。
类似地,有十一个Function
扩展器代表函数返回结果。它们是Func0<R>
、Func1<T1, R>
、Func9<T1,T2,T3,T4,T5,T6,T7,T8,T9,R>
和FuncN<R>
。它们用于映射、过滤、组合和许多其他目的。
RxJava 中的每个操作符和 subscribe 方法都适用于这些接口中的一个或多个。这意味着在 RxJava 中,几乎所有地方都可以使用 lambda 表达式而不是匿名内部类。从这一点开始,我们的所有示例都将使用 lambdas,以便更具可读性和功能性。
现在,让我们看一个用 lambdas 实现的大型 RxJava 示例。这是我们熟悉的无功和示例!
所以这一次,我们的主要代码将与前一段非常相似:
ConnectableObservable<String> input = CreateObservable.from(System.in);
Observable<Double> a = varStream("a", input);
Observable<Double> b = varStream("b", input);
reactiveSum(a, b); // The difference
input.connect();
唯一的区别是,我们将在计算和时采用更具功能性的方法,而不是保持相同的状态。我们不会实现Observer
接口;相反,我们将通过 lambdas 进行订阅。这个解决方案更干净。
CreateObservable.from(InputStream)
方法与我们之前使用的方法非常相似。我们将跳过它,查看Observable<Double> varStream(String, Observable<String>)
方法,该方法创建表示收集器的Observable
实例:
public static Observable<Double> varStream(
final String name, Observable<String> input) {
final Pattern pattern = Pattern.compile(
"\\s*" + name + "\\s*[:|=]\\s*(-?\\d+\\.?\\d*)$"
);
return input
.map(pattern::matcher) // (1)
.filter(m -> m.matches() && m.group(1) != null) // (2)
.map(matcher -> matcher.group(1)) // (3)
.map(Double::parseDouble); // (4)
}
)
这种方法比以前使用的短得多,而且看起来更简单。但在语义上,它是相同的。它创建一个Observable
实例,该实例连接到一个可观察的源,生成任意字符串,如果字符串的格式符合它的预期,它将从中提取一个双倍数字并发出该数字。负责检查输入格式和提取数字的逻辑只有四行,由简单的 lambda 表示。让我们来研究一下:
matcher
实例。filter()
方法,只过滤格式正确的输入。map()
操作符,我们从matcher
实例创建一个字符串,其中只包含我们需要的数字数据。map()
操作符,字符串被转换成一个双精度数字。对于新的void reactiveSum(Observable<Double>, Observable<Double>)
方法的实现,使用以下代码:
public static void reactiveSum(
Observable<Double> a,
Observable<Double> b) {
Observable
.combineLatest(a, b, (x, y) -> x + y) // (1)
.subscribe( // (2)
sum -> System.out.println("update : a + b = " + sum),
error -> {
System.out.println("Got an error!");
error.printStackTrace();
},
() -> System.out.println("Exiting...")
);
}
让我们看看下面的代码:
同样,我们使用combineLatest()
方法,但这次第三个参数是一个简单的 lambda,它实现了一个求和。
subscribe()
方法取三个 lambda 表达式,当以下事件发生时触发:
使用 lambdas,一切都变得简单了。查看前面的程序,我们可以看到大多数逻辑都是由小的、独立的函数组成的,这些函数使用其他函数链接在一起。这就是我们所说的功能性,即使用小型可重用函数来表达我们的程序,这些函数接受其他函数并返回函数和数据抽象,使用函数链转换输入数据,以产生想要的结果。但让我们深入了解一下这些函数。
你不必记住本章介绍的大部分术语;重要的是要理解它们是如何帮助我们编写简单但功能强大的程序的。
RxJava 的方法包含了许多功能性思想,因此,学习如何以更功能性的方式思考,以便编写更好的反应式应用,对我们来说非常重要。
纯函数是一个返回值仅由其输入决定的函数,没有可观察的副作用。如果我们用相同的参数n次调用它,我们每次都会得到相同的结果。例如:
Predicate<Integer> even = (number) -> number % 2 == 0;
int i = 50;
while((i--) > 0) {
System.out.println("Is five even? - " + even.test(5));
}
偶数函数每次返回False
,因为它只依赖于它的输入,而输入每次都是相同的,不是偶数。
纯函数的这个性质称为幂等性。幂等函数不依赖于时间,因此它们可以将连续数据视为无限数据流。这就是不断变化的数据在 RxJava(Observable
实例)中的表示方式。
注意,在这里,术语“幂等性”用于其计算机科学含义。在计算中,幂等运算是指如果使用相同的输入参数多次调用,则不会产生额外影响的运算;在数学中,幂等运算是满足以下表达式的运算:f(f(x))=f(x)。
纯功能不会产生副作用。例如:
Predicate<Integer> impureEven = (number) -> {
System.out.println("Printing here is side effect!");
return number % 2 == 0;
};
此函数不是纯函数,因为每次调用它时,它都会在输出上打印一条消息。所以它做了两件事:测试数字是否为偶数,并输出一条消息作为副作用。副作用是函数可以产生的任何可能的可观察输出,例如,触发事件、抛出异常和 I/O,与返回值不同。副作用还会改变共享状态或可变参数。
想想看。如果大部分程序由纯函数组成,则很容易扩展并并行运行部分程序,因为纯函数不会相互冲突,也不会更改共享状态。
本节中另一件值得一提的事情是不变性。不可变对象是不能更改其状态的对象。Java 中的String
类就是一个很好的例子。String
实例不可更改;甚至像substring
这样的方法也会创建String
的新实例,而不修改调用实例。
如果我们将不可变数据传递给纯函数,我们可以确保每次使用此数据调用它时,它都返回相同的数据。对于可变对象,在编写并行程序时并不完全相同,因为一个线程可以改变对象的状态。在这种情况下,如果调用 pure 函数,它将返回不同的结果,因此不再是幂等函数。
如果我们将数据存储在不可变对象中,并使用纯函数对其进行操作,在过程中创建新的不可变对象,我们将不会遇到意外的并发问题。没有全局状态和可变状态;一切都将是简单和可预测的。
使用不可变对象是很棘手的;使用它们的每个操作都会创建新实例,这可能会消耗内存。但是有一些方法可以避免这种情况;例如,尽可能多地重用源不可变对象,或使不可变对象的生命周期尽可能短(因为短生命周期对象对 GC 或缓存很友好)。功能程序应该设计为使用不可变的无状态数据。
复杂的程序不能只由纯函数组成,但只要有可能,就最好使用它们。在本章对无功和的实现中,我们只传递了map()
、filter()
和combineLatest()
纯函数。
说到map()
和filter()
函数,我们称它们为高阶函数。
具有至少一个 function 类型参数的函数或返回函数的函数称为高阶函数。当然,高阶函数可以是纯。
以下是采用函数参数的高级函数的示例:
public static <T, R> int highSum(
Function<T, Integer> f1,
Function<R, Integer> f2,
T data1,
R data2) {
return f1.apply(data1) + f2.apply(data2);
}
)
它需要两个类型为T -> int/R -> int
的函数和一些数据来调用它们并对它们的结果进行求和。例如,我们可以这样做:
highSum(v -> v * v, v -> v * v * v, 3, 2);
这里我们求三的平方和二的立方之和。
但高阶函数的思想是灵活的。例如,我们可以将highSum()
函数用于完全不同的用途,比如对字符串求和,如下所示:
Function<String, Integer> strToInt = s -> Integer.parseInt(s);
highSum(strToInt, strToInt, "4", "5");
因此,可以使用高阶函数将相同的行为应用于不同类型的输入。
如果我们传递给highSum()
函数的前两个参数是纯函数,那么它也将是纯函数。strToInt
参数是一个纯函数,如果调用highSum(strToInt, strToInt, "4", "5")
方法n次,返回的结果相同,不会有副作用。
下面是高阶函数的另一个示例:
public static Function<String, String> greet(String greeting) {
return (String name) -> greeting + " " + name + "!";
}
这是一个返回另一个函数的函数。它可以这样使用:
System.out.println(greet("Hello").apply("world"));
// Prints 'Hellow world!'
System.out.println(greet("Goodbye").apply("cruel world"));
// Prints 'Goodbye cruel world!'
Function<String, String> howdy = greet("Howdy");
System.out.println(howdy.apply("Tanya"));
System.out.println(howdy.apply("Dali"));
// These two print 'Howdy Tanya!' and 'Howdy Dali'
这样的函数可以用于实现具有共同点的不同行为。在面向对象编程中,我们定义类,然后扩展它们,从而重载它们的方法。在函数式编程中,我们将高阶函数定义为接口,并使用不同的参数调用它们,从而产生不同的行为。
这些功能是一等公民;我们可以只使用函数对逻辑进行编码、链接、处理数据、转换、过滤或将其累积为结果。
纯函数和高阶函数等函数概念对 RxJava 非常重要。RxJava 的Observable
类是流畅接口的实现。这意味着它的大多数实例方法返回一个Observable
实例。例如:
Observable mapped = observable.map(someFunction);
map()
操作符返回一个新的Observable
实例,并发送它转换的数据。像map()
这样的运算符显然是高阶函数,我们可以将其他函数传递给它们。因此,一个典型的 RxJava 程序由一系列操作符表示,这些操作符链接到一个Observable
实例,多个订户可以订阅该实例。这些链接在一起的功能可以受益于本章所涵盖的主题。我们可以将 lambda 传递给它们,而不是匿名接口实现(正如我们在反应式和的第二个实现中所看到的),并且我们应该尽可能尝试使用不可变数据和纯函数。这样,我们的代码将简单而安全。
在本章中,我们了解了一些函数式编程原则和术语。我们已经学习了如何编写由小的纯函数操作组成的程序,这些操作使用高阶函数链接在一起。
随着函数式编程越来越流行,在不久的将来,对精通函数式编程的开发人员的需求将越来越高。这是因为它帮助我们轻松实现可伸缩性和并行性。更重要的是,如果我们把被动的想法加入其中,它会变得更加吸引人。
这就是为什么我们将在下一章深入研究 RxJava 框架,学习如何使用它为我们带来好处。我们将从Observable
实例创建技术开始。这将为我们提供从所有内容创建Observable
实例的技能,从而将几乎所有内容转变为功能性反应程序。