使用Java 8 Streams API对整数列表进行洗牌

41

我试图使用Streams API将以下Scala代码行翻译为Java 8:

// Scala
util.Random.shuffle((1 to 24).toList)

要在Java中编写相当的代码,我创建了一个整数范围:

IntStream.range(1, 25)

我本以为在流API中会找到toList方法,但是IntStream只知道这个奇怪的方法:

collect(
  Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R,R> combiner)

如何使用Java 8 Streams API对列表进行洗牌?

8个回答

50
您可能会发现以下toShuffledList()方法很有用。
private static final Collector<?, ?, ?> SHUFFLER = Collectors.collectingAndThen(
        Collectors.toCollection(ArrayList::new),
        list -> {
            Collections.shuffle(list);
            return list;
        }
);

@SuppressWarnings("unchecked")
public static <T> Collector<T, ?, List<T>> toShuffledList() {
    return (Collector<T, ?, List<T>>) SHUFFLER;
}

这使得以下一行代码成为可能:

IntStream.rangeClosed('A', 'Z')
         .mapToObj(a -> (char) a)
         .collect(toShuffledList())
         .forEach(System.out::print);

示例输出:

AVBFYXIMUDENOTHCRJKWGQZSPL

6
这应该是答案。 - bhantol
3
根据 API 文档,这段代码的可行性不能得到保证,因为 toList() 方法并没有保证返回的 List 是可变的。由于当前实现的 toList() 恰好返回一个可变的 ArrayList,所以在实践中是有效的。为了确保如果实现更改仍然正确,我们可以显式声明集合类型(Collectors.toCollection(ArrayList:: new)),或者将 toList 的结果复制到可变的列表中(new ArrayList<>(integers))。 - M. Justin
7
谢谢,我已经编辑了答案。这是Stream API中最令人恼火的事情之一。它不能保证返回的List的可变性或线程安全性,甚至不能保证它具有O(1)的 get方法。与Stream API的许多部分一样,这真是太糟糕了。按照这个论点的逻辑结论,你应该实际使用返回列表上的唯一方法是 sizeiterator - Paul Boddington
2
@PaulBoddington 深入探究后,值得注意的是,一些特殊的集合实现甚至没有O(1)的size操作(虽然这些肯定是异常情况)--https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html#size-- - M. Justin

50

给你:

List<Integer> integers =
    IntStream.range(1, 10)                      // <-- creates a stream of ints
        .boxed()                                // <-- converts them to Integers
        .collect(Collectors.toList());          // <-- collects the values to a list

Collections.shuffle(integers);

System.out.println(integers);

输出:

[8, 1, 5, 3, 4, 2, 6, 9, 7]

1
实际上,最好使用Integer :: valueOf()进行映射,以利用整数缓存,即使对于如此小的范围也几乎没有影响。 - Mike Strobel
2
我编辑了答案,使用了IntStream.boxed(),这几乎是标准的。 - Louis Wasserman
30
呃,这并不是真正的流处理解决方案。 - ncmathsadist
15
根据API文档,这段代码的可行性不被保证,因为toList()方法不能保证返回的List是可变的。实际上,因为当前的toList()实现返回的是一个可变的ArrayList,所以这段代码能够正常工作。如果实现发生了改变,为保证正确性,我们可以显式地声明集合类型(Collectors.toCollection(ArrayList::new))或将toList的结果复制到一个可变列表中(new ArrayList<>(integers))。 - M. Justin
1
@M.Justin或者直接转向Kotlin/Scala,避免冗长的讨论。 - Andrey Chaschev
显示剩余7条评论

16
您可以使用自定义比较器,通过随机值对值进行“排序”:
public final class RandomComparator<T> implements Comparator<T> {

    private final Map<T, Integer> map = new IdentityHashMap<>();
    private final Random random;

    public RandomComparator() {
        this(new Random());
    }

    public RandomComparator(Random random) {
        this.random = random;
    }

    @Override
    public int compare(T t1, T t2) {
        return Integer.compare(valueFor(t1), valueFor(t2));
    }

    private int valueFor(T t) {
        synchronized (map) {
            return map.computeIfAbsent(t, ignore -> random.nextInt());
        }
    }

}

流中的每个对象都会(懒惰地)关联一个随机整数值,我们将其用于排序。对映射进行同步是为了处理并行流。

然后您可以像这样使用它:

IntStream.rangeClosed(0, 24).boxed()
    .sorted(new RandomComparator<>())
    .collect(Collectors.toList());

这种解决方案的优点在于它可以与流水线集成。

1
请参考这个答案获取一个不错的替代方案。 - shmosel
6
由于排序算法的更改,此答案不再有效:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6804124 当比较器不遵守基本契约时,可能会出现问题。 - user1928596
排序算法的变化发生在2009年,所以这里没有什么新东西。你有例子说明这会出问题吗? 我唯一看到的问题是在random.nextInt()产生(不太可能的)冲突时equals方法的不一致性,即使如此,也不应该阻止洗牌的发生。 - Xavier
@user1928596 比较器违反了哪个约定?由于它存储了每个值的映射并进行比较,因此它符合“Comparator.compareTo”要求中描述的所有标准。 - M. Justin
2
如果列表中有重复元素,它将无法完全洗牌,因为它们总是会相邻。例如,列表 [1,5,1,1,5] 将变成 [5,5,1,1,1][1,1,1,5,5] 中的一个。 - M. Justin
1
除了它不违反那个规则。它存储了一个(随机的)T => Integer 的映射,并在该整数上进行比较。由于映射在比较器的生命周期内不会改变,因此 f(A)<f(B) and f(B)<f(C) => f(A)<f(C),其中 f(x) 是映射函数。这个解决方案等同于 Comparator.comparing(t -> { synchronized (map) { return map.computeIfAbsent(t, ignore -> random.nextInt()); }});,这或许更清楚地说明了它如何满足基本契约。 - M. Justin

