Java 函数式编程开始

864 阅读9分钟

在Java8中引入的众多特性中,lambda表达式是最值得提的;lambda的出现给Java语言带来了更多的可能性

到底是什么机会或者什么好处

使Java语言更有表达能,更简洁

虽然Java8已经发布很长,但是还是没有很好掌握他们,所以我们再次梳理总结;

函数式编程是一个编程风格或者模式

那这有事什么意思呢?简单理解就是对现实世界中问题抽象的方法或者工具,面向对象就将我们要解决的问题,都抽象成各种的或者对象,但是在函数式编程中,我们基础模块是函数。都是我们认识或者理解实际问题的工具,可以互相补充。面向对象更加的容易,因为抽象中涉及的概念更贴近生活,但是函数在实际生活中似乎没有,更不用说,高阶等其他的数学特性。

从面向对象转向函数式编程,犹如从知道天圆地方到地球是圆的。

介绍

什么是Functional programming

他是一种源自lambda calculus编程范式,注重声明式。而不是我们通常使用的命令式表达。

也就是他注重定义应该做什么,而不是定义怎么样做或者以什么顺序做

这句话很迷惑,没有上下文,但是到处都在说,只要是涉及函数式编程

t: 这里怎样做,主要是指通过什么线程模型等等完成,需要做的内容。

下面我们通过几个例子来理解,什么是命令式编程,什么是声明式编程。

声明式,从更高的抽象或者另一个角度,抽象问题

我们通过一个从一个数组中返回大于某个数的数组,我们先看看命令式是怎么实现的

    public static Integer[] filterGreaterThan(int threshold, int[] array) {
        List<Integer> greaterThanNumbers = new ArrayList<>();
        for (int number : array) {
            if (number > threshold) {
                greaterThanNumbers.add(number);
            }
        }
        return greaterThanNumbers.toArray(new Integer[greaterThanNumbers.size()]);
    }

这个方法中我们指定了获取大于一个数的数组的每一步。也就是指定了具体怎么做

int[] numbers = new int[]{1, 23, 13, 9, 26, 90, 3, 8};
        final Integer[] greaterThan = filterGreaterThan(10, numbers);
        System.out.println(Arrays.toString(greaterThan));

结果是 [23, 13, 26, 90]

结果没有错,就是代码太冗余,我们看看函数式怎么实现

        final int[] greaterThanTen = Arrays.
                stream(numbers).
                filter(number -> number > 10).toArray();
        System.out.println(Arrays.toString(greaterThan));

在这个例子中我们使用Java中的stream处理数组,是用Predicate告诉stream怎样过滤元素

在stream 中通过 operator声明式的表达要做什么,通过function告诉具体怎么做 stream是什么,就是函数式编程吗?

Predicate【函数式接口,后面会讲】是一个接受一个参数,返回布尔值的Function,他只是众多Java8 提供的Function中的一个

都在Java.util.function包中中

输出的结果也是: [23, 13, 26, 90],但是实现的方式不一样。

和上面的方法比较有什么不同呢?

  • 我们指定的需要做什么,而不是怎样做
  • 更简洁
  • 减少重复代码
  • 容易阅读和理解
  • 天生的不可变性

这个对于并发编程很重要

  • 通过lambda不能修改状态

对于并发操作非常重要,将会再其他的文章中介绍

  • 不会再函数中修改参数状态
  • 惰性求值Lazy evaluation

我们声明需要做什么,但是函数不会执行,直到stream形成或者函数被调用

好的,上面简单介绍完函数式编程之后,我们现在来学习,最重要的内容Java中的函数

Functions 函数

首先,我们需要了解一个重要的概念,这个对我们理解函数式编程很重要

  • 纯函数 pure function

任何时间,同样的参数返回同样的结果;而且没有副作用,就是不影响外界

  • 非纯函数 impure function

和纯函数相反,不能总是返回相同的结果,而且有副作用

  • 高阶函数 Higher-order-functions

首先它是一个函数,高阶函数可以接受函数或者返回函数的函数

        int number = 9;
        final Function<Integer, Integer> impureTimesFunction = times -> number * times;
        System.out.println(impureTimesFunction.apply(2));

这个函数不纯,就是应该他依赖外部的变量;

所他的输出不能总是一样,例如我们将number修改为8,输出就为16,但是我们输入并没有变

那什么是纯函数呢?我们将上面的函数改造成一个纯函数,结果如下:

final BiFunction<Integer,Integer,Integer>  pureTimesFunction = (numb,times) -> numb * times;
System.out.println(pureTimesFunction.apply(9,2));

上面的函数比之前的多了一个参数,这样我们就能保证这个函数输出始终是一样的,不收外界的影响,也不影响外界,没有副作用。

这就是纯函数

纯函数真的有这么重要吗?并发编程中对不变性和非共享状态有需求,而纯函数就提供了这样的支持,这个对于并发编程太重要了

