【问题标题】:Copy instance variable of type java.util.Random to create object in same state复制 java.util.Random 类型的实例变量以创建相同状态的对象
【发布时间】:2013-08-28 16:22:59
【问题描述】:

我正在实现 simulated annealing (SA) 算法,我需要在其中复制状态(例如,记住迄今为止的最佳解决方案)。

我实现了一个复制方法,因为不鼓励使用java的clone()

SA 是一种启发式算法,因此下一步要采取的措施是随机确定的。这是通过使用Random 对象来完成的,我也想复制它。

虽然算法不需要它,但我希望副本具有完全相同的状态。 但这只是这种情况,如果我在创建对象后直接进行“复制”并使用相同的种子对其进行初始化。

但是如果我在复制过程之前随机执行一些操作,Random 对象的内在状态(即种子)会发生变化,并且复制的行为会有所不同。

那么我怎样才能获得java.util.Random 实例的精确副本?


示例

public class State
{
  private final Random r;
  private final long seed;

  private Object currentOperand;

  public State()
  {
    this(System.nanoTime(), null);
  }

  private State(long seed, Object currentOperand)
  {
    this.seed = seed;
    this.r = new Random(seed);
    this.currentOperand = currentOperand;
  }

  public State copy()
  {
    return new State(seed, currentOperand);
  }

  public void doSth()
  {
    /* operation with random operand */
    currentOperand = r.nextInt(100);
  }

  public void redo()
  {
    // redo then set to null
    currentOperand = null;
  }

  /* for completeness' sake... since it's simulated annealing */
  public int computeEnergy() { return 0; }
}

【问题讨论】:

  • 我猜想在 java doc 合同中指定,不是复制确切的状态,并指定什么是可能的,而不是遵循链接答案中的方法。
  • 我接受了我自己的答案,因为我提供的解决方案很容易(重新)使用。

标签: java algorithm random copy clone


【解决方案1】:

我想出了一个自己的解决方案。它主要覆盖Random 中的next()(因为所有其他方法都依赖于那个),以及其他一些保持一致性的东西。

它提供了调用此方法的实例的精确副本(制作随机实例的副本是否有意义是另一个话题...^^)。它的行为应该完全像它的超类,至少这是我的意图。

请随意添加您的想法!

由于其他问题是关于获取种子的:可以轻松地将getSeed() 方法添加到我的解决方案中。或getInitialSeed()getCurrentSeed()

/* Bounded parameter type since a class that implements this interface
 * should only be able to create copies of the same type (or a subtype).
 */
public interface Copyable<T extends Copyable<T>>
{
  public T copy();
}

public class CopyableRandom extends Random implements Copyable<CopyableRandom>
{
  private final AtomicLong seed = new AtomicLong(0L);

  private final static long multiplier = 0x5DEECE66DL;
  private final static long addend = 0xBL;
  private final static long mask = (1L << 48) - 1;

  public CopyableRandom() { this(++seedUniquifier + System.nanoTime()); }
  private static volatile long seedUniquifier = 8682522807148012L;

  public CopyableRandom(long seed) { this.seed.set((seed ^ multiplier) & mask); }

  /* copy of superclasses code, as you can seed the seed changes */
  @Override
  protected int next(int bits)
  {
    long oldseed, nextseed;
    AtomicLong seed_ = this.seed;
    do
    {
      oldseed = seed_.get();
      nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed_.compareAndSet(oldseed, nextseed));
    return (int) (nextseed >>> (48 - bits));
  }

  /* necessary to prevent changes to seed that are made in constructor */
  @Override
  public CopyableRandom copy() { return new CopyableRandom((seed.get() ^ multiplier) & mask); }

  public static void main(String[] args)
  {
    CopyableRandom cr = new CopyableRandom();

    /* changes intern state of cr */
    for (int i = 0; i < 10; i++)
      System.out.println(cr.nextInt(50));

    Random copy = cr.copy()

    System.out.println("\nTEST: INTEGER\n");
    for (int i = 0; i < 10; i++)
      System.out.println("CR\t= " + cr.nextInt(50) + "\nCOPY\t= " + copy.nextInt(50) + "\n");

    Random anotherCopy = (copy instanceof CopyableRandom) ? ((CopyableRandom) copy).copy() : new Random();
    System.out.println("\nTEST: DOUBLE\n");
    for (int i = 0; i < 10; i++)
      System.out.println("CR\t= " + cr.nextDouble() + "\nA_COPY\t= " + anotherCopy.nextDouble() + "\n");
  }
}

这里是main方法的输出:

19
23
26
37
41
34
17
28
29
6

TEST: INTEGER

CR      = 3
COPY    = 3

CR      = 18
COPY    = 18

CR      = 25
COPY    = 25

CR      = 9
COPY    = 9

CR      = 24
COPY    = 24

CR      = 5
COPY    = 5

CR      = 15
COPY    = 15

CR      = 5
COPY    = 5

CR      = 30
COPY    = 30

CR      = 26
COPY    = 26


TEST: DOUBLE

CR      = 0.7161924830704971
A_COPY  = 0.7161924830704971

CR      = 0.06333509362539957
A_COPY  = 0.06333509362539957

