Java 8流反转顺序

231

一般问题:如何正确地反转一个流?假设我们不知道这个流包含什么类型的元素,有什么通用方法可以反转任何流?

具体问题:

IntStream提供了range方法来在特定范围内生成整数IntStream.range(-range,0),现在我想将其反转,从0到负数的切换范围行不通,也无法使用Integer::compare

List<Integer> list = Arrays.asList(1,2,3,4);
list.stream().sorted(Integer::compare).forEach(System.out::println);

使用 IntStream 我会得到以下编译错误:

Error:(191, 0) ajc: The method sorted() in the type IntStream is not applicable for the arguments (Integer::compare)

我错过了什么?


2
一个 IntStream 没有 .sorted(Comparator) 方法;你必须先通过一个 Stream<Integer> 并在那里反转后再产生一个 IntStream - fge
5
要生成倒序的 IntStream.range(0, n),可以使用类似于 map(i -> n - i - 1) 的方式。无需进行装箱和排序。 - Stuart Marks
3
您的一般问题和具体问题看起来像是两个完全不同的问题。一般问题谈论的是反转“流”,而具体问题则谈论将数字按降序排序。如果该“流”以无序方式生成数字,例如 1, 3, 2,您期望的结果是什么?您想要倒转后的“流”,例如 2, 3, 1,还是排序后的“流”,例如 3, 2, 1 - chiccodoro
10
通常情况下,你无法反转一个流 - 例如,流可能是无限的。 - assylias
1
您可能想要重新提出问题,作为“以Java 8的方式反向迭代集合”。答案可能超出流。下面来自@venkata-raju的答案解决了这个问题,但需要额外的空间。我仍在等待看到这个问题的好答案。 - Manu Manjunath
显示剩余5条评论
31个回答

111

针对生成反向IntStream的具体问题,可以尝试以下代码:

static IntStream revRange(int from, int to) {
    return IntStream.range(from, to)
                    .map(i -> to - i + from - 1);
}

这避免了装箱和排序。
对于如何反转任何类型的流的一般问题,我不知道是否有“正确”的方法。我能想到几种方法。两种方式最终都会存储流元素。我不知道有没有一种不存储元素就可以反转流的方法。
第一种方法将元素存储到数组中,并以相反的顺序从中读取元素到流中。请注意,由于我们不知道流元素的运行时类型,因此无法正确地对数组进行类型化,需要进行未经检查的强制类型转换。
@SuppressWarnings("unchecked")
static <T> Stream<T> reverse(Stream<T> input) {
    Object[] temp = input.toArray();
    return (Stream<T>) IntStream.range(0, temp.length)
                                .mapToObj(i -> temp[temp.length - i - 1]);
}

另一种技术使用收集器将项目累积到反向列表中。这会在ArrayList对象的前面进行大量插入,因此会进行大量复制。

Stream<T> input = ... ;
List<T> output =
    input.collect(ArrayList::new,
                  (list, e) -> list.add(0, e),
                  (list1, list2) -> list1.addAll(0, list2));

使用某种定制的数据结构,可能可以编写更高效的反转收集器。

更新 2016-01-29

由于这个问题最近引起了一些关注,我认为我应该更新我的答案来解决在 ArrayList 前插入的问题。这会非常低效,需要 O(N^2) 的复制。

更好的选择是使用 ArrayDeque,它能够有效地支持在前面插入元素。一个小问题是,我们不能使用 Stream.collect() 的三个参数形式;它需要将第二个参数的内容合并到第一个参数中,而 Deque 上没有 "添加所有到前面" 的批量操作。相反,我们使用 addAll() 将第一个参数的内容附加到第二个参数的末尾,然后返回第二个参数。这需要使用 Collector.of() 工厂方法。

完整代码如下:

Deque<String> output =
    input.collect(Collector.of(
        ArrayDeque::new,
        (deq, t) -> deq.addFirst(t),
        (d1, d2) -> { d2.addAll(d1); return d2; }));

结果是一个Deque而不是List,但这并不是什么大问题,因为可以很容易地迭代或流式处理现在的反向顺序。

20
另一种选择是:IntStream.iterate(to-1, i->i-1).limit(to-from) - Holger
4
很抱歉,那个解决方案不能正确处理溢出。 - Brandon
6
确实,你需要使用 .limit(endExcl-(long)startIncl),但对于如此大的流,这种做法是不推荐的,因为它比基于range的解决方案效率低得多。当我写下这条评论时,我还不知道这种效率差异。 - Holger
或者 IntStream.rangeClosed(1, to - from).map(i -> to-i)(也可以参考bjmi的评论)。 - greybeard

