我有一个用于游戏的课程。它需要生成随机偶数值,方法是生成随机数直到结果为偶数。

还有更好的方法吗?

我目前方法是否已实现为私有实例方法,但是我应该将generateRandomNumber()evenNumber(number)声明为静态方法吗?请问有什么好处吗?

public class Game {
    //...

    public void opponentSaysEvenNumber() {
        int number = generateRandomEvenNumber();
        System.out.println("Opponent: " + number);
    }

    private int generateRandomEvenNumber() {
        Random random = new Random();
        int number = random.nextInt();
        while (!evenNumber(number)) {
            number = random.nextInt();
        }
        return number;
    }

    private boolean evenNumber(int number) {
        return (number % 2) == 0;
    }
}


评论

只是想知道,为什么不将RNG收到的任何数字乘以2?

将1加到生成的奇数吗?

@lohoris因为“加1”运算要求您事先测试您是否有奇数或偶数,所以它是一个测试加一个加法运算,或者只是一个测试加一个null。 “乘以2”总是产生偶数,因此它总是一个单一的操作,可以在汇编级将其减少为“按位左移1”。我认为不可能比这快得多...

...除了可能使用位掩码将最低有效位归零外,请考虑一下。

我还建议使用左移操作,出于完整性考虑,我想指出按位与NOT(1)进行AND运算也可以(在一个操作中)。

#1 楼

是。如果可能的话,我将更进一步,将您的函数声明为private static final。这三个关键字的组合意味着该代码将不受任何实例变量,任何超类或任何子类的影响,并且也不受该类外部的任何代码的调用。因此,编译器有足够的暗示可以决定内联整个函数。

我还将evenNumber(int number)重命名为isEven(int number)。 Java中有一个约定,名为isSomething()的函数返回boolean,并且没有副作用。您的函数符合这些条件。

要生成随机偶数,只需使用random.nextInt() & -2即可掩盖最低有效数字。这比循环,测试和丢弃更有效。在那种情况下,有关辅助函数的整个问题就无关紧要。伪随机数生成器实际上携带一些状态,即使您不这样认为。因此,应使用Random变量来存储private static对象。

评论


\ $ \ begingroup \ $
final对私有静态方法不起作用。
\ $ \ endgroup \ $
– omiel
2014年1月16日13:43



\ $ \ begingroup \ $
@omiel JIT仍会内联吗?
\ $ \ endgroup \ $
– Cruncher
2014年1月16日14:41

\ $ \ begingroup \ $
静态方法不是继承的一部分,因此其他类无论如何都无法重写它们。这就是为什么final没有意义的原因。
\ $ \ endgroup \ $
– Tim B
2014年1月16日14:59

\ $ \ begingroup \ $
是否有合理的理由认为添加final可以提高将来的可读性?
\ $ \ endgroup \ $
–StingyJack
2014年1月16日15:37

\ $ \ begingroup \ $
在使用私有静态Random变量之前,请考虑以下javadoc摘录:java.util.Random的实例是线程安全的。但是,跨线程并发使用同一java.util.Random实例可能会引起争用并因此导致性能下降。考虑在多线程设计中改用ThreadLocalRandom。
\ $ \ endgroup \ $
–嘘
2014年1月16日23:07



#2 楼

声明此类方法可以提高代码的可读性。对于读者来说,很明显,该方法不依赖于该类实例的内部状态。

评论


\ $ \ begingroup \ $
更重要的是,调用静态方法要快一些(不会将其隐式传递给方法)
\ $ \ endgroup \ $
–亚当·迪加(Adam Dyga)
2014年1月16日上午10:19

\ $ \ begingroup \ $
@Adam我很确定这会从生成的字节码中得到编译。
\ $ \ endgroup \ $
– corsiKa
2014年1月17日下午0:51

#3 楼

您的技术效率低下。要生成偶数,只需执行以下任一操作即可:屏蔽最低有效位(-20xFFFFFFFE),或者屏蔽除最后一位以外的所有位)

random.nextInt() & -2;



使用移位运算符向左移一位

random.nextInt() << 1;



使用乘法向左移

random.nextInt() * 2;




评论


\ $ \ begingroup \ $
发现得很好。至少op应该写if(!evenNumber(number))number ++,因此他不会生成其他随机数
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年1月17日,0:35

\ $ \ begingroup \ $
使用位掩码是不可接受的,因为它会修改数字分布。例如:随机输出2,3变为2,2。左移或乘法是唯一可接受的方法。
\ $ \ endgroup \ $
–卡尔
2014年1月17日在3:48



