从枚举中随机选择一个值?

217

如果我有这样一个枚举:

public enum Letter {
    A,
    B,
    C,
    //...
}

如何随机选择一个元素?不需要高强度的保证,但较为均匀的分布会更好。

我可以像这样实现:

private Letter randomLetter() {
    int pick = new Random().nextInt(Letter.values().length);
    return Letter.values()[pick];
}

但是有更好的方法吗?我觉得这是一个已经被解决过的问题。


你认为你的解决方案有什么问题吗?在我看来,它看起来相当不错。 - President James K. Polk
2
@GregS - 问题在于每次调用Letter.values()都必须创建一个内部Letter值数组的新副本。 - Stephen C
还不确定每次执行该函数时是否应该创建一个新的Random对象。 - ddcruver
16个回答

178

我唯一建议的是缓存values()的结果,因为每次调用都会复制一个数组。此外,不要每次创建一个Random实例,保留一个即可。除此之外,你所做的都没问题。因此,代码应该如下所示:

public enum Letter {
  A,
  B,
  C,
  //...

  private static final List<Letter> VALUES =
    Collections.unmodifiableList(Arrays.asList(values()));
  private static final int SIZE = VALUES.size();
  private static final Random RANDOM = new Random();

  public static Letter randomLetter()  {
    return VALUES.get(RANDOM.nextInt(SIZE));
  }
}

8
如果您认为这很有用,您可以创建一个实用类来完成此操作。类似于RandomEnum<T extends Enum>,其构造函数接收Class<T>以创建列表。 - helios
18
我认为将values()数组转换为不可修改的列表没有意义。由于VALUES对象已经被声明为private,它已经被封装。将其声明为private static final Letter[] VALUES = ...会更简单和高效。 - Stephen C
5
Java中的数组是可变的,因此如果你有一个数组字段并在公共方法中返回它,调用者可以修改它并修改私有字段,所以你需要进行防御性拷贝。如果你多次调用该方法,这可能会成为一个问题,因此最好将其放入不可变列表中,以避免不必要的防御性拷贝。 - cletus
2
@cletus:Enum.values()每次调用都会返回一个新的数组,因此在传递/使用它之前没有必要将其包装起来。 - Chii
5
"private static final Letter[] VALUES..." 是可以的。它是私有的,因此是不可变的。你只需要公共的 randomLetter() 方法,它显然返回单个值。Stephen C 是正确的。 - helios
显示剩余2条评论

168

一个方法就足以处理所有随机枚举:

    public static <T extends Enum<?>> T randomEnum(Class<T> clazz){
        int x = random.nextInt(clazz.getEnumConstants().length);
        return clazz.getEnumConstants()[x];
    }

你将使用哪些技术:

randomEnum(MyEnum.class);

我也更喜欢使用SecureRandom,因为:

private static final SecureRandom random = new SecureRandom();

1
正是我所需要的。我一直在按照被接受的答案进行操作,但当我需要从我的第二个枚举中随机选择时,这给我留下了样板代码。此外,有时很容易忘记使用SecureRandom。谢谢。 - Siamaster
你读懂了我的心思,正是我在寻找的东西,可以添加到我的随机实体生成器测试类中。感谢您的帮助。 - Roque Sosa

76

单行

return Letter.values()[new Random().nextInt(Letter.values().length)];

2
简单易懂 - CJDownUnder
5
应该是最受欢迎的答案! - Y-B Cause
每次都重新创建Random并不高效。可以使用ThreadLocalRandom代替。Letter.values()[ThreadLocalRandom.current().nextInt(Letter.values().length)] - Basil Bourque

49

根据 cletushelios 的建议进行组合。

import java.util.Random;

public class EnumTest {

    private enum Season { WINTER, SPRING, SUMMER, FALL }

    private static final RandomEnum<Season> r =
        new RandomEnum<Season>(Season.class);

    public static void main(String[] args) {
        System.out.println(r.random());
    }

    private static class RandomEnum<E extends Enum<E>> {

        private static final Random RND = new Random();
        private final E[] values;

        public RandomEnum(Class<E> token) {
            values = token.getEnumConstants();
        }

        public E random() {
            return values[RND.nextInt(values.length)];
        }
    }
}

编辑:糟糕,我忘记了有界类型参数 <E extends Enum<E>>


1
非常老的答案,但是应该是 E extends Enum<E> 吧? - Lino
1
@Lino:为了更清晰,我进行了编辑;我不认为正确的类型推断需要参数边界,但我欢迎纠正;还要注意自Java 7以来允许new RandomEnum<>(Season.class) - trashgod
如果您想将其捆绑并发布到中央仓库,那么这个单独的 RandomEnum 类作为一个微型库会非常不错。 - Greg Chabala

19

简单的 Kotlin 解决方案

MyEnum.values().random()

random()是在基础Kotlin的Collection对象中包含的默认扩展函数。Kotlin文档链接

如果你想要使用扩展函数简化它,可以尝试这个:

inline fun <reified T : Enum<T>> random(): T = enumValues<T>().random()

// Then call
random<MyEnum>()

要使枚举类静态。请确保在您的枚举文件中导入my.package.random

MyEnum.randomValue()

// Add this to your enum class
companion object {
    fun randomValue(): MyEnum {
        return random()
    }
}

如果您需要从枚举实例进行操作,请尝试使用以下扩展方法

inline fun <reified T : Enum<T>> T.random() = enumValues<T>().random()

// Then call
MyEnum.VALUE.random() // or myEnumVal.random() 

1
这个问题没有标记为Kotlin,而且特别涉及Java。虽然很好,但是这个答案似乎不适用于手头的问题。 - E-Riz

11
Letter lettre = Letter.values()[(int)(Math.random()*Letter.values().length)];

10

同意Stphen C和helios的观点。从Enum中获取随机元素的更好方法是:

public enum Letter {
  A,
  B,
  C,
  //...

  private static final Letter[] VALUES = values();
  private static final int SIZE = VALUES.length;
  private static final Random RANDOM = new Random();

  public static Letter getRandomLetter()  {
    return VALUES[RANDOM.nextInt(SIZE)];
  }
}

6

最简单的方法可能是编写一个函数,从数组中随机选择一个值。这种方法更通用,并且调用起来很直接。

<T> T randomValue(T[] values) {
    return values[mRandom.nextInt(values.length)];
}

这样调用:

MyEnum value = randomValue(MyEnum.values());

6
这里有一个使用洗牌和流的版本。
List<Direction> letters = Arrays.asList(Direction.values());
Collections.shuffle(letters);
return letters.stream().findFirst().get();

5

这可能是实现您目标的最简洁方式。您只需要调用Letter.getRandom(),就能获得一个随机枚举字母。

public enum Letter {
    A,
    B,
    C,
    //...

    public static Letter getRandom() {
        return values()[(int) (Math.random() * values().length)];
    }
}

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