将链接对象转换为流或集合

19

我想要迭代一个堆栈跟踪信息。 堆栈跟踪信息由throwable组成,其getCause()返回下一个throwable。最后一次调用getCause()返回null。(例如:a -> b -> null)

我尝试使用Stream.iterable(),但结果是NullPointerException,因为iterable中的元素不能为null。 这是问题的简短演示:

  public void process() {
      Throwable b = new Throwable();
      Throwable a = new Throwable(b);
      Stream.iterate(a, Throwable::getCause).forEach(System.out::println);
  }

我目前正在使用 while 循环手动创建一个集合:

public void process() {
    Throwable b = new Throwable();
    Throwable a = new Throwable(b);

    List<Throwable> list = new ArrayList<>();
    Throwable element = a;
    while (Objects.nonNull(element)) {
      list.add(element);
      element = element.getCause();
    }
    list.stream().forEach(System.out::println);
  }

有没有更好的方法(更短、更功能强大)来实现这个目标?


你有一个嵌套的数据结构。流和集合假定是顺序数据。你需要进行转换才能从一个得到另一个。 - f1sh
2
你可以使用 Stream.Builder 代替创建临时的 List - AJNeufeld
@f1sh 自从栈不是连续的了? - Socratic Phoenix
6个回答

17
问题在于Stream.iterate缺少停止条件。在Java 9中,您可以使用
Stream.iterate(exception, Objects::nonNull, Throwable::getCause)

这相当于Java 9的

Stream.iterate(exception, Throwable::getCause)
      .takeWhile(Objects::nonNull)

