Java中的7种功能编程技术-入门

Øriginally published in deepu.tech.

关于函数式编程(FP)的炒作很多,很多很酷的孩子正在这样做,但这不是灵丹妙药。 像其他编程范例/样式一样,函数式编程也有其优点和缺点,一个可能比另一个更喜欢一个范例。 如果您是Java开发人员并且想冒险进行函数式编程,请放心,您不必学习自Java以来​​面向函数式编程的语言,例如Haskell或Clojure(甚至是Scala或JavaScript,尽管它们不是纯粹的函数式编程语言)。 你有报道吗,这个职位是给你的。

我不会详细介绍所有函数式编程概念,而是将重点放在您可以在Java中完成的与函数式编程概念一致的事情上。 我也不会讨论一般的函数式编程的利弊。


What is functional programming?

根据维基百科,

函数式编程是一种编程范例-一种构建计算机程序结构和元素的样式-将计算视为对数学函数的评估,并避免更改状态和可变数据。

因此,在函数式编程中,有两个非常重要的规则

  • 无数据突变:表示数据对象创建后不应更改。无隐含状态:应避免隐藏/隐含状态。 在函数式编程状态下不消除,而是使其可见和显式

这意味着:

  • 没有副作用:功能或操作不得在其功能范围之外更改任何状态。 即,一个函数应仅将一个值返回给调用者,并且不应影响任何外部状态。 这意味着程序更易于理解。仅纯功能:功能代码是幂等的。 函数应仅基于传递的参数返回值,并且不应影响(副作用)或依赖于全局状态。 对于相同的参数,此类函数始终会产生相同的结果。

除了这些之外,下面还有一些可以在Java中应用的函数式编程概念,我们将进一步探讨这些概念。

使用函数式编程并不意味着全部或全部,您可以始终使用函数式编程概念来补充面向对象的概念,尤其是在Java中。 无论使用哪种范例或语言,都可以尽可能利用函数式编程的优势。 这正是我们将要看到的。


Functional programming in Java

因此,让我们看看如何在Java中应用上面的一些功能编程概念。 我们将使用Java 11,因为它目前是LTS版本。

First-class and higher-order functions

一流的函数(作为一流公民的函数)意味着您可以将函数分配给变量,将函数作为参数传递给另一个函数或从另一个函数返回一个函数。 不幸的是,Java不支持此功能,因此使诸如闭包,柯里化和高阶函数之类的概念难以编写。

The closest to first-class functions in Java is Lambda expressions. There are also some built-in functional interfaces like Function, Consumer, Predicate, Supplier and so on under the java.util.function package which can be used for functional programming.

仅当一个函数将一个或多个函数作为参数或作为结果返回另一个函数时,才可以将其视为高阶函数。 我们在Java中获得的最接近高阶函数的方法是使用Lambda表达式和内置的Functional接口。

这不是执行高阶函数的最好方法,但这就是Java中的样子,而且还不错。

public class HocSample {
    public static void main(String[] args) {
        var list = Arrays.asList("Orange", "Apple", "Banana", "Grape");

        // we are passing an array and an anonymous inner class instance of FnFactory as arguments to mapForEach method.
        var out = mapForEach(list, new FnFactory<String, Object>() {
            @Override
            public Object execute(final String it) {
                return it.length();
            }
        });
        System.out.println(out); // [6, 5, 6, 5]
    }

    // The method takes an array and an instance of FnFactory as arguments
    static <T, S> ArrayList<S> mapForEach(List<T> arr, FnFactory<T, S> fn) {
        var newArray = new ArrayList<S>();
        // We are executing the method from the FnFactory instance
        arr.forEach(t -> newArray.add(fn.execute(t)));
        return newArray;
    }

    @FunctionalInterface // this doesn't do anything it is just informative.
    public interface FnFactory<T, S> {
        // The interface defines the contract for the anonymous class
        S execute(T it);
    }
}

幸运的是,实际上可以使用内置方法进一步简化上述示例功能接口,并使用lambda表达式语法。

public class HocSample {
    public static void main(String[] args) {
        var list = Arrays.asList("Orange", "Apple", "Banana", "Grape");
        // we are passing the array and a lambda expression as arguments to mapForEach method.
        var out = mapForEach(list, it -> it.length());
        // This can be further simplified to "mapForEach(list, String::length);", I'm writing the expanded version for readability
        System.out.println(out); // [6, 5, 6, 5]
    }

    // The method takes an array and an instance of Function as arguments (we have replaced the custom interface with the built-in one)
    static <T, S> ArrayList<S> mapForEach(List<T> arr, Function<T, S> fn) {
        var newArray = new ArrayList<S>();
        // We are executing the method from the Function instance
        arr.forEach(t -> newArray.add(fn.apply(t)));
        return newArray;
    }
}

使用这些概念以及lambda表达式,我们可以编写闭包和currying,如下所示