\ $ \ begingroup \ $
@Carl不,它不是(至少对于正整数不是-对于负或正整数,可能会在零附近略有偏差,但这不适用于此处)。所有偶数仍然以相同的概率出现(它们都可以由PRNG输出自己或紧随其后的数字输出)。
\ $ \ endgroup \ $
–托马斯
2014年1月17日4:48



\ $ \ begingroup \ $
有一个小缺陷,当您乘法或左移时,需要考虑到您正在使用整数而不是无符号整数,并且溢出会导致符号改变。
\ $ \ endgroup \ $
–CoffeDeveloper
2014年1月17日在9:48



\ $ \ begingroup \ $ 从这个角度来看,您是对的,但大多数随机数仅假设为正数。以用户希望从0到6的数字为例,它将用%乘以7。但是由于还使用%符号规则返回了负整数,因此实际结果是[-6,6]范围内的数字。
\ $ \ endgroup \ $
–CoffeDeveloper
2014年1月21日在18:19

#4 楼

如果您不使用该类的任何属性,则意味着您可能应该将此方法放在其他地方。也许在另一堂课中,例如RandomEvenNumberGenerator

我一直认为静态是一种不好的做法。它打开了通往许多问题,语义问题和“我知道你住的地方”综合症的道路,这反过来又使测试更加困难等。

评论


\ $ \ begingroup \ $
您能否详细说明在单元测试中“我知道您住的地方”综合症? Google并没有帮助我解决这一问题。
\ $ \ endgroup \ $
–安德里斯
2014年1月16日上午10:44

\ $ \ begingroup \ $
单元测试是测试每个组件,每个组件与其他组件完全分开,以避免任何不可控制的副作用。通常,您将很难测试使用单例的组件,因为您无法轻松地将单例替换为您控制的东西。该组件“知道单身人士的住所”并直接寻址。更好的方法是将单例实例提供给需要它的对象。在给出的代码中,它并没有真正受到伤害,但是拥有静态方法会鼓励这种编码方式。
\ $ \ endgroup \ $
– Antoine_935
2014年1月16日上午10:59

\ $ \ begingroup \ $
不,如果该方法在该类中通过其方法使用,并且在其他地方都没有使用,则它当然应该是静态私有的。这比拥有一个充满不同的助手功能的静态类或为每个助手功能使用不同的类要有意义得多。非静态类中的静态非私有方法则不同。您反对单身人士的论点在这里并不适用。
\ $ \ endgroup \ $
– jwg
2014年1月16日15:49



\ $ \ begingroup \ $
以这种方式用于帮助程序功能的“私有静态”是毫无意义且具有误导性的。可以将其视为不良做法。 “静态”一词增加了不必要的句法噪音,可能被警告为某处隐藏了某种单例机制。
\ $ \ endgroup \ $
– Ichthyo
2014年1月16日在22:42

\ $ \ begingroup \ $
@jwg,我从不宣传Utility类。当然不是。在此类中将此方法设为静态时,每次调用该方法时都会强制重新创建一个新的Random实例,否则我们需要将其共享为静态属性(危险!)。我同意Ichthyo的观点,因为它会增加不必要的代码噪音。
\ $ \ endgroup \ $
– Antoine_935
2014年1月17日9:11

#5 楼

为什么不将生成的第一个随机数加倍并返回呢?您仍将拥有随机数,它们的分布没有受到破坏,并且分布更快。

public class Game {
    //...

    public void opponentSaysEvenNumber() {
        int number = generateRandomEvenNumber();
        System.out.println("Opponent: " + number);
    }

    private int generateRandomEvenNumber() {
        Random random = new Random();
        int number = random.nextInt();
        return number * 2;
    }
}


#6 楼

考虑使用ThreadLocalRandom

private int generateRandomEvenNumber() {
    return ThreadLocalRandom.current().nextInt() & -2;
}


Java 7中添加了ThreadLocalRandom并解决了Random对象的一些性能问题,尤其是在多线程环境中。即使您不在多胎环境中,我也认为每次都使用它是一个好主意。此外,Api比旧的Random更好(您不必创建对象,只需使用静态方法即可)。

使用& -2(或@wizzi poo提出的任何其他方法)将通过将生成的数字的最后一位设置为0,确保生成的数字始终是偶数。这比做一会儿要好,因为总有机会Random永远不会生成偶数(极不可能)。