请参见 Stream.iterateStream.takeWhile。由于这个特性在Java 8中不存在,所以需要进行回溯。
public static <T> Stream<T>
                  iterate​(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
{
    Objects.requireNonNull(next);
    Objects.requireNonNull(hasNext);
    return StreamSupport.stream(
        new Spliterators.AbstractSpliterator<T>(Long.MAX_VALUE, Spliterator.ORDERED) {
            T current = seed;
            int state;
            public boolean tryAdvance(Consumer<? super T> action) {
                Objects.requireNonNull(action);
                T value = current;
                if(state > 0) value = next.apply(value);
                else if(state == 0) state = 1;
                else return false;
                if(!hasNext.test(value)) {
                    state = -1;
                    current = null;
                    return false;
                }
                action.accept(current = value);
                return true;
            }
        },
        false);
}

这里的语义与Java 9中的Stream.iterate相同:

MyStreamFactory.iterate(exception, Objects::nonNull, Throwable::getCause)
               .forEach(System.out::println); // just an example

我不确定第一个Java 9版本是否可行。第二个参数不是停止条件,而是“hasNext”谓词。参考:http://download.java.net/java/jdk9/docs/api/java/util/stream/Stream.html#iterate-T-java.util.function.Predicate-java.util.function.UnaryOperator- - balki
1
@balki: nonNull “has next”谓词。不要与isNull混淆... - Holger

14
我认为你可以在这里进行递归调用:
static Stream<Throwable> process(Throwable t) {
    return t == null ? Stream.empty() : Stream.concat(Stream.of(t), process(t.getCause()));
}

7
这种方法的缺点是在流构建时有效地遍历了整个链。此外,嵌套的 Stream.concat 的结果可能相当低效(甚至 javadoc 也对此进行了警告)。但通常情况下,原因链不应太长... - Holger

9

递归的 Stream::concat() 方法会在一次递归调用中提前创建整个流。而懒惰的 takeWhile 方法直到Java 9才可用。

以下是一种懒惰的Java 8方法:

class NullTerminated {
    public static <T> Stream<T>  stream(T start, Function<T, T> advance) {
        Iterable<T> iterable = () -> new Iterator<T>() {
            T next = start;

            @Override
            public boolean hasNext() {
                return next != null;
            }

            @Override
            public T next() {
                T current = next;
                next = advance.apply(current);
                return current;
            }           
        };
        return StreamSupport.stream(iterable.spliterator(), false);
    }
}

使用方法:

Throwable b = new Throwable();
Throwable a = new Throwable(b);

NullTerminated.stream(a, Throwable::getCause).forEach(System.out::println);

更新:使用直接构造Spliterator替换Iterator/Iterable.spliterator()
class NullTerminated {
    public static <T> Stream<T>  stream(T start, Function<T, T> advance) {
        Spliterator<T> sp = new AbstractSpliterator<T>(Long.MAX_VALUE, Spliterator.ORDERED | Spliterator.NONNULL) {
            T current = start;
            @Override
            public boolean tryAdvance(Consumer<? super T> action) {
                if (current != null) {
                    action.accept(current);
                    current = advance.apply(current);
                    return true;
                }
                return false;
            }
        };
        return StreamSupport.stream(sp, false);
    }
}

更新2:

对于一次性、高效、最简代码实现的问题,将Throwable对象链转换为Stream<Throwable>流,并立即使用该流:

Stream.Builder<Throwable> builder = Stream.builder();
for(Throwable t = a; t != null; t = t.getCause())
    builder.accept(t);
builder.build().forEach(System.out::println);

这种方法的缺点是非惰性(在流构造时要遍历整个链),但避免了递归和Stream.concat()的低效问题。


我认为对于我当前的问题,我会倾向于@Eugene的递归方法,因为它减少了代码量,更容易理解未来与代码一起工作的开发人员,并且我的堆栈跟踪解析仅在非常特定的情况下使用,因此性能不是问题。尽管如此,我非常喜欢这种方法,如果我有更多的用例,我会使用它。(现在太懒写测试)感谢你的麻烦。 - Arigion
2
有趣的是,我喜欢@Holger的Stream.iterator()后移版...请记住,所有这些通用实现代码都写一次并存储在库中,因此可以将其用于任何类型的 <T> 并进行重复使用。@Eugene的递归方法仅适用于 Throwable 对象,因此不可直接重用,并且我发现它比基于Iterator/Spliterator 的流更难以理解。至于未来的开发人员:有人可能会从那段代码中“学到”东西,虽然它可能对于两个或三个级别的Throwable链可以工作,但存在相当大的效率问题,因此不是最佳示例。 - AJNeufeld
嗯,我只是试图弄清楚是否在Stacktrace中隐藏了ConstraintViolationException,以记录包含的验证消息。在我们的情况下,只有当数据库表与jpa实体不同步且所有其他异常处理失败时才会发生这种情况。因此,对于使用案例来说,这已经足够好了。尽管如此,我必须强烈同意,糟糕的代码就像癌症一样,试图在项目中蔓延。我已经为未来的开发人员向我们的代码添加了一条注释,解释了当前解决方案的问题,并链接到这个问题以供进一步参考。 - Arigion
1
@Arigion 更新2:针对这个单一用例增加了一个一次性、高效、最小化的实现。 - AJNeufeld
2
Stream.iterate 不仅可以添加到代码库中一次,而且在切换到 Java 9 时也可以轻松地将调用重定向到标准 API,因此它也可以被移除。 - Holger

5

我有另一种选择,使用 Spliterator

static Stream<Throwable> process(Throwable t) {

    Spliterator<Throwable> sp = new AbstractSpliterator<Throwable>(100L, Spliterator.ORDERED) {

        Throwable inner = t;

        @Override
        public boolean tryAdvance(Consumer<? super Throwable> action) {
            if (inner != null) {
                action.accept(inner);
                inner = inner.getCause();
                return true;
            }

            return false;
        }
    };

    return StreamSupport.stream(sp, false);
}

3

如果我理解正确,您可以使用种子 root (是您在链表中的头部 Throwable )创建一个 Stream 。作为 UnaryOperator ,取下一个 Throwable 。 例如:

Stream.iterate(root, Throwable::getNext)
         .takeWhile(node -> node != null)
         .forEach(node -> System.out.println(node.getCause()));

6
takeWhile 在 jdk-9 中将可用。 - Eugene
2
除了 node -> node != null 可以被替换为 Objects::nonNull - Eugene
Stream.iterate(root, Throwable::getNext)已经由于iterate的实现而抛出NPE: public static <T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) { Objects.requireNonNull(f); ... } 我不确定是否会到达takeWhile - Arigion
4
@Arigion:你误解了结果。f从来不会是null,因为它是你的函数。但是当你遍历导致问题的链时,总会有一个null原因,但由于你没有停在null处,你会尝试在该null引用上调用getCause()。并不是说null元素一般都被禁止使用。 - Holger
5
请注意,Stream.iterate(root, Throwable::getCause) .takeWhile(node -> node != null) 等同于 Stream.iterate(root, node -> node != null, Throwable::getCause) 或者 Stream.iterate(root, Objects::nonNull, Throwable::getCause),它们都需要使用Java 9或更高版本。 - Holger

2
这个有什么问题吗?
while (exception) {
    System.out.println(exception); //or whatever you want to do
    exception = exception.getCause();
}

“更加功能化”是没有意义的。函数式编程只是一种工具,显然在这里不适用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接