public class ClosureSample {
    // this is a higher-order-function that returns an instance of Function interface
    Function<Integer, Integer> add(final int x) {
        // this is a closure, i.e, a variable holding an anonymous inner class instance of the Function interface
        // which uses variables from the outer scope
        Function<Integer, Integer> partial = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer y) {
                // variable x is obtained from the outer scope of this method which is declared as final
                return x + y;
            }
        };
        // The closure function instance is returned here
        return partial;
    }

    public static void main(String[] args) {
        ClosureSample sample = new ClosureSample();

        // we are currying the add method to create more variations
        Function<Integer, Integer> add10 = sample.add(10);
        Function<Integer, Integer> add20 = sample.add(20);
        Function<Integer, Integer> add30 = sample.add(30);

        System.out.println(add10.apply(5)); // 15
        System.out.println(add20.apply(5)); // 25
        System.out.println(add30.apply(5)); // 35
    }
}

我们可以使用下面的lambda表达式进一步简化它

public class ClosureSample {
    // this is a higher-order-function that returns an instance of Function interface
    Function<Integer, Integer> add(final int x) {
        // The lambda expression is returned here as closure
        // variable x is obtained from the outer scope of this method which is declared as final
        return y -> x + y;
    }

    public static void main(String[] args) {
        ClosureSample sample = new ClosureSample();

        // we are currying the add method to create more variations
        Function<Integer, Integer> add10 = sample.add(10);
        Function<Integer, Integer> add20 = sample.add(20);
        Function<Integer, Integer> add30 = sample.add(30);

        System.out.println(add10.apply(5));
        System.out.println(add20.apply(5));
        System.out.println(add30.apply(5));
    }
}

Java中还有许多内置的高阶函数,例如,这是来自java.util.Collections

List<String> list = Arrays.asList("Apple", "Orange", "Banana", "Grape");

// This can be simplified as "Collections.sort(list, Comparator.naturalOrder());", I'm writing the expanded version for readability
Collections.sort(list, (String a, String b) -> {
    return a.compareTo(b);
});

System.out.println(list); // [Apple, Banana, Grape, Orange]

Java流API还提供了许多有趣的高阶函数,例如forEach,map等。

Pure functions

正如我们已经看到的,纯函数应该仅基于传递的参数返回值,而不应该影响或依赖全局状态。 在Java中可以执行此操作,但某些情况下会涉及检查的异常。

这很简单,下面这是一个纯函数。 对于给定的输入,它将始终返回相同的输出,并且其行为是高度可预测的。 如果需要,我们可以安全地缓存该方法。

public static int sum(int a, int b) {
    return a + b;
}

如果我们在此函数中添加额外的一行,则该行为将变得不可预测,因为它现在具有影响外部状态的副作用。

static Map map = new HashMap<String, Integer>();

public static int sum(int a, int b) {
    var c = a + b;
    map.put(a + "+" + b, c);
    return c;
}

因此,请尝试使函数保持纯净和简单。

Recursion

函数式编程优先于循环而不是循环。 在Java中,这可以通过使用流API或编写递归函数来实现。 让我们看一个计算数字阶乘的例子。

I also ran a benchmark on these using JMH and mentioned the nanoseconds/operation below

在传统的迭代方法中:

