对于初始化可能失败的懒惰单例的哪个实现?

想象一下,你有一个静态的无参数方法,它是幂等的,并且总是返回相同的值,并且可能抛出一个已检查的异常,如下所示:
class Foo {
 public static Pi bar() throws Baz { getPi(); } // gets Pi, may throw 
}
现在,如果构造返回的Object的东西很昂贵并且永远不会改变,那么这对于懒惰的单例是一个很好的选择。一种选择是Holder模式:
class Foo {
  static class PiHolder {
   static final Pi PI_SINGLETON = getPi();
  }
  public static Pi bar() { return PiHolder.PI_SINGLETON; }
}
不幸的是,这不起作用,因为我们不能从(隐式)静态初始化程序块中抛出一个已检查的异常,所以我们可以尝试这样的事情(假设我们想保留调用者获取检查异常时的行为)叫
bar()
):
class Foo {
  static class PiHolder {
   static final Pi PI_SINGLETON;
   static { 
    try { 
     PI_SINGLETON =  = getPi(); }
    } catch (Baz b) {
     throw new ExceptionInInitializerError(b);
    }
  }

  public static Pi bar() throws Bar {
   try {
    return PiHolder.PI_SINGLETON;
   } catch (ExceptionInInitializerError e) {
    if (e.getCause() instanceof Bar)
     throw (Bar)e.getCause();
    throw e;
   }
}
在这一点上,双重检查锁定可能更干净?
class Foo {
 static volatile Pi PI_INSTANCE;
 public static Pi bar() throws Bar {
  Pi p = PI_INSTANCE;
  if (p == null) {
   synchronized (this) {
    if ((p = PI_INSTANCE) == null)
     return PI_INSTANCE = getPi();
   }
  }
  return p;
 }
}
DCL仍然是反模式吗?还有其他解决方案我在这里缺少(像racy单一检查这样的小变种也是可能的,但是不要从根本上改变解决方案)?是否有充分的理由选择其中一个? 我没有尝试上面的例子,所以完全有可能他们不编译。 编辑:我没有重新实现或重新构建这个单例的消费者(即ѭ5的调用者)的奢侈,也没有机会引入DI框架来解决这个问题。我最感兴趣的是在给定约束内解决问题的答案(提供带有检查异常传播给调用者的单例)。 更新:毕竟我决定选择DCL,因为它提供了保存现有合同的最简洁方法,没有人提供为什么应该避免正确完成DCL的具体原因。我没有在接受的答案中使用该方法,因为它似乎只是一种过于复杂的方式来实现同样的事情。     
已邀请:
“Holder”技巧基本上是由JVM执行的双重检查锁定。根据规范,类初始化是在(双重检查)锁定下进行的。不幸的是,JVM可以安全地(和快速地)执行DCL,Java程序员无法使用它。我们能做的最接近的是通过中间人的最终参考。请参阅DCL上的维基百科。 您保留异常的要求并不难:
class Foo {
  static class PiHolder {
    static final Pi PI_SINGLETON;
    static Bar exception;
    static { 
      try { 
        PI_SINGLETON =  = getPi(); }
      } catch (Bar b) {
        exception = b;
      }
    }
  }
public Pi bar() throws Bar {
  if(PiHolder.exception!=null)
    throw PiHolder.exception;  
  else
    return PiHolder.PI_SINGLETON;
}
    
我强烈建议一般抛弃Singleton和可变静力学。 “正确使用构造函数。”构造对象并将其传递给需要它的对象。     
根据我的经验,当您尝试获取的对象需要的不仅仅是简单的构造函数调用时,最好使用依赖注入。
public class Foo {
  private Pi pi;
  public Foo(Pi pi) {
    this.pi = pi;
  }
  public Pi bar() { return pi; }
}
......或者如果懒惰很重要:
public class Foo {
  private IocWrapper iocWrapper;
  public Foo(IocWrapper iocWrapper) {
    this.iocWrapper = iocWrapper;
  }
  public Pi bar() { return iocWrapper.get(Pi.class); }
}
(细节将在某种程度上取决于您的DI框架) 您可以告诉DI框架将对象绑定为单例。从长远来看,这为您提供了更大的灵活性,使您的课程更具单元可测性。 另外,我的理解是Java中的双重检查锁定不是线程安全的,因为JIT编译器可能会重新排序指令。编辑:正如meriton所指出的,双重检查锁定可以在Java中工作,但您必须使用volatile关键字。 最后一点:如果您使用的是好的模式,通常很少或没有理由希望您的类被懒惰地实例化。最好让你的构造函数非常轻量级,并将大部分逻辑作为方法的一部分来执行。我并不是说你在这个特殊情况下做错了什么,但是你可能想要更广泛地看看你如何使用这个单例,看看是不是有更好的方法来构建东西。     
既然你没有告诉我们你需要什么,那么就很难提出更好的方法来实现它。我可以告诉你,懒惰的单身人士很少是最好的方法。 不过,我可以看到您的代码有几个问题:
try {
    return PiHolder.PI_SINGLETON;
} catch (ExceptionInInitializerError e) {
您如何期望字段访问引发异常? 编辑:正如Irreputable指出的那样,如果访问导致类初始化,并且初始化因静态初始化程序抛出异常而失败,那么实际上在这里得到了ExceptionInInitializerError。但是,VM在第一次失败后不会尝试再次初始化类,并使用不同的异常进行通信,如下面的代码所示:
static class H {
    final static String s; 
    static {
        Object o = null;
        s = o.toString();
    }
}

public static void main(String[] args) throws Exception {
    try {
        System.out.println(H.s);
    } catch (ExceptionInInitializerError e) {
    }
    System.out.println(H.s);
}
结果是:
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class tools.Test$H 
        at tools.Test.main(Test.java:21)
而不是ExceptionInInitializerError。 您的双重检查锁定会遇到类似的问题;如果构造失败,则该字段保持为空,并且每次访问PI时都会尝试构造“12”。如果失败是永久性和昂贵的,您可能希望以不同的方式做事。     

要回复问题请先登录注册