Java 函数式编程

概述

函数式编程是声明式编程范例的一个子集。函数式编程技术使我们的代码更加简洁,可读性和可预测性。测试和维护通过函数式编程开发的代码很容易。函数式编程涉及关键概念,例如不可变状态,引用透明度,方法引用,高阶和纯函数。它涉及编程技术,如功能组合,递归,currying和功能接口。

范围

  • 本文通过示例解释了函数式编程范例、其特征和 Java 中的实现。
  • 本文还讨论了函数式编程概念,如纯函数不变性等,以及monadscurrying等技术。
  • 本文区分函数编程和面向对象编程,并解释函数式编程的优点。

介绍

编程范式是一种编程方式或风格。编程范例可分为两种类型:

  • 命令式编程范例
  • 声明式编程范例

让我们快速浏览一下这些编程范例中的每一个。

命令式编程范例

命令式编程范例由一系列语句组成,这些语句会更改程序的状态,直到实现目标结果。主要焦点是如何实现目标。它由三种主要的编程方法组成:

  • 过程编程
  • 面向对象编程
  • 并行处理方法

例:

public class Main {
    public static void main(String[] args) {
        int sum = 0;
        
        for (int i = 1; i <= 5; i ++) {
            sum += i;
        }
        
        System.out.println(sum);
    }
}

输出

15

解释:

此示例定义一系列步骤,告诉编译器查找从 1 到 5 的总和。

声明式编程范例

声明式编程是一种范式,在这种范式中,我们定义了需要完成什么,而没有定义它必须如何实现。在声明式编程范例中,对于相同的输入参数,程序产生相同的结果。语句的执行顺序在声明式编程范例中并不重要。它由三种主要的编程方法组成:

  • 逻辑编程范例
  • 函数式编程
  • 数据库处理方法

例:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] array = new int[] {1, 2, 3, 4, 5};

        int[] evenArray  = Arrays.stream(array)
                .filter(a -> a % 2 == 0)
                .toArray();

        System.out.println(Arrays.toString(evenArray));
    }
}

输出:

[2, 4]

解释:

在此示例中,输入数组将转换为。为流中的每个值调用 filter() 方法,并使用表达式 a -> % 2 == 0 来计算该方法。

filter(1) => false
filter(2) => true
filter(3) => false
filter(4) => true
filter(5) => false

filter() 方法将值传递给下一个操作,即 toArray(), 仅当其中的表达式计算结果为 true 时。因此,在本例中,只有 2 和 4 传递给 toArray() 操作,因此输出为 [2, 4]。

在这里,我们没有定义过滤偶数的步骤。相反,我们通过表达式过滤器(->%2 == 0)告诉编译器我们想要什么。

什么是Java中的函数式编程?

函数式编程是一种范式,其中计算的基本单位是函数。这里的函数不是我们在编程中编写的方法。方法是我们编写一系列指令来告诉计算机该做什么的过程。在函数式编程中,函数被视为数学函数,我们将输入映射到输出以产生结果。例如。f(x) = 3x

在函数式编程中,软件是围绕功能评估开发的。这些函数是孤立和独立的,它们只依赖于传递给它们的参数,并且不会像修改全局变量那样改变程序的状态

函数式编程基于各种概念,如First-Class Citizens纯函数不变性和参照透明度以及函数组合递归MonadsCurrying等技术。我们将在后续部分中了解这些概念。

函数式编程示例

匿名类

匿名类是一个没有名称的内部类,这意味着我们可以同时声明和实例化类。匿名类主要用于我们想要使用一次类声明时。匿名类通常扩展子类或实现接口。

例:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(
            Arrays.asList(1, 3, 4, 5, 2)
        );

        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        };

        list.sort(comparator);

        System.out.println(Arrays.toString(list.toArray())); 
    }
}

输出

[1, 2, 3, 4, 5]

解释:

在这个例子中,我们创建了一个匿名的内部类,即新的比较器<Integer>() {}},它实现了比较器接口并覆盖了 compare() 方法。

Lambda 表达式

Lambda 表达式是一个匿名函数,它采用参数并返回一个值。它被称为匿名函数,因为它不需要名称。

语法:

(parameter1, parameter2, ...) -> expression

(parameter1, parameter2, ...) -> { body }

例:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        
        int sum = list.stream().reduce(0, (a, b) -> a + b);

        System.out.println(sum);
    }
}

输出

15

解释:

在此示例中,我们使用 reduce() 方法来查找列表中元素的总和。reduce() 接受两个参数:一个初始值和一个 lambda 表达式。在这里,我们传递了 lambda 表达式 (a, b) -> a + b。

方法参考