public class FactorialSample {
    // benchmark 9.645 ns/op
    static long factorial(long num) {
        long result = 1;
        for (; num > 0; num--) {
            result *= num;
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(factorial(20)); // 2432902008176640000
    }
}

使用下面的递归可以完成相同的功能,这在函数式编程中是有利的。

public class FactorialSample {
    // benchmark 19.567 ns/op
    static long factorialRec(long num) {
        return num == 1 ? 1 : num * factorialRec(num - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorialRec(20)); // 2432902008176640000
    }
}

递归方法的缺点是,在大多数情况下,它比迭代方法要慢(我们的目标是代码简单性和可读性),并且可能会导致堆栈溢出错误,因为每个函数调用都需要保存为 堆叠的框架。 为了避免这种尾部递归,尤其是在递归执行过多次时,尤其如此。 在尾部递归中,递归调用是函数执行的最后一件事,因此编译器不需要保存函数堆栈帧。 大多数编译器可以像优化迭代代码一样优化尾递归代码,从而避免了性能损失。 不幸的是,Java编译器没有进行此优化:(

Now using tail recursion the same function can be written as below, but Java doesn't optimize this, though there are workarounds, still it performed better in benchmarks.

public class FactorialSample {
    // benchmark 16.701 ns/op
    static long factorialTailRec(long num) {
        return factorial(1, num);
    }

    static long factorial(long accumulator, long val) {
        return val == 1 ? accumulator : factorial(accumulator * val, val - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorialTailRec(20)); // 2432902008176640000
    }
}

我们还可以使用Java流库进行递归,但目前它的速度比普通递归慢。

public class FactorialSample {
    // benchmark 59.565 ns/op
    static long factorialStream(long num) {
        return LongStream.rangeClosed(1, num)
                .reduce(1, (n1, n2) -> n1 * n2);
    }

    public static void main(String[] args) {
        System.out.println(factorialStream(20)); // 2432902008176640000
    }
}

出于可读性和不变性的考虑,在编写Java代码时考虑使用流API或递归,但是如果性能至关重要或如果迭代次数很大,则可以使用标准循环。

Lazy evaluation

惰性评估或非严格评估是将表达式的评估延迟到需要时才进行的过程。 通常,Java会进行严格的评估,但对于像&&,||和?:它做了一个懒惰的评估。 在编写Java代码时,我们可以利用它来进行惰性评估。

以Java急切评估所有内容的示例为例。

public class EagerSample {
    public static void main(String[] args) {
        System.out.println(addOrMultiply(true, add(4), multiply(4))); // 8
        System.out.println(addOrMultiply(false, add(4), multiply(4))); // 16
    }

    static int add(int x) {
        System.out.println("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    static int multiply(int x) {
        System.out.println("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    static int addOrMultiply(boolean add, int onAdd, int onMultiply) {
        return (add) ? onAdd : onMultiply;
    }
}

这将产生以下输出,我们可以看到两个函数始终执行

executing add
executing multiply
8
executing add
executing multiply
16

我们可以使用lambda表达式和高阶函数将其重写为延迟评估的版本

public class LazySample {
    public static void main(String[] args) {
        // This is a lambda expression behaving as a closure
        Function<Integer, Integer> add = t -> {
            System.out.println("executing add");
            return t + t;
        };
        // This is a lambda expression behaving as a closure
        Function<Integer, Integer> multiply = t -> {
            System.out.println("executing multiply");
            return t * t;
        };
        // Lambda closures are passed instead of plain functions
        System.out.println(addOrMultiply(true, add, multiply, 4));
        System.out.println(addOrMultiply(false, add, multiply, 4));
    }

    // This is a higher-order-function
    static <T, R> R addOrMultiply(
            boolean add, Function<T, R> onAdd,
            Function<T, R> onMultiply, T t
    ) {
        // Java evaluates expressions on ?: lazily hence only the required method is executed
        return (add ? onAdd.apply(t) : onMultiply.apply(t));
    }
}

这将输出以下内容,我们可以看到仅执行了必需的功能

executing add
8
executing multiply
16

Type system

Java has a strong type system and with the introduction of the var keyword it now also has pretty decent type inference. The only thing missing compared to other functional programming languages are case classes. There are proposals for value classes and case classes for future Java versions. Let's hope they make it.

Referential transparency

从维基百科:

功能程序没有赋值语句,也就是说,功能程序中的变量值一旦定义就不会改变。 这消除了任何副作用的可能性,因为任何变量都可以在执行的任何时候用其实际值替换。 因此,功能程序是参照透明的。

Unfortunately, there are not many ways to limit data mutation in Java, however by using pure functions and by explicitly avoiding data mutations and reassignment using other concepts we saw earlier this can be achieved. For variables, we can use the final keyword which is a non-access modifier to avoid mutations by reassignments.

例如,以下将在编译时产生错误

final var list = Arrays.asList("Apple", "Orange", "Banana", "Grape");

list = Arrays.asList("Earth", "Saturn");

但是,当变量持有对其他对象的引用时,这将无济于事,例如,以下更改将与final关键字无关地起作用。

final var list = new ArrayList<>();

list.add("Test");
list.add("Test 2");

最后关键字允许引用变量的内部状态发生突变,因此从功能编程的角度来看最后关键字仅对常量和捕获重新分配有用。

Data structures

使用函数式编程技术时,建议使用函数数据类型,例如堆栈,映射和队列。 因此,在功能编程中作为数据存储,映射比数组或哈希集更好。


Conclusion

对于那些试图在Java中应用某些函数式编程技术的人来说,这只是一个介绍。 用Java可以做的事情还很多,Java 8添加了很多API,使使用Java进行功能性编程变得容易,例如流API,Optional接口,功能性接口等。 正如我之前所说的那样,函数式编程并不是灵丹妙药,但是它提供了许多有用的技术来实现更易懂,可维护和可测试的代码。 它可以与命令式和面向对象的编程风格完美地共存。 实际上,我们所有人都应该利用一切。

This video from Venkat Subramaniam is a great resource to dive deep into functional programming in Java


希望这个对你有帮助。 如果您有任何疑问,或者您认为我错过了什么,请添加评论。

如果您喜欢这篇文章,请留下喜欢或评论。

You can follow me on Twitter and LinkedIn.

from: https://dev.to//deepu105/functional-programming-in-java-a-primer-13nb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值