Java中惰性“运行一次”初始化的方法和单元测试的覆盖

Java中惰性“运行一次”初始化的方法和单元测试的覆盖,java,multithreading,singleton,lazy-evaluation,Java,Multithreading,Singleton,Lazy Evaluation,我正在寻找一段行为有点像单例的代码,但不是因为单例不好:我所寻找的必须满足以下目标: 线程安全 简单理解和使用,即几行代码。图书馆电话没问题 快速的 不是单身;对于测试,必须能够覆盖该值,并在测试后将其重置。 本地所有必要信息必须放在一个地方 仅当实际需要该值时才运行。 在RHS上运行一次代码必须执行一次且仅执行一次 示例代码: private int i = runOnce(5); // Set i to 5 // Create the connection once and cache th

我正在寻找一段行为有点像单例的代码,但不是因为单例不好:我所寻找的必须满足以下目标:

线程安全 简单理解和使用,即几行代码。图书馆电话没问题 快速的 不是单身;对于测试,必须能够覆盖该值,并在测试后将其重置。 本地所有必要信息必须放在一个地方 仅当实际需要该值时才运行。 在RHS上运行一次代码必须执行一次且仅执行一次 示例代码:

private int i = runOnce(5); // Set i to 5
// Create the connection once and cache the result
private Connection db = runOnce(createDBConnection("DB_NAME"));

public void m() {
    String greet = runOnce("World");
    System.out.println("Hello, "+greet+"!");
}
请注意,字段不是静态的;只有表达式的RHS右侧是。。。嗯,在某种程度上是静态的。测试应该能够为i和greet注入新的值

还要注意,这段代码概述了我打算如何使用这段新代码。您可以随意用任何东西替换runOnce,或者将其移动到其他位置—可能是构造函数,也可能是init方法或getter。但LOC越少越好

一些背景资料:

我不是在寻找Spring,我在寻找一段代码,它可以用于最常见的情况:您需要实现一个接口,并且除了您希望传递模拟对象的测试之外,再也不会有第二个实现。另外,Spring失败了2、3和5:您需要学习配置语言,必须在某个地方设置应用程序上下文,它需要一个XML解析器,而不是本地信息到处传播

由于5,全局配置对象或工厂不符合要求

静态决赛已出局,因为4无法更改决赛。静态的气味是因为类加载器的问题,但您可能需要在runOnce中使用它。我只是希望能够在表达式的LHS中避免它

一种可能的解决方案是使用默认设置,该设置将返回相同的对象。因为我可以把东西放在缓存中,这也允许在任何时候覆盖该值。但也许有一个比ehcache更紧凑/简单的解决方案,它同样需要一个XML配置文件,等等

[编辑]我想知道为什么这么多人对此投了反对票。这是一个有效的问题,至少在我的代码中,用例非常常见。所以,如果你不理解问题或背后的原因,或者你没有答案或你不在乎,为什么要投否决票/

[EDIT2]如果你看看Spring的应用程序上下文,你会发现99%以上的bean都只有一个实现。你可以拥有更多,但实际上,你根本没有。因此,我没有将接口、实现和配置分开,而是考虑在最简单的情况下只有一个实现,一个当前方法和一行或两行聪明的代码,在第一次调用时为当前结果初始化一次,但同时允许重写线程安全的结果,如果可能的话。将其视为一个原子ifo==null o=new o;返回o,您可以在其中覆盖o。也许一个AtomicRunOnceReference类就是解决方案


现在,我只是觉得我们每天都拥有和使用的东西不是最好的,有一个令人困惑的解决方案,它会让我们都拍拍脑袋说就这样。正如几年前Spring出现时我们所感受到的那样,我们意识到我们所有的单例问题来自何处以及如何解决它们。

线程安全初始化代码imho的最佳习惯用法是懒惰的内部类。经典版本是

class Outer {
  class Inner {
    private final static SomeInterface SINGLETON;

    static {
      // create the SINGLETON
    }
  }

  public SomeInterface getMyObject() {
    return Inner.SINGLETON;
  }
}
因为它是线程安全的,延迟加载,而且非常优雅

现在您需要可测试性和可替换性。在不知道它到底是什么的情况下很难给出建议,但最明显的解决方案是使用依赖项注入,特别是如果您使用的是Spring并且有应用程序上下文的话


这样,您的单例行为由一个接口表示,您只需将其中一个注入到相关类或工厂中生成一个接口,然后您当然可以将其替换为任何您喜欢的用于测试目的的方法。

我认为您可以使用的一个解决方案是提供一个受保护的方法,该方法可以在测试我以前用于测试遗留代码的解决方案

