1. Overview
1.概述
In this tutorial, we’ll understand the functional programming paradigm’s core principles and how to practice them in the Java programming language.
在本教程中,我们将了解函数式编程范式的核心原则以及如何在Java编程语言中实践它们。
We’ll also cover some of the advanced functional programming techniques.
我们还将介绍一些高级函数式编程技术。
This will help us evaluate the benefits we get from functional programming, especially in Java.
这将有助于我们评估我们从函数式编程中得到的好处,特别是在Java中。
2. What Is Functional Programming?
2.什么是函数式编程?
Basically, functional programming is a style of writing computer programs that treat computations as evaluating mathematical functions.
基本上,函数式编程是一种编写计算机程序的风格,将计算视为评估数学函数。
In mathematics, a function is an expression that relates an input set to an output set.
在数学中,函数是一个表达式,它将一个输入集与一个输出集联系起来。
Importantly, the output of a function depends only on its input. More interestingly, we can compose two or more functions together to get a new function.
重要的是,一个函数的输出只取决于其输入。更有趣的是,我们可以将两个或多个函数组合在一起,得到一个新的函数。
2.1. Lambda Calculus
2.1.兰姆达微积分
To understand why these definitions and properties of mathematical functions are important in programming, we’ll have to go back in time a bit.
要理解为什么这些数学函数的定义和属性在编程中很重要,我们必须回溯一下时间。
In the 1930s, mathematician Alonzo Church developed a formal system to express computations based on function abstraction. This universal model of computation came to be known as lambda calculus.
在20世纪30年代,数学家Alonzo Church开发了一个基于函数抽象的正式系统来表达计算。这个通用的计算模型后来被称为lambda calculus。
Lambda calculus had a tremendous impact on developing the theory of programming languages, particularly functional programming languages. Typically, functional programming languages implement lambda calculus.
兰姆达微积分对发展编程语言的理论产生了巨大的影响,特别是函数式编程语言。通常情况下,函数式编程语言实现了兰姆达微积分。
Since lambda calculus focuses on function composition, functional programming languages provide expressive ways to compose software in function composition.
由于lambda calculus专注于函数组合,函数式编程语言提供了函数组合软件的表达方式。
2.2. Categorization of Programming Paradigms
2.2.编程范式的分类
Of course, functional programming is not the only programming style in practice. Broadly speaking, programming styles can be categorized into imperative and declarative programming paradigms.
当然,函数式编程不是实践中唯一的编程风格。广义上讲,编程风格可以分为命令式和声明式编程范式。
The imperative approach defines a program as a sequence of statements that change the program’s state until it reaches the final state.
迭代法将程序定义为改变程序状态的语句序列,直到它达到最终状态。
Procedural programming is a type of imperative programming where we construct programs using procedures or subroutines. One of the popular programming paradigms known as Object-Oriented Programming (OOP) extends procedural programming concepts.
程序性编程是一种命令式编程,我们使用程序或子程序来构建程序。流行的编程范式之一被称为面向对象编程(OOP),它扩展了程序性编程的概念。
In contrast, the declarative approach expresses the logic of a computation without describing its control flow in terms of a sequence of statements.
相比之下,声明式方法表达了计算的逻辑,而没有用语句序列来描述其控制流。
Simply put, the declarative approach’s focus is to define what the program has to achieve rather than how it should achieve it. Functional programming is a subset of the declarative programming languages.
简单地说,声明式方法的重点是定义程序必须实现的目标,而不是应该如何实现它。函数式编程是声明式编程语言的一个子集。
These categories have further subcategories, and the taxonomy gets quite complex, but we won’t get into that for this tutorial.
这些类别还有更多的子类别,分类法变得相当复杂,但我们在本教程中不会涉及这些。
2.3. Categorization of Programming Languages
2.3.程序设计语言的分类
Now we’ll try to understand how programming languages are divided based on their support for functional programming for our purposes.
现在我们将尝试了解编程语言是如何根据其对函数式编程的支持来划分的,以达到我们的目的。
Pure functional languages, such as Haskell, only allow pure functional programs.
纯功能语言,如Haskell,只允许纯功能程序。
Other languages allow both functional and procedural programs and are considered impure functional languages. Many languages fall into this category, including Scala, Kotlin and Java.
其他语言允许函数式程序和程序式程序,被认为是不纯函数式语言。许多语言都属于这个类别,包括Scala、Kotlin和Java。
It’s important to understand that most of the popular programming languages today are general-purpose languages, so they tend to support multiple programming paradigms.
重要的是要明白,今天大多数流行的编程语言都是通用语言,所以它们往往支持多种编程范式。
3. Fundamental Principles and Concepts
3.基本原则和概念
This section will cover some of the basic principles of functional programming and how to adopt them in Java.
本节将介绍函数式编程的一些基本原则以及如何在Java中采用这些原则。
Please note that many features we’ll be using haven’t always been part of Java, and it’s advisable to be on Java 8 or later to exercise functional programming effectively.
请注意,我们将使用的许多功能并不总是Java的一部分,建议使用Java 8或更高版本,以有效行使函数式编程。
3.1. First-Class and Higher-Order Functions
3.1.一级和高阶函数
A programming language is said to have first-class functions if it treats functions as first-class citizens.
如果一种编程语言将函数视为一等公民,就可以说它有一等函数。
This means that functions are allowed to support all operations typically available to other entities. These include assigning functions to variables, passing them as arguments to other functions and returning them as values from other functions.
这意味着函数被允许支持通常可用于其他实体的所有操作。这些包括将函数分配给变量,将其作为参数传递给其他函数,以及将其作为值从其他函数返回。
This property makes it possible to define higher-order functions in functional programming. Higher-order functions are capable of receiving functions as arguments and returning a function as a result. This further enables several techniques in functional programming such as function composition and currying.
这一属性使我们有可能在函数式编程中定义高阶函数。高阶函数能够接收函数作为参数,并返回一个函数作为结果。这进一步实现了函数式编程中的一些技术,如函数组合和currying。
Traditionally, it was only possible to pass functions in Java using constructs such as functional interfaces or anonymous inner classes. Functional interfaces have exactly one abstract method and are also known as Single Abstract Method (SAM) interfaces.
传统上,在Java中只能使用功能接口或匿名内类等结构来传递函数。功能性接口正好有一个抽象方法,也被称为单一抽象方法(SAM)接口。
Let’s say we have to provide a custom comparator to Collections.sort method:
假设我们要为Collections.sort方法提供一个自定义比较器。
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer n1, Integer n2) {
return n1.compareTo(n2);
}
});
As we can see, this is a tedious and verbose technique — certainly not something that encourages developers to adopt functional programming.
正如我们所看到的,这是一个繁琐和冗长的技术–当然不是鼓励开发者采用函数式编程的东西。
Fortunately, Java 8 brought many new features to ease the process, such as lambda expressions, method references and predefined functional interfaces.
幸运的是,Java 8带来了许多新功能,以缓解这一过程,如lambda表达式、方法引用和预定义功能接口。
Let’s see how a lambda expression can help us with the same task:
让我们看看lambda表达式如何帮助我们完成同样的任务。
Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));
This is definitely more concise and understandable.
这绝对是更简洁、更容易理解的。
However, please note that while this may give us the impression of using functions as first-class citizens in Java, that’s not the case.
然而,请注意,虽然这可能给我们的印象是在Java中把函数作为一等公民使用,但事实并非如此。
Behind the syntactic sugar of lambda expressions, Java still wraps these into functional interfaces. So, Java treats a lambda expression as an Object, which is the true first-class citizen in Java.
在lambda表达式的语法糖背后,Java仍然将这些表达式包装成功能接口。因此,Java将lambda表达式视为Object,它是Java中真正的第一等公民。
3.2. Pure Functions
3.2.纯函数
The definition of pure function emphasizes that a pure function should return a value based only on the arguments and should have no side effects.
纯函数的定义强调,纯函数应该只基于参数返回一个值,并且应该没有副作用。
This can sound quite contrary to all the best practices in Java.
这听起来可能与Java中的所有最佳实践相悖。
As an object-oriented language, Java recommends encapsulation as a core programming practice. It encourages hiding an object’s internal state and exposing only necessary methods to access and modify it. So, these methods aren’t strictly pure functions.
作为一种面向对象的语言,Java推荐将封装作为一种核心编程实践。它鼓励隐藏一个对象的内部状态,只公开必要的方法来访问和修改它。所以,这些方法并不是严格意义上的纯函数。
Of course, encapsulation and other object-oriented principles are only recommendations and not binding in Java.
当然,封装和其他面向对象的原则只是建议,在Java中没有约束力。
In fact, developers have recently started to realize the value of defining immutable states and methods without side effects.
事实上,开发人员最近已经开始意识到定义不可变的状态和方法的价值,没有副作用。
Let’s say we want to find the sum of all the numbers we’ve just sorted:
比方说,我们想找到我们刚刚排序的所有数字的总和。
Integer sum(List<Integer> numbers) {
return numbers.stream().collect(Collectors.summingInt(Integer::intValue));
}
This method depends only on the arguments it receives, so it’s deterministic. Moreover, it doesn’t produce any side effects.
这个方法只依赖于它收到的参数,所以它是确定性的。此外,它不会产生任何副作用。
Side effects can be anything apart from the intended behavior of the method. For instance, side effects can be as simple as updating a local or global state or saving to a database before returning a value. (Purists also treat logging as a side effect.)
副作用可以是方法的预期行为之外的任何东西。例如,副作用可以简单到更新本地或全局状态或在返回值之前保存到数据库。(纯粹主义者也将记录视为一种副作用)。
So, let’s look at how we deal with legitimate side effects. For instance, we may need to save the result in a database for genuine reasons. There are techniques in functional programming to handle side effects while retaining pure functions.
因此,让我们来看看我们如何处理合法的副作用。例如,我们可能因为真正的原因需要将结果保存在数据库中。在函数式编程中,有一些技术可以处理副作用,同时保留纯函数。
We’ll discuss some of them in later sections.
我们将在后面的章节中讨论其中一些。
3.3. Immutability
3.3.不变性
Immutability is one of the core principles of functional programming, and it refers to the property that an entity can’t be modified after being instantiated.
不变性是函数式编程的核心原则之一,它指的是实体在被实例化后不能被修改的属性。
In a functional programming language, this is supported by design at the language level. But in Java we have to make our own decision to create immutable data structures.
在函数式编程语言中,这是由语言层面的设计支持的。但在Java中,我们必须自己决定创建不可变的数据结构。
Please note that Java itself provides several built-in immutable types, for instance, String. This is primarily for security reasons because we heavily use String in class loading and as keys in hash-based data structures. There are also several other built-in immutable types such as primitive wrappers and math types.
请注意,Java本身提供了几个内置的不可变类型,例如,String。这主要是出于安全考虑,因为我们在类加载中大量使用String,并在基于哈希的数据结构中作为键。还有其他一些内置的不可更改的类型,如原始包装器和数学类型。
But what about the data structures we create in Java? Of course, they are not immutable by default, and we have to make a few changes to achieve immutability.
但是我们在Java中创建的数据结构呢?当然,它们在默认情况下不是不可变的,我们必须做一些改变来实现不可变。
The use of the final keyword is one of them, but it doesn’t stop there:
使用final关键词是其中之一,但它并没有就此停止。
public class ImmutableData {
private final String someData;
private final AnotherImmutableData anotherImmutableData;
public ImmutableData(final String someData, final AnotherImmutableData anotherImmutableData) {
this.someData = someData;
this.anotherImmutableData = anotherImmutableData;
}
public String getSomeData() {
return someData;
}
public AnotherImmutableData getAnotherImmutableData() {
return anotherImmutableData;
}
}
public class AnotherImmutableData {
private final Integer someOtherData;
public AnotherImmutableData(final Integer someData) {
this.someOtherData = someData;
}
public Integer getSomeOtherData() {
return someOtherData;
}
}
Note that we have to diligently observe a few rules:
请注意,我们必须勤奋地遵守一些规则。
- All fields of an immutable data structure must be immutable.
- This must apply to all the nested types and collections (including what they contain) as well.
- There should be one or more constructors for initialization as needed.
- There should only be accessor methods, possibly with no side effects.
It’s not easy to get it completely right every time, especially when the data structures start to get complex.
要每次都完全正确并不容易,特别是当数据结构开始变得复杂时。
However, several external libraries can make working with immutable data in Java easier. For instance, Immutables and Project Lombok provide ready-to-use frameworks for defining immutable data structures in Java.
然而,一些外部库可以使在Java中处理不可变数据变得更加容易。例如,Immutables和Project Lombok为在Java中定义不可变数据结构提供了即用框架。
3.4. Referential Transparency
3.4.参考文献的透明性
Referential transparency is perhaps one of the more difficult principles of functional programming to understand, but the concept is pretty simple.
参考透明度也许是函数式编程中比较难理解的原则之一,但这个概念是非常简单的。
We call an expression referentially transparent if replacing it with its corresponding value has no impact on the program’s behavior.
如果将表达式替换为其相应的值不会对程序的行为产生影响,我们称该表达式为参考透明。
This enables some powerful techniques in functional programming such as higher-order functions and lazy evaluation.
这使得函数式编程中的一些强大技术得以实现,如高阶函数和懒惰评估。
To understand this better, let’s take an example:
为了更好地理解这一点,让我们举一个例子。
public class SimpleData {
private Logger logger = Logger.getGlobal();
private String data;
public String getData() {
logger.log(Level.INFO, "Get data called for SimpleData");
return data;
}
public SimpleData setData(String data) {
logger.log(Level.INFO, "Set data called for SimpleData");
this.data = data;
return this;
}
}
This is a typical POJO class in Java, but we’re interested in finding if this provides referential transparency.
这是Java中一个典型的POJO类,但我们有兴趣发现这是否提供了参考的透明度。
Let’s observe the following statements:
让我们观察一下下面的说法。
String data = new SimpleData().setData("Baeldung").getData();
logger.log(Level.INFO, new SimpleData().setData("Baeldung").getData());
logger.log(Level.INFO, data);
logger.log(Level.INFO, "Baeldung");
The three calls to logger are semantically equivalent but not referentially transparent.
对logger的三次调用在语义上是等同的,但在指代上并不透明。
The first call is not referentially transparent since it produces a side effect. If we replace this call with its value as in the third call, we’ll miss the logs.
第一个调用在参考上是不透明的,因为它产生了一个副作用。如果我们像第三次调用那样用它的值来替换这个调用,我们会错过日志。
The second call is also not referentially transparent since SimpleData is mutable. A call to data.setData anywhere in the program would make it difficult for it to be replaced with its value.
第二个调用也不是参考透明的,因为SimpleData是可变的。在程序的任何地方调用data.setData都会使它难以被替换成它的值。
So, for referential transparency, we need our functions to be pure and immutable. These are the two preconditions we discussed earlier.
因此,为了实现参照性透明,我们需要我们的函数是纯粹的和不可变的。这就是我们之前讨论的两个前提条件。
As an interesting outcome of referential transparency, we produce context-free code. In other words, we can run them in any order and context, which leads to different optimization possibilities.
作为指代透明度的一个有趣的结果,我们产生了无上下文的代码。换句话说,我们可以以任何顺序和语境运行它们,这导致了不同的优化可能性。
4. Functional Programming Techniques
4.功能性编程技术
The functional programming principles that we discussed earlier enable us to use several techniques to benefit from functional programming.
我们前面讨论的函数式编程原则使我们能够使用几种技术来从函数式编程中获益。
In this section, we’ll cover some of these popular techniques and understand how we can implement them in Java.
在本节中,我们将介绍其中一些流行的技术,并了解我们如何在Java中实现它们。
4.1. Function Composition
4.1.功能构成
Function composition refers to composing complex functions by combining simpler functions.
函数组合指的是通过组合较简单的函数来构成复杂的函数。。
This is primarily achieved in Java using functional interfaces, which are target types for lambda expressions and method references.
这主要是在Java中使用功能接口实现的,功能接口是lambda表达式和方法引用的目标类型。
Typically, any interface with a single abstract method can serve as a functional interface. So, we can define a functional interface quite easily.
通常,任何具有单一抽象方法的接口都可以作为一个功能接口。因此,我们可以很容易地定义一个功能接口。
However, Java 8 provides us many functional interfaces by default for different use cases under the package java.util.function.
然而,Java 8在java.util.function包下默认为我们提供了许多函数接口,用于不同的使用情况。
Many of these functional interfaces provide support for function composition in terms of default and static methods. Let’s pick the Function interface to understand this better.
许多功能接口以default和static方法的形式为函数组合提供支持。让我们挑选Function接口来更好地理解这一点。
Function is a simple and generic functional interface that accepts one argument and produces a result.
Function是一个简单而通用的函数接口,它接受一个参数并产生一个结果。
It also provides two default methods, compose and andThen, which will help us in function composition:
它还提供了两个默认方法,compose和andThen,这将帮助我们进行函数组合。
Function<Double, Double> log = (value) -> Math.log(value);
Function<Double, Double> sqrt = (value) -> Math.sqrt(value);
Function<Double, Double> logThenSqrt = sqrt.compose(log);
logger.log(Level.INFO, String.valueOf(logThenSqrt.apply(3.14)));
// Output: 1.06
Function<Double, Double> sqrtThenLog = sqrt.andThen(log);
logger.log(Level.INFO, String.valueOf(sqrtThenLog.apply(3.14)));
// Output: 0.57
Both these methods allow us to compose multiple functions into a single function but offer different semantics. While compose applies the function passed in the argument first and then the function on which it’s invoked, andThen does the same in reverse.
这两种方法都允许我们将多个函数合成为一个函数,但提供了不同的语义。compose先应用参数中传递的函数,然后再应用它所调用的函数,而andThen则以相反的方式进行。
Several other functional interfaces have interesting methods to use in function composition, such as the default methods and, or and negate in the Predicate interface. While these functional interfaces accept a single argument, there are two-arity specializations, such as BiFunction and BiPredicate.
其他几个函数式接口也有有趣的方法可用于函数的组成,例如Predicate接口中的默认方法and、or和negate。虽然这些函数式接口只接受一个参数,但也有双属性的特殊化,如BiFunction和BiPredicate。
4.2. Monads
4.2 单体
Many of the functional programming concepts derive from Category Theory, which is a general theory of functions in mathematics. It presents several concepts of categories such as functors and natural transformations.
许多函数式编程的概念源于类别理论,它是数学中关于函数的一般理论。它提出了类别的几个概念,如漏斗和自然变换。
For us, it’s only important to know that this is the basis of using monads in functional programming.
对我们来说,只需要知道这是在函数式编程中使用单体的基础。
Formally, a monad is an abstraction that allows structuring programs generically. So, a monad allows us to wrap a value, apply a set of transformations, and get the value back with all transformations applied.
从形式上看,单体是一种抽象,它允许通用地构建程序。因此,单体允许我们包裹一个值,应用一组转换,并在应用了所有转换后取回该值。
Of course, there are three laws that any monad needs to follow — left identity, right identity and associativity — but we won’t get into the details here.
当然,任何单体都需要遵循三条定律–左侧同一性、右侧同一性和关联性–但我们不会在这里讨论细节。
In Java, there are a few monads that we use quite often, such as Optional and Stream:
在Java中,有几个单体是我们经常使用的,比如Optional和Stream。
Optional.of(2).flatMap(f -> Optional.of(3).flatMap(s -> Optional.of(f + s)))
Why do we call Optional a monad?
为什么我们称Optional为单体?
Here Optional allows us to wrap a value using the method of and apply a series of transformations. We’re applying the transformation of adding another wrapped value using the method flatMap.
这里的Optional允许我们使用of方法包装一个值,并应用一系列的转换。我们正在应用使用flatMap方法添加另一个包裹的值的转换。
We could show that Optional follows the three laws of monads. However, an Optional does break the monad laws under some circumstances. But it should be good enough for us for most practical situations.
我们可以证明,Optional遵循单体的三个定律。然而,在某些情况下,Optional确实会破坏单体定律。但对于我们来说,在大多数实际情况下,它应该是足够好的。
If we understand monads’ basics, we’ll soon realize that there are many other examples in Java, such as Stream and CompletableFuture. They help us achieve different objectives, but they all have a standard composition in which context manipulation or transformation is handled.
如果我们了解了monads的基础知识,我们很快就会意识到,Java中还有很多其他的例子,比如Stream和CompletableFuture。它们帮助我们实现不同的目标,但它们都有一个标准的组合,在其中处理上下文操作或转换。
Of course, we can define our own monad types in Java to achieve different objectives such as log monad, report monad or audit monad. For example, the monad is one of the functional programming techniques to handle side effects in functional programming.
当然,我们可以在Java中定义自己的单体类型来实现不同的目标,如日志单体、报告单体或审计单体。例如,单体是函数式编程技术之一,用于处理函数式编程中的副作用。
4.3. Currying
4.3.咖喱
Currying is a mathematical technique of converting a function that takes multiple arguments into a sequence of functions that take a single argument.
Currying是一种数学技术,将一个需要多个参数的函数转换为一连串需要一个参数的函数。
In functional programming, it gives us a powerful composition technique where we don’t need to call a function with all its arguments.
在函数式编程中,它给了我们一个强大的组合技术,我们不需要用所有的参数来调用一个函数。
Moreover, a curried function does not realize its effect until it receives all the arguments.
此外,一个curried函数在收到所有参数之前不会实现其效果。
In pure functional programming languages such as Haskell, currying is well supported. In fact, all functions are curried by default.
在纯函数式编程语言中,如Haskell,currying得到了很好的支持。事实上,所有的函数都是默认固化的。
However, in Java it’s not that straightforward:
然而,在Java中,这并不是那么简单的事情。
Function<Double, Function<Double, Double>> weight = mass -> gravity -> mass * gravity;
Function<Double, Double> weightOnEarth = weight.apply(9.81);
logger.log(Level.INFO, "My weight on Earth: " + weightOnEarth.apply(60.0));
Function<Double, Double> weightOnMars = weight.apply(3.75);
logger.log(Level.INFO, "My weight on Mars: " + weightOnMars.apply(60.0));
Here we’ve defined a function to calculate our weight on a planet. While our mass remains the same, gravity varies by the planet we’re on.
这里我们定义了一个函数来计算我们在一个星球上的重量。虽然我们的质量保持不变,但重力因我们所处的星球而异。
We can partially apply the function by passing just the gravity to define a function for a specific planet. Moreover, we can pass this partially applied function around as an argument or return value for arbitrary composition.
我们可以部分应用这个函数,只传递重力来定义一个特定星球的函数。此外,我们可以将这个部分应用的函数作为参数或返回值传递给任意的组成。
Currying depends on the language to provide two fundamental features: lambda expressions and closures. Lambda expressions are anonymous functions that help us to treat code as data. We’ve seen earlier how to implement them using functional interfaces.
Currying 依赖于语言提供的两个基本特性:Lambda表达式和闭包。Lambda表达式是匿名函数,帮助我们将代码视为数据。我们之前已经看到了如何使用函数式接口来实现它们。
A lambda expression may close upon its lexical scope, which we define as its closure.
一个lambda表达式可以在其词法范围内关闭,我们将其定义为关闭。
Let’s see an example:
让我们看一个例子。
private static Function<Double, Double> weightOnEarth() {
final double gravity = 9.81;
return mass -> mass * gravity;
}
Please note how the lambda expression, which we return in the method above, depends on the enclosing variable, which we call closure. Unlike other functional programming languages, Java has a limitation that the enclosing scope has to be final or effectively final.
请注意,我们在上面的方法中返回的lambda表达式是如何依赖于包围变量的,我们称之为封闭性。与其他函数式编程语言不同,Java有一个限制,即封闭范围必须是最终或有效最终。
As an interesting outcome, currying also allows us to create a functional interface in Java of arbitrary arity.
作为一个有趣的结果,currying还允许我们在Java中创建一个任意算数的函数接口。
4.4. Recursion
4.4.递归
Recursion is another powerful technique in functional programming that allows us to break down a problem into smaller pieces. The main benefit of recursion is that it helps us eliminate the side effects, which is typical of any imperative style looping.
递归是函数式编程中另一种强大的技术,允许我们将问题分解成更小的片段。递归的主要好处是,它可以帮助我们消除副作用,这是任何命令式循环的典型现象。
Let’s see how we calculate the factorial of a number using recursion:
让我们看看如何利用递归计算一个数字的阶乘。
Integer factorial(Integer number) {
return (number == 1) ? 1 : number * factorial(number - 1);
}
Here we call the same function recursively until we reach the base case and then start to calculate our result.
在这里,我们递归地调用同一个函数,直到我们达到基本情况,然后开始计算我们的结果。
Notice that we’re making the recursive call before calculating the result at each step or in words at the head of the calculation. So, this style of recursion is also known as head recursion.
请注意,我们是在每一步计算结果之前进行递归调用的,或者说是在计算的头部进行的。所以,这种递归方式也被称为头部递归。。
A drawback of this type of recursion is that every step has to hold the state of all previous steps until we reach the base case. This is not really a problem for small numbers, but holding the state for large numbers can be inefficient.
这种递归方式的缺点是,每一步都必须保持之前所有步骤的状态,直到我们到达基数。这对小数字来说并不是一个问题,但对大数字来说,保持状态可能是低效的。
A solution is a slightly different implementation of the recursion known as tail recursion. Here we ensure that the recursive call is the last call a function makes.
一个解决方案是略微不同的递归实现,称为尾部递归。这里我们确保递归调用是一个函数的最后一次调用。
Let’s see how we can rewrite the above function to use tail recursion:
让我们看看如何重写上述函数以使用尾部递归。
Integer factorial(Integer number, Integer result) {
return (number == 1) ? result : factorial(number - 1, result * number);
}
Notice the use of an accumulator in the function, eliminating the need to hold the state at every step of recursion. The real benefit of this style is to leverage compiler optimizations where the compiler can decide to let go of the current function’s stack frame, a technique known as tail-call elimination.
注意在函数中使用了一个累加器,消除了在递归的每一步中保持状态的需要。这种风格的真正好处是利用了编译器的优化,编译器可以决定放开当前函数的堆栈框架,这种技术被称为尾调用消除。
While many languages such as Scala support tail-call elimination, Java still does not have support for this. This is part of the backlog for Java and will perhaps come in some shape as part of larger changes proposed under Project Loom.
虽然许多语言(如Scala)支持消除尾随调用,但Java仍然没有这方面的支持。这是Java积压工作的一部分,也许将作为Project Loom项下提出的更大变化的一部分出现。
5. Why Functional Programming Matters
5.为什么函数式编程很重要
By now, we might wonder why we even want to make this much effort. For someone coming from a Java background, the shift that functional programming demands is not trivial. So, there should be some really promising advantages for adopting functional programming in Java.
现在,我们可能会想,为什么我们要做这么多努力。对于一个来自Java背景的人来说,函数式编程所要求的转变并不简单。因此,在Java中采用函数式编程应该有一些真正有希望的优势。
The biggest advantage of adopting functional programming in any language, including Java, is pure functions and immutable states. If we think back, most of the programming challenges are rooted in the side effects and mutable state one way or the other. Simply getting rid of them makes our program easier to read, reason about, test and maintain.
在任何语言中采用函数式编程,包括Java,最大的优势就是纯函数和不可变的状态。如果我们回想一下,大多数编程的挑战都是源于副作用和可变的状态这种或那种方式。简单地摆脱它们使我们的程序更容易阅读、推理、测试和维护。。
Declarative programming leads to very concise and readable programs. As a subset of declarative programming, functional programming offers several constructs such as higher-order functions, function composition and function chaining. Think of the benefits that Stream API has brought into Java 8 for handling data manipulations.
作为声明式编程的一个子集,函数式编程提供了若干构造,如高阶函数、函数组合和函数链。想一想Stream API在Java 8中为处理数据操作带来的好处。
But don’t get tempted to switch over unless completely ready. Please note that functional programming is not a simple design pattern that we can immediately use and benefit from.
但是,除非完全准备好,否则不要被诱惑去转换。请注意,函数式编程不是一个简单的设计模式,我们可以立即使用并从中受益。
Functional programming is more of a change in how we reason about problems and their solutions and how to structure the algorithm.
函数式编程更多的是改变了我们对问题及其解决方案的推理方式以及如何构建算法。
So, before we start using functional programming, we must train ourselves to think about our programs in terms of functions.
因此,在我们开始使用函数式编程之前,我们必须训练自己以函数的方式思考我们的程序。
6. Is Java a Suitable Fit?
6.Java是否合适?
It’s hard to deny functional programming benefits, but is Java a suitable choice for it?
很难否认函数式编程的好处,但Java是否适合于它?
Historically, Java evolved as a general-purpose programming language more suitable for object-oriented programming. Even thinking about using functional programming before Java 8 was tedious! But things have definitely changed after Java 8.
从历史上看,Java是作为一种通用的编程语言发展起来的,更适合面向对象的编程。在Java 8之前,即使考虑使用函数式编程也是很乏味的!但在Java 8之后,情况肯定有所改变。
The fact that there are no true function types in Java goes against functional programming’s basic principles. The functional interfaces disguised as lambda expressions largely make up for it, at least syntactically.
Java中没有真正的函数类型这一事实违背了函数式编程的基本原则。伪装成lambda表达式的函数式接口在很大程度上弥补了这一点,至少在语法上是如此。
Then it doesn’t help that types in Java are inherently mutable and we have to write so much boilerplate to create immutable types.
那么这也是无济于事的,Java中的类型本来就是可变的,我们不得不写这么多的模板来创建不可变的类型。
We expect other things from a functional programming language that are missing or difficult in Java. For instance, the default evaluation strategy for arguments in Java is eager. But lazy evaluation is a more efficient and recommended way in functional programming.
我们对函数式编程语言的其他期望,在Java中是缺失或难以实现的。例如,Java中对参数的默认评估策略是急切的。但在函数式编程中,懒惰评估是一种更有效的、值得推荐的方式。
We can still achieve lazy evaluation in Java using operator short-circuiting and functional interfaces, but it’s more involved.
我们仍然可以在Java中使用运算符短路和函数式接口来实现懒人评估,但它的工作量更大。
The list is certainly not complete and can include generics support with type-erasure, missing support for tail-call optimization and other things. However, we get a broad idea.
这个列表当然不是完整的,可以包括对类型消除的泛型支持,对尾随调用优化的支持缺失以及其他事情。然而,我们可以得到一个大致的概念。
Java is definitely not suitable for starting a program from scratch in functional programming.
Java绝对不适合在函数式编程中从头开始编写程序。
But what if we already have an existing program written in Java, probably in object-oriented programming? Nothing stops us from getting some of the benefits of functional programming, especially with Java 8.
但是,如果我们已经有了一个用Java编写的现有程序,可能是面向对象的编程,那该怎么办?没有什么能阻止我们获得函数式编程的一些好处,尤其是在Java 8中。
This is where most of the benefits of functional programming lie for a Java developer. A combination of object-oriented programming with the benefits of functional programming can go a long way.
这就是函数式编程对Java开发人员的大部分好处所在。将面向对象的编程与函数式编程的好处结合起来,可以走得更远。
7. Conclusion
7.结语
In this article, we went through the basics of functional programming. We covered the fundamental principles and how we can adopt them in Java.
在这篇文章中,我们回顾了函数式编程的基础知识。我们涵盖了基本原则以及我们如何在Java中采用这些原则。
Further, we discussed some popular techniques in functional programming with examples in Java.
此外,我们用Java的例子讨论了函数式编程中的一些流行技术。
Finally, we covered some of the benefits of adopting functional programming and answered if Java is suitable for the same.
最后,我们介绍了采用函数式编程的一些好处,并回答了Java是否适合于此。
The source code for the article is available over on GitHub.
该文章的源代码可在GitHub上获得。