Java 열거 형으로 싱글 톤을 구현하면 단점은 무엇입니까?


14

전통적으로 싱글 톤은 일반적으로 다음과 같이 구현됩니다.

public class Foo1
{
    private static final Foo1 INSTANCE = new Foo1();

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Java 열거 형을 사용하면 단일 톤을 다음과 같이 구현할 수 있습니다

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

두 번째 버전만큼이나 멋진 점이 있습니까?

(나는 그것에 대해 몇 가지 생각을했고 내 자신의 질문에 대답 할 것입니다.


16
단점은 싱글 톤이라는 것입니다. 완전히 과장된 ( 기침 ) "패턴"
Thomas Eding

답변:


32

열거 형 싱글 톤의 일부 문제 :

구현 전략에 전념

일반적으로 "단일"은 API 사양이 아닌 구현 전략을 나타냅니다. Foo1.getInstance()항상 같은 인스턴스를 반환한다고 공개적으로 선언하는 것은 매우 드 rare니다 . 필요한 경우 Foo1.getInstance()예를 들어 스레드 당 하나의 인스턴스를 반환 하도록 구현을 발전시킬 수 있습니다.

함께 Foo2.INSTANCE우리는 공개적으로이 인스턴스가 있음을 선언 예, 그 변화 할 기회가 없습니다. 단일 인스턴스를 갖는 구현 전략이 노출되어 있습니다.

이 문제는 치명적이지 않습니다. 예를 들어 Foo2.INSTANCE.doo()스레드 당 인스턴스 를 효과적으로 갖기 위해 스레드 로컬 도우미 개체를 사용할 있습니다.

열거 형 클래스 확장

Foo2슈퍼 클래스를 확장합니다 Enum<Foo2>. 우리는 보통 슈퍼 클래스를 피하고 싶습니다. 특히이 경우, 강제 수퍼 클래스 는 예상되는 Foo2것과 아무 관련이 없습니다 Foo2. 이것이 애플리케이션의 유형 계층 구조에 대한 오염입니다. 우리가 정말로 슈퍼 클래스를 원한다면 그것은 보통 응용 프로그램 클래스이지만 Foo2수퍼 클래스는 고정되어 있습니다.

Foo2와 같은 재미있는 인스턴스 메소드를 상속합니다.이 메소드 name(), cardinal(), compareTo(Foo2)Foo2의 사용자에게 혼란을 줍니다. 의 인터페이스 에서 해당 메소드가 바람직하더라도 Foo2자체 name()메소드를 가질 수 없습니다 Foo2.

Foo2 재미있는 정적 메소드도 포함되어 있습니다.

    public static Foo2[] values() { ... }
    public static Foo2 valueOf(String name) { ... }
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)

사용자에게는 무의미한 것으로 보입니다. 싱글 톤은 일반적으로 어쨌든 pulbic 정적 방법이 없어야합니다 (이외의 getInstance())

직렬화

싱글 톤이 상태를 유지하는 것은 매우 일반적입니다. 이러한 싱글 톤은 일반적으로 직렬화 할 수 없습니다 . 한 VM에서 다른 VM으로 상태 저장 싱글 톤을 전송하는 것이 의미가있는 현실적인 예는 생각할 수 없습니다. 싱글 톤은 "유니버스에서 고유 한"것이 아니라 "VM 내에서 고유 한"을 의미합니다.

직렬화가 상태 저장 싱글 톤에 실제로 의미가있는 경우 싱글 톤은 동일한 유형의 싱글 톤이 이미 존재할 수있는 다른 VM에서 싱글 톤을 역 직렬화하는 것이 무엇을 의미하는지 명시적이고 정확하게 지정해야합니다.

Foo2단순한 직렬화 / 직렬화 전략을 자동으로 수행합니다. 그건 그냥 기다리고 사고입니다. Foo2t1에서 VM1 의 상태 변수를 개념적으로 참조하는 데이터 트리가 직렬화 / 역 직렬화를 통해 값이 다른 값 ( Foo2t2에서 VM2 의 동일한 변수 값)이되어 버그를 감지하기 어렵습니다. 직렬화 할 수없는이 버그는 Foo1자동으로 발생하지 않습니다 .

코딩 제한

일반 수업에서는 할 수 있지만 수업에서는 할 수없는 일이 있습니다 enum. 예를 들어 생성자의 정적 필드에 액세스합니다. 프로그래머는 특별한 수업을하고 있기 때문에 더 조심해야합니다.

결론

열거 형을 피기 백하면 코드 2 줄을 절약 할 수 있습니다. 그러나 가격이 너무 비싸서 열거 형의 모든 수하물과 제한을 수행해야하며 의도하지 않은 결과를 초래하는 열거 형의 "기능"을 실수로 상속합니다. 유일하게 주장되는 장점 인 자동 직렬화 기능은 단점으로 밝혀졌습니다.


2
-1 : 직렬화에 대한 토론이 잘못되었습니다. 역 직렬화하는 동안 열거 형이 일반 인스턴스와 매우 다르게 처리되므로 메커니즘이 단순하지 않습니다. 실제 역 직렬화 메커니즘이 "상태 변수"를 수정 하지 않으므로 설명 된 문제가 발생하지 않습니다.
scarfridge

이 혼란의 예를보십시오 : coderanch.com/t/498782/java/java/…
믿을 수없는

2
실제로 연결된 토론은 내 요점을 강조합니다. 내가 주장하는 문제로 내가 이해 한 것을 설명하겠습니다. 일부 객체 A는 두 번째 객체 B를 참조합니다. 싱글 톤 인스턴스 S도 B를 참조합니다. 이제 우리는 enum 기반 싱글 톤의 직렬화 된 인스턴스를 직렬화 해제합니다 (직렬화시 B '! = B 참조). 실제로 발생하는 것은 B '가 직렬화되지 않기 때문에 A S가 B를 참조하는 것입니다. A와 S가 더 이상 동일한 객체를 참조하지 않는다고 표현하고 싶다고 생각했습니다.
scarfridge

1
우리는 실제로 같은 문제에 대해 이야기하고 있지 않습니까?
scarfridge

1
@Kevin Krumwiede : Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);, 예를 들어 정말 좋은 예는 다음과 같습니다 Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);.; ^), Java 7 및 Java 8에서 테스트되었습니다.
Holger