在某些情况下,lambda 表达式调用 Java 中的现有或内置方法。在这种情况下,更清楚地说明是按名称调用方法,而不是使用 lambda 表达式来调用该方法。这使得代码更加紧凑和可读。

语法:

Object :: methodName 

例:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        
        list.forEach(System.out::print);
    }
}

输出

12345

解释

在此示例中,我们没有使用 lambda value -> System.out.print(value) 作为 forEach() 方法的参数,而是使用了 System.out::print 字符串 。这是我们使用方法参考的地方。在每次迭代期间,列表的每个值都将传递给 System.out 对象的 print() 方法。

函数式编程语言的特征

First Class Citizens

在函数式编程中,函数被认为是First Class Citizens。如果函数可以是:

  • 存储在变量中
  • 作为参数传递给函数
  • 作为其他函数的值返回

所有这些操作都是使用 Java 8 中引入的功能接口实现的。

功能接口

函数式接口是只包含一个抽象方法的接口。它也被称为单抽象方法(SAM)接口。它可以有任意数量的默认方法。在Java中,一个接口用@FunctionalInterface注释,使其成为一个功能性接口。@FunctionalInterface注释可确保接口不能有多个抽象方法。函数式接口旨在与 lambda 一起使用。

例:

@FunctionalInterface
interface Concatenator {
    String concat(String s1, String s2);
}

public class Main {
    public static void execute(Concatenator concatenator) {
        System.out.println(concatenator.concat("A", "B"));
    }

    public static void main(String[] args) {
        // Function stored in a variable
        Concatenator concatenator = (s1, s2) -> s1 + s2;
        
        // Function passed as an argument
        execute(concatenator);
    }
}

输出:

AB

解释

  • 我们定义了一个函数接口串联器,只有 concat() 方法。concat() 方法接受两个字符串作为输入,并返回这些字符串的串联。
  • 我们将 concat 的实现存储为 lambda 表达式 (s1, s2) -> s1 + s2 在变量串联器中。
  • lambda 表达式将被视为抽象方法 concat() 的实现。
  • 串联器作为参数传递给调用串联器.concat() 的方法 execute()。
  • 这里,(s1, s2) -> s1 + s2 是一个匿名函数,它存储在变量(串联器)中,并作为参数传递给 execute() 方法,就像其他基元类型一样。

惰性求值

函数式编程支持惰性求值,该求值也被称为:按需求调用求值

惰性求值是一种求值策略,它将表达式的求值延迟到需要其值(非严格求值),并且还可以避免重复求值。

惰性求值基于两个原则:

  • 如果不需要表达式,则不要计算该表达式。
  • 如果不需要多次计算表达式的值,请不要重新计算该表达式。

例:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

        Stream<Integer> stream = list.stream().filter(value -> {
            System.out.println("Checking " + value);
            return value % 2 == 0;
        });

        System.out.println("Started filtering ...");

        System.out.println("Result: " + stream.collect(Collectors.toList()));
    }
}

输出

Started filtering ...
Checking 1
Checking 2
Checking 3
Checking 4
Checking 5
Result: [2, 4]

解释:

  • 当我们看到上面的代码时,我们期望以下输出:
Checking 1
Checking 2
Checking 3
Checking 4
Checking 5
Started filtering ...
Result: [2, 4]
  • 但是,Java 流使用惰性求值,并且在需要其结果之前不会计算表达式。
  • 只有stream.collect()  方法才需要filter() 方法的结果。
  • 因此,仅当编译器执行 steam.collect() 方法时,才会计算 filter() 方法。

无状态Stateless

函数式编程不包含任何状态。它不会在执行期间更改程序的状态。

public static int factorialImperative(int n) {
    int ans = 1;
    for (int i = 2; i <= n; i++) {
        ans *= i;
    }

    return ans;
}

public static int factorialDeclarative(int n) {
    if (n == 0 || n == 1) return 1;
    return n * factorialDeclarative(n - 1);
}

考虑上述方法factorialImperative() 和factorialDeclarative() 来计算数字的阶乘。

factorialImperative() 方法使用变量和在每次迭代期间重复赋值。因此,它正在改变程序的状态。

此factorialDeclarative()方法使用递归来计算数字的阶乘。它不使用任何变量,只使用方法参数,并且不改变程序的状态。

无流量控制

函数式编程不使用像 if..else, switch, for, while, do..while.它完全基于函数调用。

函数式编程的原则和概念

First-Class Functions

如果函数像编程语言中的其他变量一样处理,则称其为头等函数First-Class function。头等函数满足以下条件。

  1. 可以将函数分配给变量。
  2. 一个函数可以作为参数传递给另一个函数。
  3. 可以从另一个函数返回一个函数。

