在Java中,如何有效地完成我和优雅的流树节点的后代? [英] In Java, how do I efficiently and elegantly stream a tree node's descendants?

查看:167
本文介绍了在Java中,如何有效地完成我和优雅的流树节点的后代?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我们有对象,由唯一的字符串 S来标识的集合,连同类的定义了他们的层次结构。这个类是使用地图从节点(再由他们的ID psented $ P $),以集合是他们的执行各个孩子的标识。

Assume we have a collection of objects that are identified by unique Strings, along with a class Tree that defines a hierarchy on them. That class is implemented using a Map from nodes (represented by their IDs) to Collections of their respective children's IDs.

class Tree {
  private Map<String, Collection<String>> edges;

  // ...

  public Stream<String> descendants(String node) {
    // To be defined.
  }
}

我想使流节点的后代的一个简单的解决办法是这样的:

I would like to enable streaming a node's descendants. A simple solution is this:

private Stream<String> children(String node) {
    return edges.getOrDefault(node, Collections.emptyList()).stream();
}

public Stream<String> descendants(String node) {
    return Stream.concat(
        Stream.of(node),
        children(node).flatMap(this::descendants)
    );
}

在继续,我想提出这个解决方案下断言。 (我是正确了解这些?)

Before continuing, I would like to make the following assertions about this solution. (Am I correct about these?)

  1. 走在从的后裔 返回占用的资源(时间和内存) - 相对于尺寸树 - 复杂的手工编码的递归会的顺序相同。特别是,中间对象重新presenting迭代状态( S, Spliterator S,... )形成堆叠,因此所需的存储在任何给定时间是在复杂度作为树的深度的顺序相同。

  1. Walking the Stream returned from descendants consumes resources (time and memory) – relative to the size of the tree – in the same order of complexity as hand-coding the recursion would. In particular, the intermediate objects representing the iteration state (Streams, Spliterators, ...) form a stack and therefore the memory requirement at any given time is in the same order of complexity as the tree's depth.

据我了解,只要我在流从的后裔返回,根级呼叫 flatMap 将导致所有载秒 - 一个用于每个(递归)调用的后裔 - 要立即实现。因此,得到的是只有懒递归的第一级,但从未被超越。 (根据 Tagir Valeevs答案编辑。)

As I understand this, as soon as I perform a terminating operation on the Stream returned from descendants, the root-level call to flatMap will cause all contained Streams – one for each (recursive) call to descendants – to be realized immediately. Thus, the resulting Stream is only lazy on the first level of recursion, but not beyond. (Edited according to Tagir Valeevs answer.)

如果我理解正确的这几点,我的问题是这样的:如何定义的后裔使生成的是懒惰?

If I understood these points correctly, my question is this: How can I define descendants so that the resulting Stream is lazy?

我想的解决方案是优雅地,在我preFER一个解决方案,这让迭代状态隐含的意义​​。 (要澄清我的意思是说:我知道,我可以写一个 Spliterator 行走的树,同时保持一个明确的堆栈 Spliterator 每个级别s。我想避免的。)

I would like the solution to be as elegant as possible, in the sense that I prefer a solution which leaves the iteration state implicit. (To clarify what I mean by that: I know that I could write a Spliterator that walks the tree while maintaining an explicit stack of Spliterators on each level. I would like to avoid that.)

(有可能的方式在Java中,制订这是一个生产者 - 消费者的工作流程,就像人们可以像朱莉娅语言使用和出路在哪里?)

(Is there possibly a way in Java to formulate this as a producer-consumer workflow, like one could use in languages like Julia and Go?)

推荐答案

对我来说,你的解决方案已经优雅地和它不是你的错有限的懒惰。最简单的办法是等待,直到它被固定在JRE的开发。

To me, your solution is already as elegant as possible and the limited laziness of it not your fault. The simplest solution is to wait until it gets fixed by the JRE developers.

不过,如果今日实施本次有限懒惰确实是一个问题,它也许时间来解决这个在一般的方式。那么,它的的有关实施 Spliterator ,而不是具体到你的任务。相反,它是一个重新实施 flatmap 运行服务于所有情况下的原执行事宜有限的懒惰:

However, if this limited laziness of today’s implementation really is a concern, it’s perhaps time to solve this in a general way. Well, it is about implementing a Spliterator, but not specific to your task. Instead, it’s a re-implementation of the flatmap operation serving all cases where the limited laziness of the original implementation matters:

public class FlatMappingSpliterator<E,S> extends Spliterators.AbstractSpliterator<E>
implements Consumer<S> {

    static final boolean USE_ORIGINAL_IMPL
        = Boolean.getBoolean("stream.flatmap.usestandard");

    public static <T,R> Stream<R> flatMap(
        Stream<T> in, Function<? super T,? extends Stream<? extends R>> mapper) {

        if(USE_ORIGINAL_IMPL)
            return in.flatMap(mapper);

        Objects.requireNonNull(in);
        Objects.requireNonNull(mapper);
        return StreamSupport.stream(
            new FlatMappingSpliterator<>(sp(in), mapper), in.isParallel()
        ).onClose(in::close);
    }

    final Spliterator<S> src;
    final Function<? super S, ? extends Stream<? extends E>> f;
    Stream<? extends E> currStream;
    Spliterator<E> curr;

    private FlatMappingSpliterator(
        Spliterator<S> src, Function<? super S, ? extends Stream<? extends E>> f) {
        // actually, the mapping function can change the size to anything,
        // but it seems, with the current stream implementation, we are
        // better off with an estimate being wrong by magnitudes than with
        // reporting unknown size
        super(src.estimateSize(), src.characteristics()&ORDERED);
        this.src = src;
        this.f = f;
    }

    private void closeCurr() {
        try { currStream.close(); } finally { currStream=null; curr=null; }
    }

    public void accept(S s) {
        curr=sp(currStream=f.apply(s));
    }

    @Override
    public boolean tryAdvance(Consumer<? super E> action) {
        do {
            if(curr!=null) {
                if(curr.tryAdvance(action))
                    return true;
                closeCurr();
            }
        } while(src.tryAdvance(this));
        return false;
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        if(curr!=null) {
            curr.forEachRemaining(action);
            closeCurr();
        }
        src.forEachRemaining(s->{
            try(Stream<? extends E> str=f.apply(s)) {
                if(str!=null) str.spliterator().forEachRemaining(action);
            }
        });
    }

    @SuppressWarnings("unchecked")
    private static <X> Spliterator<X> sp(Stream<? extends X> str) {
        return str!=null? ((Stream<X>)str).spliterator(): null;
    }

    @Override
    public Spliterator<E> trySplit() {
        Spliterator<S> split = src.trySplit();
        if(split==null) {
            Spliterator<E> prefix = curr;
            while(prefix==null && src.tryAdvance(s->curr=sp(f.apply(s))))
                prefix=curr;
            curr=null;
            return prefix;
        }
        FlatMappingSpliterator<E,S> prefix=new FlatMappingSpliterator<>(split, f);
        if(curr!=null) {
            prefix.curr=curr;
            curr=null;
        }
        return prefix;
    }
}

您只需要使用它,是一个进口静态添加 flatMap 方法到您的code和变化前$ P $形式pssions stream.flatmap(功能) flatmap(流功能)

All you need for using it, is to add a import static of the flatMap method to your code and change expressions of the form stream.flatmap(function) to flatmap(stream, function).

即。在code

public Stream<String> descendants(String node) {
    return Stream.concat(
        Stream.of(node),
        flatMap(children(node), this::descendants)
    );
}

那么,你有充分的偷懒行为。我甚至有无限流测试它...

then you have full lazy behavior. I tested it even with infinite streams…

请注意,我添加了一个切换,使转回到原来的执行,如指定当 -Dstream.flatmap.usestandard = TRUE 在命令行上。

Note that I added a toggle to allow turning back to the original implementation, e.g. when specifying    -Dstream.flatmap.usestandard=true on the command line.

这篇关于在Java中,如何有效地完成我和优雅的流树节点的后代?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