为了能更形象的理解纯函数,我们将纯函数想象成pipe,管道,pipe就是输入通过的地方,安全而且封闭,其他线程不能访问到

这个概念对于我们写出简单、安全的并发程序非常重要

现在我们对函数式编程核心概念有了一些 的了解,接下来,我们看看Java是怎么整合这些概念以。

Java中的函数式编程

主要接口

怎么说着函数,说到了接口

从接口有说到了函数式接口,哪到底什么是函数,接口和函数有啥区别

最主要就是下面三个接口

  • Supplier
  • Consumer
  • Function

还有这个三个函数的变种,理解和掌握着三个接口,其他的接口就能很快掌握

下面我们看看,在Java中着三个接口是怎么使用的

lambda 表达式和匿名类

我们可以使用lambda表达式表达上面的接口。

lambda表达式可以让我们脱离类的存在定义函数,就是函数的定义不在依附于类了

function自己做为一个对象,可以保存和作为参数传递

lambda 表达式的结构如下

(argument1, argument2) -> expression

举例之前我们讨论一下匿名类,匿名类是我们创建一个接口示例,而不创建实现类的方法

因此我们内联定义和实例化,但是不给名字,这就是他的名字的由来

知道了这个,我们现在实例化一个Supplier,我们同时使用匿名类和lambda表达看看

    final Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "hello world";
            }
        };

这个实现没错,但是太烦了;看看lambda表达式怎么实现

final Supplier<String> supplier1 = () -> "hello world";

如上所示,使用lambda表达式,表达函数更加的简洁

下面看看其他的接口使用lambda表达式怎么表示 ?

        final Supplier<String> supplier2 = () -> "hello world";
        final Consumer<String> consumer = element -> System.out.println("This is next element: " + element);
        final Function<String,String> upCase = word -> word.toLowerCase();
        final Function<String,String> upperCaseUsingMethodReference = String::toUpperCase;

最后一行我们看到一个**::**操作,这是方法应用,如果一个函数只是调用另一个函数,我们就可以将这个方法引用赋值给这个函数。

函数接口

除了这些已经存在的接口,任何只有一个方法的接口都被认为是一个函数式接口

我么可以在这些类上添加 @FunctionalInterface 注解,但是这个注解知识给编译器使用的信息,并不会改变接口本身

编译器可以推断函数式接口的类型,只要函数的参数个数和返回值相同,因此我们可以自己定义函数式接口,并且应用在已有的方法上。

我们可以使用之前的例如做个试验,我们定义一个 GreaterThan

    interface GreaterThan {
        boolean apply(int number);
    }

因为这个函数也是接受一个int返回一个boolean,我们可以代替之前示例中的predicate,

    private static int[] elementsGreaterThan(int[] number, GreaterThan greaterThan) {
        return Arrays.stream(number)
                .filter(greaterThan::apply)
                .toArray();
    }

在上面的方法中 filter的参数预期是一个Predicate,但是GreaterThan和Predicate有相同的返回值类型和参数个数,所以编译器和自己推断成功

    int[] numbers = new int[]{1, 23, 13, 9, 26, 90, 3, 8};
        final int[] greaterThanTen1 = elementsGreaterThan(numbers,n -> n >10);
        System.out.println(Arrays.toString(greaterThanTen1));

这个例子依然是解释Java中的函数式接口

Java Stream

Java8 中新添加的Stream特性,让我们可以定义一个操作或者方法的pipeline,处理一个元素的列(集合)。

Stream的特性之一就是延迟计算,就是stream中的方法或者操作不直接执行,直到遇到terminal 操作或者终止操作。 终止操作就是那些想获取值,例如 forEach, collect, sum等等

还有一个特性就是一个stream只能被消费一次。

如上所示,我们只是,声明了我们需要这个stream需要施加的操作,从string 到 int,但是没有终止操作。

如上所示,我们在之前的基础上增加了,终止操作sum。

总结,stream可以让我们使用简单的方法定义一个整个集合的操作pipeline。没有终结操作时不执行,有终结操作开始执行。

试想如果没有stream如果我们想实现一个操作集合施加到集合上,应该怎么实现 ??

常见的stream的方法

  • filter
  • map
  • flatMap
  • reduce

其他的特性支持

现在我们看看除了函数式特性,还增加了那些特性

CompletableFuture

异步编程工具,在其中大量使用function功能。使用function功能使定义回调和串联多个CompletableFuture变得的简单

Optional

NPE 工具 这些一个处理NPE的工具,可以是我们的代码简洁。

Collection.forEach

还是和函数式编程功能相关,可以通过给forEach方法提供函数,这些就可以针对每个元素执行这个函数。

collection.forEach(element -> System.out.println(element));

还有Iterator.forEachRemaining 或者 Collection.removeIf.