高阶函数Higher-Order Functions

如果函数满足以下任何条件,则称其为高阶函数。

  1. 一个函数接收另一个函数作为参数。
  2. 返回新函数的函数。

@FunctionalInterface
interface Multiplier<T, U> {
    int multiply(T a, U b);
}

public class Main {
    // Higher-Order 1: Function accepts a function as an argument
    private static int execute(Multiplier<Integer, Integer> multiplier){
        return multiplier.multiply(5, 10);
    }

    // Higher-Order 2: Function returns another function
    private static Multiplier<Integer, Integer> getMultiplier() {
        // First-Class 1: Function assigned to a variable
        Multiplier<Integer, Integer> multiplier =  (a, b) -> a * b;

        // First-Class 3: Function returned from a function
        return multiplier;
    }

    public static void main(String[] args) {
        Multiplier<Integer, Integer> multiplier = getMultiplier();

        // First-Class 2: Function passed as an argument
        int ans = execute(multiplier);
        
        System.out.println(ans);
    }
}

输出

50

纯函数Pure Functions

如果函数满足以下条件,则称其为纯函数。

  1. 给定相同的输入,函数返回相同的输出。
  2. 函数没有任何副作用(即)修改全局或本地状态或将数据保存到数据库或磁盘。
public int sum(int a, int b) {
    return a + b;
}

函数 sum() 是纯函数,因为它只对参数 a 和 b 进行操作。sum() 函数返回相同的输出,给定相同的输入,并且不修改任何局部或全局状态。

不可变性

不可变性意味着对象一旦创建就无法修改。Java的不变性可以使用final 关键字和private 访问修饰符来实现。不可变类是使用以下规则在 Java 中创建的。

  1. 该类必须声明为 final,这样可以防止它进行子类化。
  2. 所有字段都必须是private 的和final。
  3. 该类应具有一个或多个用于实例化的public 公共构造函数。
  4. 该类应只有获取器,而不应具有设置器。

// 1. Class is final
final class ImmutableStudent {
    // 2. All fields are private and final
    private final String id;
    private final String name;

    // 3. Public constructor for instantiation
    public ImmutableStudent(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    // 4. Only getters. No setters.
    public String getId() {
        return this.id;
    }
    
    public String getName() {
        return this.name;
    }
}

参照透明度Referential Transparency

如果表达式可以在不更改程序行为的情况下替换为其相应的值,则该表达式在引用上是透明的。

如果表达式中涉及的所有函数都是纯且不可变的函数,则该表达式在引用上是透明的。由于引用透明性,我们生成上下文无关的代码。这意味着,这些功能可以按任何顺序执行,以探索优化的可能性。

例:

public class Main {
    private static int sum(int a, int b) {
        System.out.printf("sumReferential: Adding %d and %d%n", a, b);
        return a + b;
    }

    private static int sumReferential(int a, int b) {
        return a + b;
    }
    
    public static void main(String[] args) {
        sumReferential(1, sumReferential(2, 3));
        sum(1, sum(2, 3));
    }
}

输出:

sumReferential: Adding 2 and 3
sumReferential: Adding 1 and 5

解释:

在此示例中,将 sumReferential(2, 3) 替换为值 5 将得到相同的结果 6。但是如果我们用 5 替换 sum(2,3),我们将错过 sum() 方法中的 printf 语句。因此,sum() 在引用上并不透明。

函数式编程技术

功能组成

函数组合是通过组合较小的函数来组合较大函数的过程。函数组合可以在Java中通过谓词和函数功能接口来实现。

谓语Predicate

Predicate是一个函数接口,它只接受一个输入,并且可以返回布尔输出。它提供了内置的方法,如and(), or(), negate()和isEqual()方法来组合多个函数。组合函数可以使用 test() 方法执行。语法为Predicate<T> 其中 T 是输入类型。Java 还提供了接受两个输入的双谓词。

and()

仅当所有谓词返回 true 时,才返回 true,否则为 false。它的行为类似于逻辑 AND

predicateA.and(predicateB).and(predicateC)...test(value);

or()

如果任何谓词返回 true,则返回 true,否则为 false。它的行为类似于逻辑 OR

predicateA.or(predicateB).or(predicateC)...test(value);

negate()

如果谓词返回 false,则返回 true,反之亦然。它的行为就像逻辑上的NOT

predicateA.negate()...test(value)

isEqual()

返回一个谓词,该谓词检查两个值是否相等。相等性是通过Object.equals()  方法确定的。

Predicate.isEqual(value1).test(value2);

例:

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isDivisibleByTwo = (value) -> value % 2 == 0;
        Predicate<Integer> isDivisibleByFive = (value) -> value % 5 == 0;
        
