자바에서 열거 형 확장

1. 개요

Java 5에 도입 된 enum 유형은 상수 그룹을 나타내는 특수 데이터 유형입니다.

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

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

이 튜토리얼에서는 새로운 상수 값과 새로운 기능을 추가하는 등 Java에서 열거 형을 확장하는 방법에 대해 설명합니다.

2. 열거 형 및 상속

Java 클래스를 확장하려는 경우 일반적으로 하위 클래스를 생성합니다. Java에서 열거 형은 클래스이기도합니다.

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

2.1. Enum 유형 확장

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

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 의 하위 클래스로 표시되는 것을 볼 수 있습니다 .

$ javap BasicStringOperation public final class com.baeldung.enums.extendenum.BasicStringOperation extends java.lang.Enum { 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; ... } 

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

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

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

3.1. 상수 확장 에뮬레이트

이 기술을 빠르게 이해하기 위해 BasicStringOperation 열거 형을 확장 하여 MD5_ENCODEBASE64_ENCODE 작업 을 포함하는 방법을 살펴 보겠습니다 .

먼저 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) {...}

Since we can't change the enum code, we can use EnumMap to associate the enum constants and required implementations.

First, let's create an interface:

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

Next, we'll create the mapping between enum constants and the Operator implementations using an EnumMap:

public class Application { private static final Map 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); }

In this way, our applyImmutableOperation() method can apply the corresponding operation to the given input string:

@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. Validating the EnumMap Object

Now, if the enum is from an external library, we don't know if it has been changed or not, such as by adding new constants to the enum. In this case, if we don't change our initialization of the EnumMap to contain the new enum value, our EnumMap approach may run into a problem if the newly added enum constant is passed to our application.

To avoid that, we can validate the EnumMap after its initialization to check if it contains all enum constants:

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!"); } } 

As the code above shows, if any constant from ImmutableOperation is not mapped, an IllegalStateException will be thrown. Since our validation is in a static block, IllegalStateException will be the cause of ExceptionInInitializerError:

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

Thus, once the application fails to start with the mentioned error and cause, we should double-check the ImmutableOperation to make sure all constants are mapped.

5. Conclusion

The enum is a special data type in Java. In this article, we've discussed why enum doesn't support inheritance. After that, we addressed how to emulate extensible enums with interfaces.

Also, we've learned how to extend the functionalities of an enum without changing it.

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