java8函数式编程笔记-延迟性

延迟性

    众所周知,java8的新特性之一便是Stream,通过Stream,我们可以生成无限流(也就是没有结束的流)。这要依托Stream的特点:流中的数据并不是一开始就加载入内存中生成的,它是用到的时候才生成。依据这个才能创造出无限流:

IntStream stream = IntStream.iterate(2, i -> i + 1);
复制代码

如果我们要对无限的流进行递归会怎么样呢? 例子:

我们要获取所有的质数,方法步骤如下:

  • 1:获取一个从2开始的无限流
  • 2:获取流的第一个元素
  • 3:从下一个元素开始遍历,如果能整除第一个元素则从流中过滤掉
  • 4:重复2-3步骤
static IntStream primes (IntStream stream) {
    int head = stream.findFirst().getAsInt();
    return IntStream.concat(
        IntStream.of(head),
        primes(stream.skip(1).filter(i -> i % head != 0))
    );
}
复制代码

    很简单的结果便是会发生java.lang.IllegalStateException异常,因为java的流和迭代器类似只能遍历一遍,遍历完之后我们便说流被消耗掉了。这个原因就和流的数据结构有关,流的一大特点就是随用随生成,我们可以把流看成是一组分布在时间上的一组数据。。而和它类似的集合是在空间概念上的一组数据,从内存中看也一样,集合在内存中划出空间保存数据,因此可以复用。

IntStream.concat(
        IntStream.of(head),
        primes(stream.skip(1).filter(i -> i % head != 0))
    );
复制代码

这行代码的concat第二个参数是直接递归调用,最终会导致出现无限递归的状况。直接递归的方法导致方法运行的时候传递的所有参数都要在第一时间计算出来。因此会一直申请内存空间导致java.lang.StackOverflowError。

对大多数的Java应用而言,Java 8在Stream上的这一限制,即“不允许递归 定义”是完全没有影响的,使用Stream后,数据库的查询更加直观了,程序还具备了并发的能力。 所以,Java 8的设计者们进行了很好的平衡,选择了这一皆大欢喜的方案。不过,Scala和Haskell 这样的函数式语言中Stream所具备的通用特性和模型仍然是你编程武器库中非常有益的补充。

因此我们需要一种方法能和Stream一样随用随时生成。如果用更加技术性的程序设计术语来描述的话这个称之为:延迟计算非限制式计算或者名调用。我们只需要处理质数的那个时刻对Stream进行计算。 Scala提供了对这种算法的支持,在Scala中我们可以用现年的方式重写前面的代码,操作符#::实现了延迟连接的功能(只有在我们需要的时候才进行计算):

def numbers(n: Int): Stream[Int] = n #:: numbers(n+1)
    def primes(numbers: Stream[Int]): Stream[Int] = {
        numbers.head #:: primes(numbers.tail filter (n -> n % numbers.head != 0))
    }
复制代码

创建我们自己的延迟列表

    java的Stream以延迟性著称。他们被刻意设计成这样,即延迟操作,有其独特的原因:Stream就像一个黑盒,它接收请求生成结果。当我们向一个Stream发起一系列的操作请求时,这些请求只是被一一保存起来。只有当我们向Stream发起一个终端操作是,才会实际地进行计算。我们拿最常用的IntStream.filter(IntPredicate predicate)来看,在IntPipeline.java中有该方法的具体实现:

@Override
    public final IntStream filter(IntPredicate predicate) {
        Objects.requireNonNull(predicate);
        return new StatelessOp<Integer>(this, StreamShape.INT_VALUE,
                                        StreamOpFlag.NOT_SIZED) {
            @Override
            Sink<Integer> opWrapSink(int flags, Sink<Integer> sink) {
                return new Sink.ChainedInt<Integer>(sink) {
                    @Override
                    public void begin(long size) {
                        downstream.begin(-1);
                    }

                    @Override
                    public void accept(int t) {
                        if (predicate.test(t))
                            downstream.accept(t);
                    }
                };
            }
        };
    }
复制代码

它返回一个

abstract static class StatelessOp<E_IN> extends IntPipeline<E_IN> {
       
        StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
                    StreamShape inputShape,
                    int opFlags) {
            super(upstream, opFlags);
            assert upstream.getOutputShape() == inputShape;
        }

        @Override
        final boolean opIsStateful() {
            return false;
        }
    }
复制代码

类。直接看作者注释:

Base class for a stateless intermediate stage of an IntStream
IntStream的无状态中间阶段的基类
Construct a new IntStream by appending a stateless intermediate
通过将无状态中间操作附加到现有流来构造新的IntStream。
<E_IN>
上游源中的元素类型

  • operation to an existing stream.
  • @param upstream The upstream pipeline stage 上游管道阶段
  • @param inputShape The stream shape for the upstream pipeline stage 上游管道阶段的流形状
  • @param opFlags Operation flags for the new stage 新阶段的操作标志

光看这些可能还无法理解他有什么作用,因此我们直接查看他的父类构造器:

IntPipeline(AbstractPipeline<?, E_IN, ?> upstream, int opFlags) {
        super(upstream, opFlags);
    }
复制代码

查看注释可以知道

Constructor for appending an intermediate operation onto an existing
用于将中间操作附加到现有操作的构造函数

现在就明白了,这是一个将中间操作附加到现有操作上的函数。让我们继续查看它的super:

AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
        if (previousStage.linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        previousStage.linkedOrConsumed = true;
        previousStage.nextStage = this;

        this.previousStage = previousStage;
        this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
        this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
        this.sourceStage = previousStage.sourceStage;
        if (opIsStateful())
            sourceStage.sourceAnyStateful = true;
        this.depth = previousStage.depth + 1;
    }
复制代码

先不看代码,直接看注释:

Constructor for appending an intermediate operation stage onto an existing pipeline.
用于将中间操作阶段附加到现有管道的构造函数。

按理说我们都是在Stream上进行的操作,但是代码的注释出现的是pipeline。显然,Stream应该是一种特殊的pipeline。那么上面关于流无法复用的真正原因也就知道了:pipeline是单向的。Stream作为特殊的pipeline当然就无法复用了。

    言归正传,我们接下来要说的延迟列表,它是一种更加通用的Stream形式(延迟列表构建了一个跟Stream非常类似的概念),它还同时提供了一种极好的方式去理解高阶函数:我们可以将一个函数作为值放置到某个数据结构中,大多数时候他就静静地待在那里,一旦对其进行调用(即根据需要),用它能够创建更多的数据结构:

LinkedList的元素存在于(并不断延展)内存中。而LazyList的 元素由函数在需要使用时动态创建,你可以将它们看成实时延展的
让我们开始创建一个简单的链接-列表-式的类:

interface MyList<T> {
    T head ();
    
    MyList<T> tail ();
    
    default boolean isEmpty () {
        return true;
    }
}

class MyLinkedList<T> implements MyList<T> {
    
    private final T head;
    
    private final MyList<T> tail;
    
    public MyLinkedList (T head, MyList<T> tail) {
        this.head = head;
        this.tail = tail;
    }
    
    public T head () {
        return head;
    }
    
    public MyList<T> tail () {
        return tail;
    }
    
    public boolean isEmpty () {
        return false;
    }
    
}

class Empty<T> implements MyList<T> {
    
    public T head () {
        throw new UnsupportedOperationException();
    }
    
    public MyList<T> tail () {
        throw new UnsupportedOperationException();
    }
    
    public boolean isEmpty () {
        return false;
    }
    
}
复制代码

现在我们可以构造一个MyLinkedList值:

    MyList<Integer> list = new MyLinkedList<> (5, new MyLinkedList<> (10, new Empty<>()));
复制代码

对这个类进行改造使得它符合延迟列表的思想,根据上面所描述的我们需要将函数当成元素让它“静静地待在那里”。而我们最开始之所以会栈溢出是因为程序一开始想要计算所有的递归函数。因此我们将递归函数部分改造成函数。当我们需要的时候返回对应的MyList<T>对象,这里我们需要的函数描述符为T -> ();因此使用Supplier<T>方法。

class MyLazyList<T> implements MyList<T> {
    
    private final T head;
    
    private final Supplier<MyList<T>> tail;
    
    public MyLazyList (T head, Supplier<MyList<T>> tail) {
        this.head = head;
        this.tail = tail;
    }
    
    public T head () {
        return head;
    }
    
    public MyList<T> tail () {
        //这里使用get()方法提供延迟性
        return tail.get();
    }
    
    public boolean isEmpty () {
        return false;
    }
    
}
复制代码

调用tail方法会触发get()进行节点创建返回节点,就像工厂一样创建新对象。现在我们可以传递一个Supplier作为MyLazyList的构造器的tail参数,创建由数字构成的无限延迟列表了:

