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

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

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

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

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

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

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

Ответы [ 4 ]

1 голос
/ 03 марта 2011

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

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

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

1 голос
/ 03 марта 2011

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

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.

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

1 голос
/ 03 марта 2011

Уловка "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;
}
1 голос
/ 02 марта 2011

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

...