CR      = 0.6340753697524675
A_COPY  = 0.6340753697524675

CR      = 0.13546677259518425
A_COPY  = 0.13546677259518425

CR      = 0.37133033932410586
A_COPY  = 0.37133033932410586

CR      = 0.796277965335522
A_COPY  = 0.796277965335522

CR      = 0.8610310118615391
A_COPY  = 0.8610310118615391

CR      = 0.793617231340077
A_COPY  = 0.793617231340077

CR      = 0.3454111197621874
A_COPY  = 0.3454111197621874

CR      = 0.25314618087856255
A_COPY  = 0.25314618087856255

我还进行了一项测试,将 CopyableRandom 与 Random 进行了比较。它产生了相同的结果。

long seed = System.nanoTime();

Random cr  = new CopyableRandom(seed);
Random cmp = new Random(seed);

【讨论】:

  • 目前我看到的唯一问题是Random 实现了Serializable。我觉得我应该做点什么。如果有人想阻止它,Sun 建议抛出 NotSerializableException...
  • 这可能会导致返回不同序列的高斯样本,因为正态分布在 nextGaussian() 方法的每一秒调用中一次采样两个,其中一个样本被记住用于下一个打电话。
【解决方案2】:

我知道这是一个已被接受的老问题,但我自己在寻找答案时遇到了这个问题,我最终采取了不同的方法。

看到上面 mike 的注释,Random 实现了Serializable,我只是用它来制作副本:

/**
 * Uses serialization to create a copy of the given Random, needed for
 * repeatability in some tests.
 */
public static Random cloneRandom(Random src) throws Exception {
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bo);
    oos.writeObject(src);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(
            new ByteArrayInputStream(bo.toByteArray()));
    return (Random)(ois.readObject());
}

它的性能可能不如 mike 的 CopyableRandom,但它要简单得多,并且足以满足我的小单元测试。

(我有一个现有的单元测试,它以具有已知种子的Random 开始,然后执行了一系列操作;我试图在测试的中间中添加一点,并想要一份Random 的副本;在测试中间调用nextLong() 或类似的方法来获取/a 种子会改变种子,从而破坏测试的其余部分。真的我只是想要@ 之类的东西987654328@.)

【讨论】:

    【解决方案3】:

    我认为你应该存储在你的 State 类中,不仅是开始种子,还有你已经调用 nextInt() 的次数。这是由于Random 生成伪随机数字序列的内在事实。那就是:

    伪随机数生成器可以使用种子状态从任意起始状态开始。此后,当使用该状态初始化时,它将始终产生相同的序列

    让我先用一个示例来解释一下:

    public static void main(String[] args){
    
         Random r = new Random(42);      
         Random s = new Random(42);
    
         for(int i = 0; i < 5; i++){
           System.out.println("First random " +r.nextInt());
         }
    
         for(int i = 0; i < 5; i++){
           System.out.println("Second random " +s.nextInt());
         }
    
      }
    

    结果是:

    First random -1170105035
    First random 234785527
    First random -1360544799
    First random 205897768
    First random 1325939940
    Second random -1170105035
    Second random 234785527
    Second random -1360544799
    Second random 205897768
    Second random 1325939940
    

    由于两个 Random 实例都以相同的种子开始,我总是得到相同的数字序列。

    因此,在复制对象时,您应该将新的 Random 初始化到相同的源种子(您已经这样做了)和 “consume” 尽可能多地调用您已经使用过的 nextInt()在源对象中(这就是为什么你必须保留这个数字)。

    完成此操作后,在副本上调用nextInt() 将为您提供与源对象相同的结果。我希望我的代码足以重构你的代码,让你理解我的解决方案。

    要更好地理解伪随机数生成器(PRNG),也称为确定性随机位生成器(DRBG),可以看一下这个Wikipedia article

    【讨论】:

    • 正如我所说的每个数字生成的随机变化的内在状态。好的,没想到让Random 执行“空运行”,直到状态相同。嗯,但不知道我应该怎么看这种干跑步......
    • 在什么意义上?如果需要,我的解决方案还允许您实现类似 previousInt() 的功能,只需创建一个新的 Random 并执行 (n-1) 次“空运行”,假设 n 是 nextInt() 的当前调用次数。在这种情况下,您甚至不需要 State 的副本来执行回滚。
    • 状态类只是一个简化的例子。不过,您的方法创建了 Random 的 1:1 副本。但我不确定这种空运行是否会成为性能问题,因为我的运行次数在 10,000 到 20,000 次之间。不知道nextInt() 电话有多贵。我也不确定“复制”一个随机数是否是个好习惯。
    • 好的。只是想猜测一下,如果你有很多这个对象的副本,并且你担心性能,你总是可以将多次运行 Random 的结果存储到一个静态数组中,然后使用从 0 访问该数组来模拟 nextInt 和增加一个索引。当然,早期优化 99.9% 是个坏主意 :)。但如前所述,这只是一个猜测。
    • 我添加了自己的解决方案,该解决方案无需“空运行”即可工作,并且没有向想要使用和复制 Random 的客户端添加任何逻辑。
    猜你喜欢
    • 1970-01-01
    • 2014-05-24
    • 2020-07-15
    • 2012-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-30
    相关资源
    最近更新 更多