public static MyLazyList<Integer> form (int n) {
    return new MyLazyList<> (n, () -> form(n + 1));
}
复制代码

对它的使用如下:

    //从数字2开始生成
    LazyList<Integer> numbers = from(2);
    int two = numbers.head();
    int three = numbers.tail().head();
    int four = numbers.tail().tail().head();
    System.out.println(two + " " + three + " " + four);
    
复制代码

回到生成质数

让我们将原来的Stream代码改造成使用延迟列表,这里我们还需要在原来的代码中加上filter(Predicate<T> p) 方法来过滤数据.

public MyList<T> filter (Predicate<T> p) {
    return isEmpty() ? 
        this : //返回的将会是空对象
        p.test(head()) ? 
            new MyLazyList<T>(head(), () -> tail().filter(p)) : //满足条件返回对象
            tail().filter(p); //否则跳到下一个
}
复制代码

这时候我们改造后的primes方法如下:

public static MyList<Integer> primes (MyList<Integer> list) {
    return new MyLazyList<> (
        list.head(),
        () -> primes (list.tail().filter(n -> n % list.head() != 0))
    );
}
复制代码

现在我们可以使用该方法了,让我们看看计算头三个质数:

MyLazyList<Integer> list = form (2);
int two = primes(list).head();
int three = primes(list).tail().head();
int four = primes(list).tail().tail().head();

System.out.println(two + " " + three + " " + four);
//out:2 3 5
复制代码

现在我们可以随意生成质数了,甚至可以让他一直生成下去

static <T> void printAll(MyList<T> list){
    while (!list.isEmpty()){
        System.out.println(list.head());
        list = list.tail();
    }
}

printAll(primes(from(2)));
复制代码

当然,为了可读性和对应改文章主题等多方面着想,我们最好将它修改成更加简洁的递归方式:

static <T> printAll (MyList<T> list) {
    if (list.isEmpty()) {
        return;
    }
    System.out.println(list.head());
    printAll(list.tail());
}
复制代码

最终程序会因为栈溢出而失败,因为Java不支持尾部调用消除。

通常而言,执行一次递归式方法调用的开销要比迭代执行单一机器级的分支指令大不少。

为什么呢?每次执行递归方法调用都会在调用栈上创建一个新的栈帧,用于保存每个方法调用的状态(即它需要进行的乘法运算),这个操作会一直指导程序运行直到结束。这意味着你的递归迭代方法会依据它接收的 输入成比例地消耗内存。

而这种问题函数式语言就提供并支持一个解决办法:尾调优化。

尾调优化(属于扩展,与本文无关)

例:

使用迭代计算阶乘

static int factorialIterative (int n) {
    int r = 1;
    for (int i = 1; i <= n; i++) {
        r *= i;
    }
    return r;
}

/**
 * 递归的方法计算
 */
static long factorialRecursive (long n) {
    return n == 1 ? 1 : n * factorialRecursive(n - 1);
}

/**
 * 基于Stream的计算
 */
static long factorialStreams (long n) {
    return LongStream.rangeClosed(1, n)
                        .reduce(1, (a, b) -> a * b);
}
复制代码

尾调优化对于上述问题的解决:

编写阶乘的一个迭代定义,不过迭代调用发生在函数的最后(所以我们说调用发生在尾部).这种新的迭代调用经过优化后执行的速度快很多。
复制代码

以下是java基于尾-递思想的定义:

static long factorialTailRecursive (long n) {
    return factorialHelper(1, n);
}

static long factorialHelper (long acc, long n) {
    return n == 1 ? acc : factorialHelper(acc * n, n - 1);
}
复制代码

方法factorialHelper属于尾-递类型的函数,原因是递归调用发生在方法最后,对比上面的factorialRecursive方法的定义,这个犯法的最后一个操作是乘以n,从而得到递归调用的结果.

这种形式的递归是非常有意义的,现在我们不需要在不同的栈帧上保存每次递归计算的中间 值,编译器能够自行决定复用某个栈帧进行计算。实际上,在factorialHelper的定义中,立 即数(阶乘计算的中间结果)直接作为参数传递给了该方法。再也不用为每个递归调用分配单独 的栈帧用于跟踪每次递归调用的中间值——通过方法的参数能够直接访问这些值。

坏消息是java当前不支持(笑)。很多现在的JVM语言:Scala和Groovy都已经支持这种形式的递归优化,他们的运行速度和迭代不相上下,在保留函数式编程风格的同时兼顾了运行速度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值