Java进阶之函数式编程

一、什么是函数式编程

       函数式编程是一种编程范式,不在于具体的语言,具体的API。它属于结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。       

       在函数式编程中,一个变量一旦被赋值,是不可改变的。没有可变的变量,意味着没有状态。而中间状态是导致软件难以管理的一个重要原因,尤其在并发状态下,稍有不慎,中间状态的存在很容易导致问题。没有中间状态,也就能避免这类问题。无中间状态,更抽象地说是没有副作用。说的是一个函数只管接受一些入参,进行计算后吐出结果,除此以外不会对软件造成任何其他影响,把这个叫做没有副作用。因为没有中间状态,因此一个函数的输出只取决于输入,只要输入是一致的,那么输出必然是一致的。

       例如:

(1 + 2) * 3 - 4

      传统的过程式编程,可能这样写:

int a = 1 + 2;
int b = a * 3;
int c = b - 4;

       函数式编程要求使用函数,我们可以把运算过程[定义]为不同的函数,然后写成下面这样:

int result = subtract(multiply(add(1, 2), 3), 4);

二、函数式编程的特点

1. 函数是“第一等公民”

       所谓“第一等公民”,指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

       例如:下面代码中的print就是一个函数,可以作为另一个函数的参数。

public class Test {

    private void print(String str) {
        System.out.println(str);
    }

    public static void main(String[] args) {
        Test test = new Test();
        List list = Arrays.asList("aaa", "bbb", "ccc");
        list.forEach(str -> test.print((String) str));
    }
}

2. 只用“表达式”,不用“语句”

       “表达式”是一个单纯的运算过程,总是有返回值;“语句”是执行某种操作,没有返回值。

       函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

       原因是函数式编程的开发动机,一开始就是为了处理运算,不考虑系统的读写(I/O)。“语句”属于对系统的读写操作,所以就被排斥在外。当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3. 没有“副作用”

       所谓“副作用”,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

       函数式编程强调没有“副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

4. 不修改状态
       上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。在其他类型的语言中,变量往往用来保存“状态”。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。

5. 引用透明
       指的是函数的运行不依赖于外部变量或“状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。

上述内容参考:
作者:mrZhao丶
链接:https://www.jianshu.com/p/62934dbf2818

三、Java8的函数式编程

       函数式编程是Java8的一大特色,从Java8起,Java也开始支持函数式编程。简单的理解就是可以将函数作为一个参数传递给指定方法。对此,Java8提供了三大全新的特性来支持函数式编程:Lambda表达式函数式接口类型推断方法引用。

1. Lambda表达式

       Lambda 表达式,有时候也称为匿名函数或箭头函数,几乎在当前的各种主流的编程语言中都有它的身影。Java8 中引入 Lambda 表达式,使原本需要用匿名类实现接口来传递行为,现在通过 Lambda 可以更直观的表达。

  • Lambda 表达式,也可称为闭包。闭包就是一个定义在函数内部的函数,闭包使得变量即使脱离了该函数的作用域范围也依然能被访问到。
  • Lambda 表达式的本质只是一个“语法糖”,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。
  • Lambda 表达式是一个匿名函数,即没有函数名的函数。有些函数如果只是临时一用,而且它的业务逻辑也很简单时,就没必要非给它取个名字不可。
  • Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

Lambda 表达式语法如下:形参列表 -> 函数体(函数体多于一条语句的可用大括号括起)。在Java里就是() -> {}

(parameters) -> expression

(parameters) ->{ statements; }

Lambda表达式的重要特征

  • Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。
  • Lambda表达式是通过函数式接口(必须有且仅有一个抽象方法声明)识别的。
  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值,则编译器会自动返回值,大括号需要指定表达式返回一个值。