#7 楼

您不想掩盖使您的元素均匀。有一个奇数个正值和一个奇数个负值。通过使它们略有偏差,可以使您陷入循环(无双关语)。相反,您想要提供一个最小值和最大值,并在该范围内找到一个偶数。您想通过移位(实际上是除法和乘法)来做到这一点,但是因为我们处理的是2的幂,所以可以通过移位来做到这一点,而不是四舍五入(本质上就是屏蔽)。

作为一般的经验法则,您不想在不需要时丢弃随机位。

private static final Random rand = new Random(); 

public static int randomEvenNumber(int min, int max) {
    if(min % 2 != 0) throw new IllegalArgumentException("Minimum value must be even");
    if(max % 2 != 0) throw new IllegalArgumentException("Maximum value must be even");
    int range = max - min;
    int rangeHalf = range >> 1; // divided by 2
    int randomValue = rand.nextInt(rangeHalf);
    randomValue = randomValue << 1; // multiplied by 2 to make it even
    return min + randomValue;
}

public static final int randomEvenNumber(int max) {
    return randomEvenNumber(0,max);
}


评论


\ $ \ begingroup \ $
公平地说,如果您非常关心偏见,则可能不想使用java.util.Random开始。如果您执行* 2和/ 2而不是进行移位,那么这也将更具可读性,并且同样有效。
\ $ \ endgroup \ $
–克里斯·海斯(Chris Hayes)
2014年1月17日,下午3:17

\ $ \ begingroup \ $
这取决于您认为什么是“公平”的。屏蔽LSB(或偏移MSB)确实会从所有可表示的偶数整数集中以均匀的概率选择数字。但是,由于可表示的负偶数比可表示的正偶数多,所以您正确地注意到分布并不完全以0为中心。
\ $ \ endgroup \ $
– 200_success
2014年1月17日下午7:02

\ $ \ begingroup \ $
为什么不对min和max抛出异常,为什么不使用舍入值呢?
\ $ \ endgroup \ $
–汉克·兰格维德
2014年1月17日12:21

\ $ \ begingroup \ $
@Henek最小值和最大值参数通常使用以下协定:最小值是可接受的最低值,最大值是大于可接受的最高值的第一个值。如果min不为偶数,则它不会成为破坏合同的可接受值。 max的情况与此类似。有人可能会说这些条件不属于那里。例如,randomEvenNumber(9,19)可能具有解集{10,12,14,16,18}-逻辑会更加复杂(确定min是否为奇数并相应地进行调整),但不涉及舍入。
\ $ \ endgroup \ $
– corsiKa
2014年1月17日下午16:45

#8 楼



随机生成器方法(generateRandomEvenNumber()和evenNumber())本质上是数学的,实际上并不是特定于游戏的。它们不依赖于游戏类(例如,它们不使用字段)并且没有副作用。

我的方法是做类似的事情。使用这些方法创建一个RandomService类,并将服务实例注入游戏。使用Spring或其他DI(依赖注入)框架,可以将服务创建为单例。 RandomService上的方法可以是公共的,因为它们没有依赖关系或副作用,并且易于与游戏分开进行测试。

#9 楼

如果要集中生成这些数字,则声明一个静态且唯一的随机数生成器方法是一个不错的选择。但是,如果您打算在多线程环境中运行代码,请不要忘记将方法标记为synchronized

还应该将Random实例声明为private final static

最后,由于evenNumber方法包含一个测试,因此您可以将该测试移至generateRandomEvenNumber方法,并让类仅由两个方法组成。您想要集中生成那些随机数的方法是创建一个Singleton随机数生成器。

评论


\ $ \ begingroup \ $
我同意您的看法,generateRandomEvenNumber应该移出该类,因为它与Game类的核心问题无关。但是我对evenNumber的内联不同意。您通过名称松散了自我文档,并强迫用户弄清楚代码的作用。这将违反“单一抽象级别”的原则。
\ $ \ endgroup \ $
– Ichthyo
2014年1月16日23:03

\ $ \ begingroup \ $
贾马尔(Jamal)关于“抽象的单一层次”原理很高兴。
\ $ \ endgroup \ $
–user3067411
2014年1月19日19:08

#10 楼

另一个选择是在随机数为奇数的情况下加减1。

...
number = random.nextInt();
if (number % 2 == 1) {
    number++;
    number %= MAX_NUMBER;   // assuming upper limit is required
}
...