比如:

private SomeObject object;

protected SomeObject getObject() {
   if (object == null) {
       object = new SomeObject();
   }
   return object;
}
然后在测试课上,您可以执行以下操作:

public void setUp() {
   MyClassUnderTest cut = new MyClassUserTest() {
      @Override
      protected SomeObject getObject() }
         return mockSomeObject;
      }
   };
}

我不得不说,我对这种模式不太感兴趣,因为它暴露了您可能并不真正想要的受保护字段,但它有助于您摆脱注入不可用的情况

您可以使用IoC技术,即使您不使用IoC框架Spring/Guice/。。。。在我看来,这是避免单身的唯一干净方法。

以下是满足我所有要求的解决方案:

/** Lazy initialization of a field value based on the (correct)
* double checked locking idiom by Joschua Bloch
*
* <p>See "Effective Java, Second Edition", p. 283
*/
public abstract class LazyInit<T>
{
    private volatile T field;

    /** Return the value.
    *
    *  <p>If the value is still <code>null</code>, the method will block and
    *  invoke <code>computeValue()</code>. Calls from other threads will wait
    *  until the call from the first thread will complete.
    */
    @edu.umd.cs.findbugs.annotations.SuppressWarnings("UG_SYNC_SET_UNSYNC_GET")
    public T get ()
    {
        T result = field;
        if (result == null) // First check (no locking)
        {
            synchronized (this)
            {
                result = field;
                if (result == null) // Second check (with locking)
                {
                    field = result = computeValue ();
                }
            }
        }
        return result;
    }

    protected abstract T computeValue ();

    /** Setter for tests */
    public synchronized void set (T value)
    {
        field = value;
    }

    public boolean hasValue()
    {
        return field != null;
    }
}

从你的“需求”中,我无法收集到你真正想要的东西。你能解释一下吗?我正在寻找一种简单的方法来计算一次表达式,缓存结果,并能够覆盖单元测试的结果。你不能发布列表
Yuval:我觉得这在Java中是一个非常常见的问题,我想知道是否有人有比Spring或static final更好的解决方案。那么,发布我的需求有什么错呢?我正在考虑将其转化为一个社区wiki,以收集所有正确的方法,来延迟初始化一个变量+pro/cons。评论?实际上第三段提到了可测试性。你到底想吃多少汤匙?你投了反对票吗?我已经说过春天不是一个选择。我不想被人用勺子舀;我已经看到了十几种不同的lazyinit+重写方法,它们都有各自的缺点。我只想知道是否有人有比Spring/Guice/等更好的解决方案。我投了反对票,因为答案不适合我的问题,但如果你对此感到很糟糕,我可以撤销它,没问题。没有压力,好吗?在我看来,编译内部类和进行适当JITted的时间加上它所占用的额外内存并不能使它看起来很优雅。从性能上看,易失性读取可能会超过它。同样,不能创建一次singleton意味着它将永远不会被创建。。。您有2个选项保留:它为null或ExceptionInInitializerError+NoClassDefFoundError.-1,不适合运行一次新7,以前仅在说明中。我想避免在新的SomeObject中重复调用代码,因为它很昂贵。现在进行惰性初始化。听起来很有希望。你知道分布式IoC容器是什么样子吗?IoC容器基本上是一个同步映射。如何将所有必要的逻辑移到几行代码中?如何更改值?您需要确保没有线程正在访问,因此所有线程都已完成。因此,如果任何线程仍在使用“field”,那么使用synchronized set就没有帮助,解决方案不是线程安全的。@bestsss:这是不正确的;该字段是可变的,在Java中更改单个字段是原子的,因此setter是线程安全的。即使没有synchronized关键字,它也应该是线程安全的。事实上,我刚刚检查了AtomicReference并删除了同步的。@bestsss:还要记住,在通常情况下,没有人调用setter-它的存在是为了让测试用例可以覆盖singleton。AtomicReference只是一个启用了CAS的易失性文件。我说的不是线程安全的赋值,而是赋值对象的使用。再次说明:如果线程仍在使用旧对象,那么一旦集合被“调用”,就会出现数据竞争。现在,代码明确地依赖于,在调用集合时,没有对象创建正在进行中,您有一个真正的数据竞争,并且不再使用“旧”对象。set上的Synchronized很好,至少可以防止在computeValue期间不进行设置。假设computeValue正在进行,并且执行了对set的调用?最后完成的人无条件覆盖上一个值。同时,当“field”包含其他引用时,computeValue的结果将被某些线程使用。您最好的选择是使用版本检查和ThreadLocals来实现目标。您无法重置仍在使用的值。