1. 개요

열거 자바 5에 도입 유형, 상수의 그룹을 나타내는 특수 데이터 유형입니다.

열거형을 사용하여 유형 안전 방식으로 상수를 정의하고 사용할 수 있습니다. 상수에 대한 컴파일 타임 검사를 제공합니다.

또한 switch-case에서 상수를 사용할 수 있습니다 .

이 예제에서는 예를 들어 새로운 상수 값과 새로운 기능을 추가하는 것과 같이 Java에서 열거형을 확장하는 것에 대해 논의할 것입니다.

2. 열거형과 상속

Java 클래스를 확장하려면 일반적으로 하위 클래스를 만듭니다. Java에서 열거형도 클래스입니다.

이 섹션에서는 일반 Java 클래스와 마찬가지로 열거형을 상속할 수 있는지 살펴보겠습니다.

2.1. 열거형 확장

먼저 문제를 빠르게 이해할 수 있도록 예제를 살펴보겠습니다.

public enum BasicStringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;

    // constructor and getter
}

위의 코드에서 볼 수 있듯이 세 가지 기본 문자열 연산을 포함 하는 열거형 BasicStringOperation 이 있습니다.

이제 MD5_ENCODEBASE64_ENCODE 와 같은 일부 확장을 열거형에 추가하려고 한다고 가정보겠습니다 . 우리는 다음과 같은 간단한 해결책을 생각해 낼 수 있습니다.

public enum ExtendedStringOperation extends BasicStringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter
}

그러나 클래스를 컴파일하려고 하면 컴파일러 오류가 표시됩니다.

Cannot inherit from enum BasicStringOperation

2.2. 열거형에 대한 상속은 허용되지 않습니다.

이제 컴파일러 오류가 발생한 이유를 알아보겠습니다.

열거형을 컴파일할 때 Java 컴파일러는 다음과 같은 마법을 수행합니다.

  • 열거형을 추상 클래스 java.lang.Enum 의 하위 클래스로 바꿉니다.
  • enum을 최종 클래스 로 컴파일합니다.

예를 들어, javap를 사용하여 컴파일된 BasicStringOperation 열거형을 디스어셈블하면 이것이 java.lang.Enum<BasicStringOperation> 의 하위 클래스로 표시되는 것을 볼 수 있습니다 .

$ javap BasicStringOperation  
public final class com.baeldung.enums.extendenum.BasicStringOperation 
    extends java.lang.Enum<com.baeldung.enums.extendenum.BasicStringOperation> {
  public static final com.baeldung.enums.extendenum.BasicStringOperation TRIM;
  public static final com.baeldung.enums.extendenum.BasicStringOperation TO_UPPER;
  public static final com.baeldung.enums.extendenum.BasicStringOperation REVERSE;
 ...
}

알다시피, 우리는 자바에서 최종 클래스를 상속할 수 없습니다 . 또한, 우리가 만들 수있는 경우에도 ExtendedStringOperation의 상속에 열거 BasicStringOperation을 , 우리의 ExtendedStringOperation의 : 열거는 두 개의 클래스 확장 할 BasicStringOperationjava.lang.Enum을. 즉, Java에서는 지원하지 않는 다중 상속 상황이 됩니다.

3. 인터페이스로 확장 가능한 열거형 에뮬레이션

기존 열거형의 하위 클래스를 만들 수 없다는 것을 배웠습니다. 그러나 인터페이스는 확장 가능합니다. 따라서 인터페이스를 구현하여 확장 가능한 열거형을 에뮬레이트할 수 있습니다 .

3.1. 상수 확장 에뮬레이션

이 기술을 빠르게 이해하기 위해 MD5_ENCODEBASE64_ENCODE 작업 을 갖도록 BasicStringOperation 열거형을 확장하는 것을 에뮬레이트하는 방법을 살펴보겠습니다 .

먼저 StringOperation 인터페이스를 생성해 보겠습니다 .

public interface StringOperation {
    String getDescription();
}