6

열거 형 인스턴스는 클래스 로더에 따라 다릅니다. 즉, 같은 열거 클래스를로드하는 부모로서 첫 번째 클래스 로더가없는 두 번째 클래스 로더가있는 경우 메모리에 여러 인스턴스를 가져올 수 있습니다.


코드 샘플

다음 열거 형을 만들고 .class 파일을 jar에 넣습니다. (물론 항아리는 올바른 패키지 / 폴더 구조를 갖습니다)

package mad;
public enum Side {
  RIGHT, LEFT;
}

이제이 테스트를 실행하여 클래스 경로에 위 열거 형의 사본이 없는지 확인하십시오.

@Test
public void testEnums() throws Exception
{
    final ClassLoader root = MadTest.class.getClassLoader();

    final File jar = new File("path to jar"); // Edit path
    assertTrue(jar.exists());
    assertTrue(jar.isFile());

    final URL[] urls = new URL[] { jar.toURI().toURL() };
    final ClassLoader cl1 = new URLClassLoader(urls, root);
    final ClassLoader cl2 = new URLClassLoader(urls, root);

    final Class<?> sideClass1 = cl1.loadClass("mad.Side");
    final Class<?> sideClass2 = cl2.loadClass("mad.Side");

    assertNotSame(sideClass1, sideClass2);

    assertTrue(sideClass1.isEnum());
    assertTrue(sideClass2.isEnum());
    final Field f1 = sideClass1.getField("RIGHT");
    final Field f2 = sideClass2.getField("RIGHT");
    assertTrue(f1.isEnumConstant());
    assertTrue(f2.isEnumConstant());

    final Object right1 = f1.get(null);
    final Object right2 = f2.get(null);
    assertNotSame(right1, right2);
}

그리고 이제 "동일한"열거 형 값을 나타내는 두 개의 객체가 있습니다.

나는 이것이 희귀하고 고안된 코너 케이스이며 거의 항상 열거 형을 Java 싱글 톤에 사용할 수 있다는 데 동의합니다. 나는 그것을 스스로한다. 그러나이 질문은 잠재적 인 단점에 대해 물었고이주의 사항은 알아야 할 가치가 있습니다.


그 문제에 대한 언급이 있습니까?

이제 예제 코드를 사용하여 초기 답변을 편집하고 향상 시켰습니다. 그들이 요점을 설명하고 MichaelT의 질문에 대답하는 데 도움이되기를 바랍니다.
Mad G

@MichaelT : 질문에 대한 답변이 되었기를 바랍니다 :-)
Mad G

따라서 싱글 톤에 대해 클래스 대신 열거 형을 사용하는 이유가 안전에 불과한 경우 지금은 아무런 이유가 없습니다 ... 우수, +1
Gangnus

1
"전통적인"싱글 톤 구현이 두 개의 다른 클래스 로더에서도 예상대로 작동합니까?
Ron Klein

3

생성자에서 예외를 발생시키는 클래스에는 열거 형 패턴을 사용할 수 없습니다. 이것이 필요한 경우 공장을 사용하십시오.

class Elvis {
    private static Elvis self = null;
    private int age;

    private Elvis() throws Exception {
        ...
    }

    public synchronized Elvis getInstance() throws Exception {
        return self != null ? self : (self = new Elvis());
    }

    public int getAge() {
        return age;
    }        
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.