6

如果你想在不太麻烦的情况下处理整个流,你可以使用 Collectors.collectingAndThen() 创建自己的收集器:

public static <T> Collector<T, ?, Stream<T>> toEagerShuffledStream() {
    return Collectors.collectingAndThen(
      toList(),
      list -> {
          Collections.shuffle(list);
          return list.stream();
      });
}

但是,如果你想要对结果Stream进行limit()操作,则效果可能不佳。为了克服这个问题,可以创建一个自定义的Spliterator:

package com.pivovarit.stream;

import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.RandomAccess;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Supplier;

class ImprovedRandomSpliterator<T, LIST extends RandomAccess & List<T>> implements Spliterator<T> {

    private final Random random;
    private final List<T> source;
    private int size;

    ImprovedRandomSpliterator(LIST source, Supplier<? extends Random> random) {
        Objects.requireNonNull(source, "source can't be null");
        Objects.requireNonNull(random, "random can't be null");

        this.source = source;
        this.random = random.get();
        this.size = this.source.size();
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        if (size > 0) {
            int nextIdx = random.nextInt(size);
            int lastIdx = --size;

            T last = source.get(lastIdx);
            T elem = source.set(nextIdx, last);
            action.accept(elem);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Spliterator<T> trySplit() {
        return null;
    }

    @Override
    public long estimateSize() {
        return source.size();
    }

    @Override
    public int characteristics() {
        return SIZED;
    }
}

接着:

public final class RandomCollectors {

    private RandomCollectors() {
    }

    public static <T> Collector<T, ?, Stream<T>> toImprovedLazyShuffledStream() {
        return Collectors.collectingAndThen(
          toCollection(ArrayList::new),
          list -> !list.isEmpty()
            ? StreamSupport.stream(new ImprovedRandomSpliterator<>(list, Random::new), false)
            : Stream.empty());
    }

    public static <T> Collector<T, ?, Stream<T>> toEagerShuffledStream() {
        return Collectors.collectingAndThen(
          toCollection(ArrayList::new),
          list -> {
              Collections.shuffle(list);
              return list.stream();
          });
    }
}

我在这里解释了性能方面的考虑:https://4comprehension.com/implementing-a-randomized-stream-spliterator-in-java/

3
为了有效地执行洗牌操作,您需要提前准备好所有值。您可以在将流转换为列表后使用Collections.shuffle(),就像在Scala中一样。

1
我该如何将 IntStream 转换为 List<Integer> - deamon
@deamon 请查看安德烈的回答。如果您只是使用循环来构建数组,那当然会更简单和更快。 - Peter Lawrey
谢谢。我只是在玩Java 8,所以目前性能并不是那么重要。 - deamon
@deamon Java仍然不如Scala功能强大 :| - Peter Lawrey

1
List<Integer> randomShuffledRange(int startInclusive, int endExclusive) {
    return new Random().ints(startInclusive, endExclusive)
            .distinct()
            .limit(endExclusive-startInclusive)
            .boxed()
            .collect(Collectors.toList());
}

var shuffled = randomShuffledRange(1, 10);
System.out.println(shuffled);

示例输出:

[4, 6, 8, 9, 1, 7, 3, 5, 2]

1
如果你只需要一种“仅流式处理”的解决方案,并且确定性的、仅仅是“偶然”的排序与“随机”排序相比足够好,那么你可以通过哈希值对你的 int 进行排序。
List<Integer> xs=IntStream.range(0, 10)
    .boxed()
    .sorted( (a, b) -> a.hashCode() - b.hashCode() )
    .collect(Collectors.toList());

如果你更喜欢使用int[]而不是List<Integer>,那么你可以在之后将它们解包。不幸的是,如果要应用自定义的Comparator,则必须经过装箱步骤,因此无法消除该过程的这一部分。
List<Integer> ys=IntStream.range(0, 10)
    .boxed()
    .sorted( (a, b) -> a.hashCode() - b.hashCode() )
    .mapToInt( a -> a.intValue())
    .toArray();

1
我认为这不会起作用。问题在于Integer :: hashCode返回与Integer :: intValue相同的内容。(这在javadoc中有说明!)即使您使用System :: identityHashCode,由于几个原因,洗牌的随机性也将依赖于系统。 - Stephen C

-2
这是我的一行代码解决方案: 我正在随机选择一种颜色:
colourRepository.findAll().stream().sorted((o1,o2)-> RandomUtils.nextInt(-1,1)).findFirst().get()

5
这个 Comparator 违反了 Comparator 的一般契约。比较相同的两个值是不一致的,并且比较 xy 与比较 yx 并不一定返回相反的结果。通过正确的输入和随机种子,我能够使得这种排序方式抛出异常:java.lang.IllegalArgumentException: Comparison method violates its general contract! 在我的Java版本中,一个具体的例子是:Random r = new Random(9); Collections.nCopies(32, 1).stream().sorted((o1, o2) -> r.nextInt(3) - 1).findFirst().get() - M. Justin

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