다음으로 두 열거형 모두 위의 인터페이스를 구현합니다.

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;
    // constructor and getter override
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter override
}

마지막으로 확장 가능한 BasicStringOperation 열거형 을 에뮬레이트하는 방법을 살펴보겠습니다 .

애플리케이션에 BasicStringOperation 열거형에 대한 설명을 가져오는 메서드가 있다고 가정해 보겠습니다 .

public class Application {
    public String getOperationDescription(BasicStringOperation stringOperation) {
        return stringOperation.getDescription();
    }
}

이제 매개변수 유형 BasicStringOperation 을 인터페이스 유형 StringOperation 으로 변경하여 메소드가 두 열거형의 인스턴스를 모두 허용하도록 할 수 있습니다.

public String getOperationDescription(StringOperation stringOperation) {
    return stringOperation.getDescription();
}

3.2. 기능 확장

우리는 인터페이스로 열거형의 확장 상수를 에뮬레이트하는 방법을 보았습니다.

또한 인터페이스에 메서드를 추가하여 열거형의 기능을 확장할 수도 있습니다.

예를 들어, 각 상수가 실제로 주어진 문자열에 연산을 적용할 수 있도록 StringOperation 열거형 을 확장하려고 합니다.

public class Application {
    public String applyOperation(StringOperation operation, String input) {
        return operation.apply(input);
    }
    //...
}

이를 달성하기 위해 먼저 인터페이스에 apply() 메서드를 추가해 보겠습니다  .

public interface StringOperation {
    String getDescription();
    String apply(String input);
}

다음으로 각 StringOperation 열거형이 이 메서드를 구현하도록 합니다.

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces.") {
        @Override
        public String apply(String input) { 
            return input.trim(); 
        }
    },
    TO_UPPER("Changing all characters into upper case.") {
        @Override
        public String apply(String input) {
            return input.toUpperCase();
        }
    },
    REVERSE("Reversing the given string.") {
        @Override
        public String apply(String input) {
            return new StringBuilder(input).reverse().toString();
        }
    };

    //...
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm.") {
        @Override
        public String apply(String input) {
            return DigestUtils.md5Hex(input);
        }
    },
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.") {
        @Override
        public String apply(String input) {
            return new String(new Base64().encode(input.getBytes()));
        }
    };

    //...
}

테스트 방법은 이 접근 방식이 예상대로 작동함을 증명합니다.

@Test
public void givenAStringAndOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " hello";
    String expectedToUpper = " HELLO";
    String expectedReverse = "olleh ";
    String expectedTrim = "hello";
    String expectedBase64 = "IGhlbGxv";
    String expectedMd5 = "292a5af68d31c10e31ad449bd8f51263";
    assertEquals(expectedTrim, app.applyOperation(BasicStringOperation.TRIM, input));
    assertEquals(expectedToUpper, app.applyOperation(BasicStringOperation.TO_UPPER, input));
    assertEquals(expectedReverse, app.applyOperation(BasicStringOperation.REVERSE, input));
    assertEquals(expectedBase64, app.applyOperation(ExtendedStringOperation.BASE64_ENCODE, input));
    assertEquals(expectedMd5, app.applyOperation(ExtendedStringOperation.MD5_ENCODE, input));
}

4. 코드를 변경하지 않고 열거형 확장하기

인터페이스를 구현하여 열거형을 확장하는 방법을 배웠습니다.

그러나 때로는 열거형을 수정하지 않고 기능을 확장하고 싶을 때가 있습니다. 예를 들어 타사 라이브러리에서 열거형을 확장하고 싶습니다.

4.1. 열거형 상수와 인터페이스 구현 연결

먼저 열거형 예제를 살펴보겠습니다.

public enum ImmutableOperation {
    REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}

열거형이 외부 라이브러리에서 가져온 것이므로 코드를 변경할 수 없다고 가정해 보겠습니다.

