【问题标题】:Shuffle a list of integers with Java 8 Streams API使用 Java 8 Streams API 打乱整数列表
【发布时间】:2013-11-18 21:31:29
【问题描述】:

我尝试使用 Streams API 将以下 Scala 行转换为 Java 8:

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

为了用 Java 编写等价的代码,我创建了一个整数范围:

IntStream.range(1, 25)

我怀疑在stream API中找到了toList方法,但是IntStream只知道奇怪的方法:

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

如何使用 Java 8 Streams API 打乱列表?

【问题讨论】:

    标签: java scala java-stream


    【解决方案1】:

    给你:

    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]
    

    【讨论】:

    • 在实践中,最好使用 Integer::valueOf() 进行映射,以便利用 Integer 缓存,即使对于这么小的范围几乎不重要。
    • 我编辑了答案以使用IntStream.boxed(),这非常规范。
    • 我发帖时没有看到这个答案,也不知道boxed();这比我的要好,尽管我认为您可以静态导入 Collectors.toList 并且可以让编译器推断列表的类型。
    • 呃。这不是一个真正的流解决方案。
    • 这不能保证按照the API documentation 所写的那样工作,因为toList() 不保证返回的List 的可变性。这在实践中有效,因为toList() 的当前实现恰好返回一个ArrayList,它是可变的。为了确保在实现发生变化时继续保持正确性,我们可以显式声明集合类型 (Collectors.toCollection(ArrayList::new)),或将 toList 的结果复制到可变列表 (new ArrayList&lt;&gt;(integers)) 中。
    【解决方案2】:

    您可能会发现以下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
    

    【讨论】:

    • 这应该是答案
    • 这不能保证按照the API documentation 所写的那样工作,因为toList() 不保证返回的List 的可变性。这在实践中有效,因为toList() 的当前实现恰好返回一个ArrayList,它是可变的。为了确保在实现发生变化时继续保持正确性,我们可以显式声明集合类型 (Collectors.toCollection(ArrayList::new)),或将 toList 的结果复制到可变列表 (new ArrayList&lt;&gt;(integers)) 中。
    • @M.Justin 谢谢,我已经编辑了答案。这是关于 Stream API 最令人愤怒的事情之一。它不保证返回列表的可变性或线程安全,甚至不保证它是否具有 O(1) get 方法。连同这么多的 Stream API,这简直太糟糕了。将论点推向其合乎逻辑的结论,返回列表中您应该实际使用的唯一方法是 sizeiterator
    • @PaulBoddington 更进一步,值得注意的是,一些特殊的集合实现甚至没有 O(1) size 操作(尽管这些绝对是异常值) — docs.oracle.com/javase/8/docs/api/java/util/concurrent/…跨度>
    【解决方案3】:

    您可以使用自定义比较器按随机值“排序”值:

    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());
    

    此解决方案的优势在于它集成在流管道中。

    【讨论】:

    • 查看this answer 以获得不错的选择。
    • 由于排序算法的更改,此答案不再有效:bugs.java.com/bugdatabase/view_bug.do?bug_id=6804124 当比较器不遵守基本合同时可能会抱怨。
    • 排序算法的变化发生在 2009 年,所以这里没有什么新鲜事。你有例子吗?我看到的唯一问题是在 random.nextInt() 中(不太可能)发生冲突的情况下与 equals 不一致,即使这样也不应该阻止洗牌的发生。
    • @user1928596 比较器违反了什么合同?由于它存储每个值的映射并进行比较,因此它满足Comparator.compareTo 的要求中描述的所有标准。
    • 如果列表中有重复的元素,这不会完全洗牌,因为它们总是会彼此相邻。例如,列表[1,5,1,1,5] 将变为[5,5,1,1,1][1,1,1,5,5]
    【解决方案4】:

    如果您想轻松处理整个 Stream,您可以使用 Collectors.collectingAndThen() 创建自己的 Collector:

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

    但是,如果您想limit() 生成的 Stream,这将不会很好。为了克服这个问题,可以创建一个自定义 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/

    【讨论】:

      【解决方案5】:

      要有效地执行随机播放,您需要提前获取所有值。在将流转换为列表后,您可以使用 Collections.shuffle(),就像在 Scala 中所做的那样。

      【讨论】:

      • 我如何将IntStream 转换为List&lt;Integer&gt;
      • @deamon 查看安德烈的回答。当然如果只使用循环来构建数组会更简单更快。
      • 谢谢。我只是在玩 Java 8,所以目前性能并不是那么重要。
      • @deamon Java 的功能仍然不如 Scala :|
      【解决方案6】:
      public static List<Integer> getSortedInRandomOrder(List<Integer> list) {
          return list
                  .stream()
                  .sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
                  .collect(Collectors.toList());
      }
      

      【讨论】:

      • 这是个坏主意。提供的比较器会破坏导致运行时异常的比较器合同。不要这样做:)
      • @JanZyka 你能提供重现运行时异常的方法吗?
      • 鉴于它是随机的,没有确定性的方式来重现它。但是我正在尝试使用约 500 个元素的列表并经常遇到异常。有了更多元素(数千个),我会说会有很好的机会获得例外
      【解决方案7】:

      如果您正在寻找“仅流式传输”解决方案和确定性的、仅“随意”排序而不是“随机”排序就足够了,您始终可以按哈希值对您的 ints 进行排序:

      List<Integer> xs=IntStream.range(0, 10)
          .boxed()
          .sorted( (a, b) -> a.hashCode() - b.hashCode() )
          .collect(Collectors.toList());
      

      如果您更喜欢int[] 而不是List&lt;Integer&gt;,您可以在之后将它们拆箱。不幸的是,您已经通过装箱步骤来应用自定义 Comparator,因此没有消除该过程的一部分。

      List<Integer> ys=IntStream.range(0, 10)
          .boxed()
          .sorted( (a, b) -> a.hashCode() - b.hashCode() )
          .mapToInt( a -> a.intValue())
          .toArray();
      

      【讨论】:

      • 我认为这行不通。问题是Integer::hashCode 返回与Integer::intValue 相同的内容。 (它在 javadoc 中!)即使您使用 System::identityHashCode,由于几个原因,随机播放的随机性也将取决于系统。
      【解决方案8】:
      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]
      

      【讨论】:

        【解决方案9】:

        这是我的单行解决方案: 我正在选择一种随机颜色:

        colourRepository.findAll().stream().sorted((o1,o2)-> RandomUtils.nextInt(-1,1)).findFirst().get()
        

        【讨论】:

        • 这个Comparator 违反了Comparator 的一般合同。比较相同的两个值并不一致,将xyyx 进行比较不一定会返回相反的结果。使用正确的输入和随机种子,我能够以这种方式进行排序以引发异常:java.lang.IllegalArgumentException: Comparison method violates its general contract!。在我的 Java 版本中,一个具体的例子是:Random r = new Random(9); Collections.nCopies(32, 1).stream().sorted((o1, o2) -&gt; r.nextInt(3) - 1).findFirst().get()
        猜你喜欢
        • 1970-01-01
        • 2018-03-26
        • 2020-06-27
        • 2018-02-22
        • 1970-01-01
        • 2020-07-03
        • 2016-11-16
        • 2022-12-18
        • 2019-01-19
        相关资源
        最近更新 更多