Какая реализация для ленивого синглтона, инициализация которого может быть неудачной?

Представьте, что у вас есть статический метод без аргументов, который идемпотентен и всегда возвращает одно и то же значение и может выдать проверенное исключение, например, так:

class Foo {
 public static Pi bar() throws Baz { getPi(); } // gets Pi, may throw 
}

Теперь это хороший кандидат на ленивый синглтон, если материал, который создает возвращаемый объект, дорог и никогда не меняется. Одним из вариантов будет шаблон 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 все еще является анти-паттерном? Есть ли другие решения, которые я здесь упускаю (второстепенные варианты, такие как однократная проверка, также возможны, но принципиально не меняют решение)? Есть ли веская причина выбирать один над другим?

Я не пробовал приведенные выше примеры, поэтому вполне возможно, что они не компилируются.

Редактировать: я не могу позволить себе роскошь повторно внедрить или изменить архитектуру потребителей этого синглтона (то есть, абоненты Foo.bar()), и у меня нет возможности представить DI-фреймворк для решения этой проблемы. В основном меня интересуют ответы, которые решают проблему (предоставление синглтона с проверенными исключениями, передаваемыми вызывающей стороне) в рамках заданных ограничений.

Обновление: в конце концов я решил пойти с DCL, поскольку он предоставил самый чистый способ сохранить существующий контракт, и никто не представил конкретную причину, почему DCL, сделанный должным образом, следует избегать. Я не использовал метод в принятом ответе, потому что он казался слишком сложным способом достижения того же самого.

4 ответа

Решение

Трюк "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;
}

Я настоятельно рекомендую выбросить синглтон и изменяемую статику в целом. "Используйте конструкторы правильно". Построить объект и передать его в объекты, которые в нем нуждаются.

Поскольку вы не говорите нам, что вам это нужно, трудно предложить лучшие способы достижения этого. Я могу сказать вам, что ленивые синглтоны редко являются лучшим подходом.

Я вижу несколько проблем с вашим кодом, хотя:

try {
    return PiHolder.PI_SINGLETON;
} catch (ExceptionInInitializerError e) {

Как вы ожидаете, что доступ к полю вызовет исключение?


Редактирование: Как указывает Irreputable, если доступ вызывает инициализацию класса, и инициализация завершается неудачно, потому что статический инициализатор выдает исключение, вы фактически получаете здесь ExceptionInInitializerError. Однако виртуальная машина не будет пытаться инициализировать класс снова после первого сбоя и сообщать об этом с другим исключением, как показано в следующем коде:

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 производится каждый раз при обращении к PI. Если сбой является постоянным и дорогостоящим, вы можете сделать что-то по-другому

По моему опыту, лучше всего использовать внедрение зависимостей, когда объект, который вы пытаетесь получить, требует чего-то большего, чем простой вызов конструктора.

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. редактировать: как указывает меритон, двойная проверка блокировки может работать в Java, но вы должны использовать ключевое слово volatile.

И последнее замечание: если вы используете хорошие шаблоны, обычно мало или нет причин, чтобы ваши классы лениво создавались. Лучше всего, чтобы ваш конструктор был очень легковесным, а основная часть вашей логики выполнялась как часть методов. Я не говорю, что вы обязательно делаете что-то не так в данном конкретном случае, но вы можете более подробно взглянуть на то, как вы используете этот синглтон, и посмотреть, нет ли лучшего способа структурировать вещи.

Другие вопросы по тегам