이제 Application 클래스에서 입력 문자열에 주어진 작업을 적용하는 메서드를 원합니다.

public String applyImmutableOperation(ImmutableOperation operation, String input) {...}

열거형 코드를 변경할 수 없으므로 EnumMap사용 하여 열거형 상수와 필요한 구현을 연결할 수 있습니다 .

먼저 인터페이스를 생성해 보겠습니다.

public interface Operator {
    String apply(String input);
}

다음 으로 EnumMap<ImmutableOperation, Operator>를 사용하여 열거형 상수와 연산자 구현 간의 매핑을 만듭니다 .

public class Application {
    private static final Map<ImmutableOperation, Operator> OPERATION_MAP;

    static {
        OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
        OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
        OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
        OPERATION_MAP.put(ImmutableOperation.REMOVE_WHITESPACES, input -> input.replaceAll("\\s", ""));
    }

    public String applyImmutableOperation(ImmutableOperation operation, String input) {
        return operationMap.get(operation).apply(input);
    }

이런 식으로 우리의 applyImmutableOperation() 메서드는 해당 작업을 주어진 입력 문자열에 적용할 수 있습니다.

@Test
public void givenAStringAndImmutableOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " He ll O ";
    String expectedToLower = " he ll o ";
    String expectedRmWhitespace = "HellO";
    String expectedInvertCase = " hE LL o ";
    assertEquals(expectedToLower, app.applyImmutableOperation(ImmutableOperation.TO_LOWER, input));
    assertEquals(expectedRmWhitespace, app.applyImmutableOperation(ImmutableOperation.REMOVE_WHITESPACES, input));
    assertEquals(expectedInvertCase, app.applyImmutableOperation(ImmutableOperation.INVERT_CASE, input));
}

4.2. EnumMap  개체 유효성 검사

이제 열거형이 외부 라이브러리에서 가져온 것이라면 열거형에 새 상수를 추가하는 것과 같이 변경되었는지 여부를 알 수 없습니다. 이 경우 새로운 열거형 값을 포함 하도록 EnumMap 의 초기화를 변경하지 않으면 새로 추가된 열거형 상수가 애플리케이션에 전달되면 EnumMap 접근 방식에 문제가 발생할 수 있습니다.

이를 방지하기 위해 초기화 후에 EnumMap의 유효성을 검사하여 모든 열거형 상수가 포함되어 있는지 확인할 수 있습니다.

static {
    OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
    OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
    OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
    // ImmutableOperation.REMOVE_WHITESPACES is not mapped

    if (Arrays.stream(ImmutableOperation.values()).anyMatch(it -> !OPERATION_MAP.containsKey(it))) {
        throw new IllegalStateException("Unmapped enum constant found!");
    }
}

위의 코드에서 알 수 있듯이 ImmutableOperation의  상수 가 매핑되지 않으면 IllegalStateException 이 발생합니다. 유효성 검사가  정적 블록에 있으므로 IllegalStateExceptionExceptionInInitializerError 의 원인이 됩니다 .

@Test
public void givenUnmappedImmutableOperationValue_whenAppStarts_thenGetException() {
    Throwable throwable = assertThrows(ExceptionInInitializerError.class, () -> {
        ApplicationWithEx appEx = new ApplicationWithEx();
    });
    assertTrue(throwable.getCause() instanceof IllegalStateException);
}

따라서 애플리케이션이 언급된 오류 및 원인으로 시작하지 못하면 ImmutableOperation 을 다시 확인하여 모든 상수가 매핑되었는지 확인해야 합니다.

5. 결론

열거형은 Java의 특수 데이터 유형입니다. 이 기사에서 열거형이 상속을 지원하지 않는 이유에 대해 논의했습니다. 그 후 인터페이스로 확장 가능한 열거형을 에뮬레이트하는 방법을 다루었습니다.

또한 열거형을 변경하지 않고 기능을 확장하는 방법을 배웠습니다.

항상 그렇듯이 기사의 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .

Junit footer banner