Какая реализация для ленивого синглтона, инициализация которого может быть неудачной?
Представьте, что у вас есть статический метод без аргументов, который идемпотентен и всегда возвращает одно и то же значение и может выдать проверенное исключение, например, так:
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.
И последнее замечание: если вы используете хорошие шаблоны, обычно мало или нет причин, чтобы ваши классы лениво создавались. Лучше всего, чтобы ваш конструктор был очень легковесным, а основная часть вашей логики выполнялась как часть методов. Я не говорю, что вы обязательно делаете что-то не так в данном конкретном случае, но вы можете более подробно взглянуть на то, как вы используете этот синглтон, и посмотреть, нет ли лучшего способа структурировать вещи.