        /*-------------- and() --------------*/
        System.out.println("--- and() ---");
        Predicate<Integer> isDivisibleByTwoAndFive 
            = isDivisibleByTwo.and(isDivisibleByFive);
        
        System.out.println("10 divisible by 2 and 5: " 
                           + isDivisibleByTwoAndFive.test(10));
        System.out.println("4 divisible by 2 and 5: " 
                           + isDivisibleByTwoAndFive.test(4));
        
        System.out.println();
        
        /*-------------- or() --------------*/
        System.out.println("--- or() ---");
        Predicate<Integer> isDivisibleByTwoOrFive 
            = isDivisibleByTwo.or(isDivisibleByFive);
        
        System.out.println("4 divisible 2 or 5: " 
                           + isDivisibleByTwoOrFive.test(4));
        System.out.println("3 divisible 2 or 5: " 
                           + isDivisibleByTwoOrFive.test(3));
        
        System.out.println();
        
        /*-------------- negate() --------------*/
        System.out.println("--- negate() ---");
        System.out.println("2 divisible by 2: " 
                           + isDivisibleByTwo.negate());
        
        System.out.println();
        
        /*-------------- isEqual() --------------*/
        System.out.println("--- isEqual() ---");
        System.out.println("Tony equal to Stark: " 
                           + Predicate.isEqual("Tony").test("Stark"));
    }
}

输出:

--- and() ---
10 divisible by 2 and 5: true
4 divisible by 2 and 5: false

--- or() ---
4 divisible 2 or 5: true
3 divisible 2 or 5: false

--- negate() ---
2 divisible by 2: false

--- isEqual() ---
Tony equal to Stark: false

解释:

  • isDivisibleByTwo和isDivisibleByFive谓词是使用 and() 构成的,以形成可分割的两个和五个谓词。 isDivisibleByTwoAndFive仅当两个谓词都返回 true 时,否则为 true
  • isDivisibleByTwo和isDivisibleByFive是使用 or() 组成,以形成 isDivisibleByTwoOrFive。isDivisibleByTwoOrFive返回真,如果任何一个谓词返回,否则为假
  • isDivisibleByTwo.negate()则返回 true,否则为。这是因为实际结果被否定了。
  • Predicate.isEqual() 仅当 isEqual() 和 test() 方法的参数相等时,才返回 true。在示例中,结果是错误的,因为托尼斯塔克并不相等。

函数

函数Function也是一个函数接口,它只接受一个输入,并且可以返回任何输出。函数提供了内置的方法,如compose()和andThen() 来编写多个函数。组合函数可以使用 apply() 方法执行。语法为Function<T, U>其中T和U分别是输入和输出的类型。Java 还提供了接受两个输入的 BiFunction。

compose()

predicateA.compose(predicateY).test(value);

compose() 方法首先执行具有值的predicateA,然后使用predicateA 的结果执行predicateB。上述声明可以扩展如下。

temp = predicateA.apply(value);
result = predicateB.apply(temp);

andThen()

predicateA.andThen(predicateY).test(value);

andThen()  方法首先使用值执行predicateB,然后使用predicateB 的结果执行predicateA。上述声明可以扩展如下。

temp = predicateB.apply(value);
result = predicateA.apply(temp);

注意:

predicateA.compose(predicateB) 等价于predicateB.andThen(predicateA)并产生相同的结果。

例:

public class Main {
    public static void main(String[] args) {
        Function<String, String> appendX = (value) -> value + "-X";
        Function<String, String> appendY = (value) -> value + "-Y";

        // Executes appendX first and then appendY
        Function<String, String> appendXAndThenY 
            = appendX.andThen(appendY);
        
        // Executes appendY first and then appendX
        Function<String, String> appendYAndThenX 
            = appendX.compose(appendY);

        System.out.println("andThen: " + appendXAndThenY.apply("A"));
        System.out.println("compose: " + appendYAndThenX.apply("A"));
        
    }
}

输出:

andThen: A-X-Y
compose: A-Y-X

解释:

  • appendXAndThenY.apply(“A”)  返回 A-X-Y,因为它是由 andThen() 和appendX 组成的,并且appendX 在appendY.之前执行。
  • appendYAndThenX.apply(“A”)A-Y-X,因为它是用compose() 组合而成的,而appendY 是在appendX之前执行的。

Monads

Monad 是一种技术,它允许我们包装一个值并对其应用一系列转换。Monads应该遵循三个定律,左同一性右恒等性和结合性。可选Optional是 Java 中 monad 的一个很好的例子。