Lambda表达式中的变量作用域

  • 访问权限与匿名对象的方式非常类似。只能够访问局部对应的外部区域的局部final变量,以及成员变量和静态变量
  • 在Lambda表达式中能访问域外的局部非final变量、但不能修改Lambda域外的局部非final变量。因为在Lambda表达式中,Lambda域外的局部非final变量会在编译的时候,会被隐式地当做final变量来处理
  • Lambda表达式内部无法访问接口默认(default)方法。

例子:使用Java 8之前的方法来实现对一个string列表进行排序:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

Java 8 Lambda 表达式:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});
// 只有一条逻辑语句,可以省略大括号
Collections.sort(names, (String a, String b) -> b.compareTo(a));
// 可以省略入参类型
Collections.sort(names, (a, b) -> b.compareTo(a));

2. 函数式接口

       Java8中采用函数式接口作为Lambda 表达式的目标类型。函数式接口(Functional Interface)是一个有且仅有一个抽象方法声明接口。任意只包含一个抽象方法的接口,我们都可以用来做成Lambda表达式。每个与之对应的lambda表达式必须要与抽象方法的声明相匹配。函数式接口与其他普通接口的区别:

  • 函数式接口中只能有一个抽象方法(这里不包括与Object的方法重名的方法)
  • 接口中唯一抽象方法的命名并不重要,因为函数式接口就是对某一行为进行抽象,主要目的就是支持 Lambda 表达式
  • 自定义函数式接口时,应当在接口前加上@FunctionalInterface标注(虽然不加也不会有错误)。编译器会注意到这个标注,如果你的接口中定义了第二个抽象方法的话,编译器会抛出异常。

JDK 1.8之前已有的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.nio.file.PathMatcher
  • java.lang.reflect.InvocationHandler
  • java.beans.PropertyChangeListener
  • java.awt.event.ActionListener
  • javax.swing.event.ChangeListener

JDK 1.8 新增加的函数接口:

       java.util.function 包下,包含了很多接口,用来支持Java8的函数式编程,该包中的函数式接口有:

3. 类型推断

       通常 Lambda 表达式的参数并不需要显示声明类型。那么对于给定的Lambda表达式,程序如何知道对应的是哪个函数接口以及参数的类型呢?编译器通过 Lambda 表达式所在的上下文来进行目标类型推断,通过检查 Lambda 表达式的入参类型及返回类型,和对应的目标类型的方法签名是否一致,推导出合适的函数接口。比如:

Stream.of("foo", "bar").map(s -> s.length()).filter(l -> l == 3);

       在上面的例子中,对于传入 map 方法的 Lamda 表达式,从 Stream 的类型上下文可以推导出入参是 String 类型,从函数的返回值可以推导出出参是整形类型,因此可推导出对应的函数接口类型为 Function;对于传入 filter 方法的 Lamda 表达式,从 pipeline 的上下文可得知入参是整形类型,因此可推导出函数接口 Predicate。

4. 方法引用

       Java8中还可以通过方法引用表示 Lambda 表达式。方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。Java8允许你通过"::"关键字获取方法或者构造函数的引用。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

       常用的方法引用有:

  • 静态方法引用:ClassName::methodName
  • 实例对象上的方法引用:instanceReference::methodName
  • 类上的方法引用:ClassName::methodName
  • 构造方法引用:Class::new
  • 数组构造方法引用:TypeName[]::new

例子:

// 静态方法引用
Stream.of(someStringArray).allMatch(StringUtils::isNotEmpty);
// 实例对象上的方法引用
Stream.of(someStringArray).map(this::someTransform);
// 类上的方法引用
Stream.of(someStringArray).mapToInt(String::length);
// 构造方法引用
Stream.of(someStringArray).collect(Collectors.toCollection(LinkedList::new));
// 数组构造方法引用
Stream.of(someStringArray).toArray(String[]::new);

上述内容参考:
作者:EnjoyMoving
链接:https://zhuanlan.zhihu.com/p/92687444

 

发布了35 篇原创文章 · 获赞 37 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34519487/article/details/103985668