概述

在这个快速教程中,我们将学习如何使用Java核心类生成不重复的随机数。首先,我们将从头开始实现一些解决方案,然后利用Java 8及更高版本的功能,以获得更可扩展的方法。

2. 从小范围生成随机数

如果我们需要的数字范围较小,我们可以不断将递增的数字添加到列表中,直到达到大小n。然后,我们调用Collections.shuffle()方法,它的时间复杂度是线性的。之后,我们就得到了一个随机排列的独特数字列表。让我们创建一个实用类来生成并使用这些数字:

public class UniqueRng implements Iterator<Integer> {
    private List<Integer> numbers = new ArrayList<>();

    public UniqueRng(int n) {
        for (int i = 1; i <= n; i++) {
            numbers.add(i);
        }

        Collections.shuffle(numbers);
    }
}

创建对象后,我们就会得到从1到n的随机顺序数字。注意我们实现了Iterator,因此每次调用next()时都会得到一个随机数。此外,我们还可以通过hasNext()检查是否还有数字剩余。因此,我们需要重写它们:

@Override
public Integer next() {
    if (!hasNext()) {
        throw new NoSuchElementException();
    }
    return numbers.remove(0);
}

@Override
public boolean hasNext() {
    return !numbers.isEmpty();
}

remove()方法会返回列表中移除的第一个项目。同样地,如果我们没有对集合进行随机排序,我们也可以传入一个随机索引。但提前在构造时打乱顺序的好处是可以预先知道整个序列。

2.1. 实际应用

使用时,只需选择需要的数字并消耗它们:

UniqueRng rng = new UniqueRng(5);
while (rng.hasNext()) {
    System.out.print(rng.next() + " ");
}

这可能会产生如下输出:

4 1 2 5 3

3. 从大范围生成随机数

如果需要的数字范围更大,但只使用其中一部分,我们需要采取不同的策略。首先,我们不能依赖于向ArrayList中添加随机数,因为这可能导致重复。因此,我们将使用Set,因为它保证了唯一的项。然后,我们将使用LinkedHashSet实现,因为它维护插入顺序。

这次,我们将在一个循环中向集合中添加元素,直到达到大小n。同时,我们将使用Random生成0到max之间的随机整数:

public class BigUniqueRng implements Iterator<Integer> {
    private Random random = new Random();
    private Set<Integer> generated = new LinkedHashSet<>();

    public BigUniqueRng(int size, int max) {
        while (generated.size() < size) {
            Integer next = random.nextInt(max);
            generated.add(next);
        }
    }
}

请注意,我们不需要检查数字是否已经在集合中,因为add()方法会处理这个。由于无法通过索引删除项目,我们需要借助Iterator来实现next()

public Integer next() {
    Iterator<Integer> iterator = generated.iterator();
    Integer next = iterator.next();
    iterator.remove();
    return next;
}

4. 利用Java 8+功能

虽然自定义实现更易于重用,但我们可以通过使用Stream创建一个解决方案。从Java 8开始,Random有一个ints()方法,它返回一个IntStream。我们可以流式处理它,并像之前一样设定要求,如范围和限制。让我们结合这些特性,将结果收集到一个Set中:

Set<Integer> set = new Random().ints(-5, 15)
  .distinct()
  .limit(5)
  .boxed()
  .collect(Collectors.toSet());

遍历的集合可能会输出类似以下内容:

-5 13 9 -4 14

使用ints(),从负整数开始的范围更加简单。但必须小心,避免生成无限流,例如如果不调用limit()

5. 总结

在这篇文章中,我们为两种场景编写了解决方案,生成不重复的随机数。首先,我们使这些类可迭代,以便轻松消费它们。然后,我们使用流创建了一个更自然的解决方案。

如往常一样,源代码可以在GitHub上找到。