例:

public class Main {
    public static void main(String[] args) {
        Optional<String> concat = Optional.of("A")
            .flatMap(a -> Optional.of("B")
                     .flatMap(b -> Optional.of(a + b)));
        
        System.out.println(concat.get());
    }
}

输出:

AB

解释:

在此示例中,我们使用 of 方法包装值,并使用 flatMap 方法应用了一系列转换。每个flatMap都接受一个输入并返回一个Optional可选。

Currying

库里宁是将具有多个参数的函数转换为具有单个参数的多个函数。

例:

public class Main {
    public static String concatTraditional(String a, String b) {
        return a + b;
    }
    
    
    public static void main(String[] args) {
        Function<String, Function<String, String>> concatCurried
                = a -> b -> a + b;
        
        System.out.println("concatTraditional: " 
                           + concatTraditional("A", "B"));
        System.out.println("concatCurried: " 
                           + concatCurried.apply("A").apply("B"));
    }
}

输出:

concatTraditional: AB
concatCurried: AB

解释:

在此示例中,我们创建了两个方法:concatCurried(). concatTraditional()是接受两个字符串的标准连接方法,而concatCurried()具有一系列将单个字符串作为参数的方法。

Currying通过 lambda 表达式 a -> b -> a + b 合并。我们可以将此表达式拆分为两个不同的表达式,如下所示。

  1. a 是输入,b -> a + b 是输出。
  2. b 是输入,a + b 是输出。

递归

递归是使函数调用自身的技术。由于该函数是重复调用的,因此需要一个基本条件来中断循环。

例:

public static int factorial(int n) {
    if (n == 0 || n == 1) return 1;
    return n * factorial(n - 1);
}

class Main {
    public static void main(String[] args) {
        System.out.println(factorial(5));
    }
}

输出

120

解释:

factorial()方法最初调用,其值为 5 来自 main() 方法。在每次递归调用期间,n 的值递减 1。当 n 的值变为 1 时,递归调用中断,并将该值返回到 main() 方法。中断递归调用的基本条件是if (n == 0 || n == 1) return 1;

为什么函数式编程很重要?

我们将问题分解成部分的能力直接取决于我们将解决方案粘合在一起的能力。为了支持模块化编程,语言必须提供良好的粘合剂。函数式编程语言提供了两种新的粘合剂 – 高阶函数和惰性求值。

— 为什么函数式编程很重要

  • 函数式编程对函数进行操作,函数负责完成特定任务。这鼓励了模块化的概念。
  • 一个程序可以分为几个模块,每个模块负责一个特定的任务。每个模块可以进一步划分为几个模块来处理原子任务。
  • 采用函数式编程的最大优点是纯函数和不可变状态。这两种技术的实现使代码易于维护和调试。
  • 作为声明式编程的一个子集,函数式编程提供了其他几个好处,如惰性求值高阶函数函数组合链接

函数式编程范式与面向对象编程范式

函数式编程范例面向对象的编程范例
函数式编程遵循声明式编程范式面向对象的编程遵循命令式编程范式
函数式编程使用不可变数据面向对象的编程使用可变数据
函数式编程支持并行编程。面向对象的编程不支持并行编程。
函数式编程对变量函数进行操作。面向对象的编程对对象方法进行操作。
函数式编程使用递归进行迭代。面向对象的编程使用循环进行迭代。
函数式编程是无状态的面向对象的编程是有状态的

函数式编程的优点

  • 函数式编程使用纯函数,它们仅依赖于传递给函数的输入参数,并且始终为给定的输入生成相同的输出。
  • 函数式编程处理不可变的数据(即)状态不会改变,因此更容易调试。
  • 函数式编程支持并行编程,因为纯函数不会更改状态,并且只对输入参数进行操作。
  • 函数式编程支持惰性求值,这避免了不需要的重复计算并节省了时间。
  • 函数式编程是线程安全的,因为纯函数不会修改程序的状态,并且没有共享状态。
  • 在函数式编程中,函数可以存储在变量中并传递给其他函数。这增强了代码的可读性

结论

  • 编程范式是一种编程方式或风格,可以分为命令式和声明式。
  • 函数式编程是一种声明式编程范例。
  • 函数式编程在不可变的数据上运行,并且是无状态的,这意味着它不会改变程序的状态。
  • 函数式编程支持惰性求值、并行编程,并且是线程安全的。
  • 函数式编程基于一等函数、纯函数、不变性和参照透明性。
  • 函数组合、currying、单调和递归是函数式编程技术。
  • 函数式编程鼓励模块化,其中复杂的函数被细分为更简单的函数。