72

优雅的解决方案

List<Integer> list = Arrays.asList(1,2,3,4);
list.stream()
    .sorted(Collections.reverseOrder()) // Method on Stream<Integer>
    .forEach(System.out::println);

14
它看起来很优雅,但似乎并不完全有效,因为列表的元素需要是“可比较的”... - Krzysztof Wolny
106
假设我们希望元素按相反顺序排序。这个问题是关于翻转流的顺序。 - Dillon Ryan Redding
@KrzysztofWolny,你可以将比较函数传递给reverseOrder(),这样就不会太麻烦了。 - Dávid Leblay
1
我认为我们错过了问题的重点,流API通常由有限源(数组/列表等)支持,而不像热可观察流。目的是要像编写向下迭代的for循环一样高效地完成这个操作...我认为可以通过自定义分割器实现。 - vach

58

通用问题:

流不存储任何元素。

因此,如果没有在某个中间集合中存储元素,则无法按相反的顺序迭代元素。

Stream.of("1", "2", "20", "3")
      .collect(Collectors.toCollection(ArrayDeque::new)) // or LinkedList
      .descendingIterator()
      .forEachRemaining(System.out::println);

更新:将LinkedList更改为ArrayDeque(更好)详情请参见此处

输出:

3

20

2

1

顺便说一下,使用sort方法是不正确的,因为它排序而不是翻转(假设流可能具有无序元素)

具体问题:

我发现这个方法简单、容易理解(复制自@Holger的评论

IntStream.iterate(to - 1, i -> i - 1).limit(to - from)

5
一些流操作,例如 sorteddistinct 实际上会存储一个中间结果。请参阅包 API 文档以获取有关此内容的一些信息。 - Lii
@Lii,我在同一页中看到“无存储”。即使它存储了,我们也无法访问该存储(所以“无存储”应该是可以的)。 - Venkata Raju
好的回答。但由于使用了额外的空间,程序员在处理非常大的集合时可能不太适合采用你的方法。 - Manu Manjunath
我喜欢这个解决方案的简单性,从可理解性的角度来看,利用现有数据结构上的现有方法...之前的使用映射实现的解决方案更难理解,但肯定是正确的,对于大型集合,我不会使用这种直观的方法,而是选择上面的映射方法。 - Beezer
这是这里少数正确的答案之一。其他大多数答案实际上并没有反转流,而是试图在某种程度上避免这样做(这只在特定情况下有效,但通常你本来就不需要反转)。如果你试图反转一个无法放入内存的流,那么你做错了。将其转储到数据库中,并使用常规SQL获取反转流。 - Cubic

41

这里许多解决方案都对 IntStream 进行排序或反转,但这样会不必要地需要中间存储。 Stuart Marks 的解决方案 才是正确的:

static IntStream revRange(int from, int to) {
    return IntStream.range(from, to).map(i -> to - i + from - 1);
}

它也能正确地处理溢出,通过了这个测试:

@Test
public void testRevRange() {
    assertArrayEquals(revRange(0, 5).toArray(), new int[]{4, 3, 2, 1, 0});
    assertArrayEquals(revRange(-5, 0).toArray(), new int[]{-1, -2, -3, -4, -5});
    assertArrayEquals(revRange(1, 4).toArray(), new int[]{3, 2, 1});
    assertArrayEquals(revRange(0, 0).toArray(), new int[0]);
    assertArrayEquals(revRange(0, -1).toArray(), new int[0]);
    assertArrayEquals(revRange(MIN_VALUE, MIN_VALUE).toArray(), new int[0]);
    assertArrayEquals(revRange(MAX_VALUE, MAX_VALUE).toArray(), new int[0]);
    assertArrayEquals(revRange(MIN_VALUE, MIN_VALUE + 1).toArray(), new int[]{MIN_VALUE});
    assertArrayEquals(revRange(MAX_VALUE - 1, MAX_VALUE).toArray(), new int[]{MAX_VALUE - 1});
}

很棒而且简单。这是来自某个开源工具(Estreams)还是你的代码片段? - vach
4
好的,“令人惊叹且简单”……这个解决方案是否处理了任何一种情况,而Stuart Marks更简单的解决方案在一年半前就已经处理了? - Holger
我鼓励@vach接受他自己的建议,而不是我的。一旦他的帖子被我建议的修改所接受,我将完全删除我的回答。 - Brandon
1
@vach,您可以使用StreamEx并指定步长:IntStreamEx.rangeClosed(from-1, to, -1) - Tagir Valeev
@TagirValeev 谢谢,我在几个地方看到了这个库,包括 habrahabr,我一定会去看看的,似乎是一个不错的工具。 - vach
显示剩余4条评论

40

如何不要做:

  • 不要使用.sorted(Comparator.reverseOrder()).sorted(Collections.reverseOrder()),因为它只会按降序排序元素。
    对于给定的整数输入,使用它:
    [1, 4, 2, 5, 3]
    输出将如下所示:
    [5, 4, 3, 2, 1]
    对于字符串输入:
    ["A", "D", "B", "E", "C"]
    输出将如下所示:
    [E, D, C, B, A]
  • 不要使用.sorted((a, b) -> -1)(解释在结尾处)

正确的最简单方法:

List<Integer> list = Arrays.asList(1, 4, 2, 5, 3);
Collections.reverse(list);
System.out.println(list);

输出:
[3, 5, 2, 4, 1]

对于String也是同样的操作:

List<String> stringList = Arrays.asList("A", "D", "B", "E", "C");
Collections.reverse(stringList);
System.out.println(stringList);

输出:
[C, E, B, D, A]

不要使用 .sorted((a, b) -> -1)
这会破坏比较器的合同,并且可能仅适用于某些情况,例如仅在单线程中而不是并行中。
简单解释:

(a, b) -> -1 违反了 Comparator 的合同。这是否有效取决于排序算法的实现。JVM 的下一个版本可能会破坏此功能。实际上,我可以使用 IntStream.range(0, 10000).parallel().boxed().sorted((a, b) -> -1).forEachOrdered(System.out::println); 在我的计算机上可重现地破坏它。

//Don't use this!!!
List<Integer> list = Arrays.asList(1, 4, 2, 5, 3);
List<Integer> reversedList = list.stream()
        .sorted((a, b) -> -1)
        .collect(Collectors.toList());
System.out.println(reversedList);

正常情况下的输出结果为:
[3, 5, 2, 4, 1]

在并行流或其他JVM实现中可能会出现以下输出结果:
[4, 1, 2, 3, 5]

String类型同理:

//Don't use this!!!
List<String> stringList = Arrays.asList("A", "D", "B", "E", "C");
List<String> reversedStringList = stringList.stream()
        .sorted((a, b) -> -1)
        .collect(Collectors.toList());
System.out.println(reversedStringList);

正常情况下的输出结果为:
[C, E, B, D, A]

在并行流或其他JVM实现中,可能的输出结果为:
[A, E, B, D, C]


2
(a, b) -> -1 breaks the contract for Comparator. Whether this works depends on the implementation of the sort algorithm. The next release of the JVM might break this. Actually I can already break this reproduciblly on my machine using IntStream.range(0, 10000).parallel().boxed().sorted((a, b) -> -1).forEachOrdered(System.out::println); - yankee
@yankee 是的,你说得对。这违反了“Comparator”合同,有点滥用“Comparator”的意思。它在单线程中运行良好,但即使您不使用.parallel(),也不能依赖它,因为您不知道虚拟机将如何运行它,也不知道将使用哪个排序算法(甚至单线程工作的排序算法可能会在此实现中出现问题)。谢谢您的评论。我会在空闲时间里修改我的答案。 - luke
我根据 yankee 的评论修改了我的答案。现在答案应该是正确的。 - luke

22

没有使用外部库...

import java.util.List;
import java.util.Collections;
import java.util.stream.Collector;

public class MyCollectors {

    public static <T> Collector<T, ?, List<T>> toListReversed() {
        return Collectors.collectingAndThen(Collectors.toList(), l -> {
            Collections.reverse(l);
            return l;
        });
    }

}

1
Collectors.toList() 明确不保证生成的列表是可变的,而 Collections.reverse 需要改变列表才能生效。 "返回的列表的可变性没有任何保证;如果需要更多对返回列表的控制,请使用 toCollection(Supplier)。" Collections.toCollection(ArrayList::new) 可以保证工作,而 toList 只是在当前 Java 库的实现中偶然起作用。 - M. Justin

13
您可以定义自己的收集器,以相反的顺序收集元素:
public static <T> Collector<T, List<T>, List<T>> inReverse() {
    return Collector.of(
        ArrayList::new,
        (l, t) -> l.add(t),
        (l, r) -> {l.addAll(r); return l;},
        Lists::<T>reverse);
}

并且像这样使用:

stream.collect(inReverse()).forEach(t -> ...)

我使用ArrayList按顺序高效地插入收集项目(在列表末尾),并使用Guava Lists.reverse高效地提供列表的反向视图而不制作另一个副本。

这里是自定义收集器的一些测试用例:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

import org.hamcrest.Matchers;
import org.junit.Test;

import com.google.common.collect.Lists;

public class TestReverseCollector {
    private final Object t1 = new Object();
    private final Object t2 = new Object();
    private final Object t3 = new Object();
    private final Object t4 = new Object();

    private final Collector<Object, List<Object>, List<Object>> inReverse = inReverse();
    private final Supplier<List<Object>> supplier = inReverse.supplier();
    private final BiConsumer<List<Object>, Object> accumulator = inReverse.accumulator();
    private final Function<List<Object>, List<Object>> finisher = inReverse.finisher();
    private final BinaryOperator<List<Object>> combiner = inReverse.combiner();

    @Test public void associative() {
        final List<Object> a1 = supplier.get();
        accumulator.accept(a1, t1);
        accumulator.accept(a1, t2);
        final List<Object> r1 = finisher.apply(a1);

        final List<Object> a2 = supplier.get();
        accumulator.accept(a2, t1);
        final List<Object> a3 = supplier.get();
        accumulator.accept(a3, t2);
        final List<Object> r2 = finisher.apply(combiner.apply(a2, a3));

        assertThat(r1, Matchers.equalTo(r2));
    }

    @Test public void identity() {
        final List<Object> a1 = supplier.get();
        accumulator.accept(a1, t1);
        accumulator.accept(a1, t2);
        final List<Object> r1 = finisher.apply(a1);

        final List<Object> a2 = supplier.get();
        accumulator.accept(a2, t1);
        accumulator.accept(a2, t2);
        final List<Object> r2 = finisher.apply(combiner.apply(a2, supplier.get()));

        assertThat(r1, equalTo(r2));
    }

    @Test public void reversing() throws Exception {
        final List<Object> a2 = supplier.get();
        accumulator.accept(a2, t1);
        accumulator.accept(a2, t2);

        final List<Object> a3 = supplier.get();
        accumulator.accept(a3, t3);
        accumulator.accept(a3, t4);

        final List<Object> r2 = finisher.apply(combiner.apply(a2, a3));

        assertThat(r2, contains(t4, t3, t2, t1));
    }

    public static <T> Collector<T, List<T>, List<T>> inReverse() {
        return Collector.of(
            ArrayList::new,
            (l, t) -> l.add(t),
            (l, r) -> {l.addAll(r); return l;},
            Lists::<T>reverse);
    }
}

11
如果实现了 Comparable<T> 接口(如 IntegerStringDate),您可以使用 Comparator.reverseOrder() 方法来实现。
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.stream()
     .sorted(Comparator.reverseOrder())
     .forEach(System.out::println);

28
这并不是将流逆转,而是按相反的顺序对流进行排序。因此,如果你有一个 Stream.of(1,3,2),结果将是 Stream.of(3,2,1) 而不是 Stream.of(2,3,1) - wilmol

9

cyclops-react 的 StreamUtils 具有一个反向的 Stream 方法 (javadoc)。

  StreamUtils.reverse(Stream.of("1", "2", "20", "3"))
             .forEach(System.out::println);

它的工作原理是通过收集到一个ArrayList,然后利用ListIterator类进行迭代,该类可以向前或向后迭代,以向列表后方迭代。

如果您已经有一个List,那么效率会更高。

  StreamUtils.reversedStream(Arrays.asList("1", "2", "20", "3"))
             .forEach(System.out::println);

1
很好,不知道这个项目。 - vach
1
Cyclops 现在还配备了 Spliterators,用于高效地反转 Stream(目前适用于 Ranges、数组和 Lists)。使用 SequenceM.of、SequenceM.range 和 SequenceM.fromList 创建 Cyclops SequenceM Stream 扩展时,将自动利用可高效反转的 Spliterator。 - John McClean

7
我建议使用jOOλ,它是一个很棒的库,可以为Java 8流和lambda添加许多有用的功能。
然后,您可以执行以下操作:
List<Integer> list = Arrays.asList(1,2,3,4);    
Seq.seq(list).reverse().forEach(System.out::println)

就是这么简单。它是一个相当轻量级的库,非常值得添加到任何Java 8项目中。


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