1. 概要

Java5で導入されたenumタイプは、定数のグループを表す特別なデータ型です。

列挙型を使用すると、型安全性の方法で定数を定義して使用できます。 コンパイル時のチェックを定数にもたらします。

さらに、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
}

上記のコードが示すように、3つの基本的な文字列操作を含む列挙型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のサブクラスに変換します
  • 列挙型をfinalクラスとしてコンパイルします

たとえば、コンパイル済みを分解すると BasicStringOperation 列挙型を使用 java p 、のサブクラスとして表されていることがわかります java .lang.Enum

$ 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;
 ...
}

ご存知のように、Javaではfinalクラスを継承できません。 また、作成できたとしても ExtendedStringOperation 継承する列挙型 BasicStringOperation 、 私たちの ExtendedStringOperation enumは2つのクラスを拡張します: BasicStringOperation java.lang.Enum。 つまり、Javaではサポートされていない多重継承状況になります。

3. インターフェイスを使用して拡張可能な列挙型をエミュレートする

既存の列挙型のサブクラスを作成できないことを学びました。 ただし、インターフェイスは拡張可能です。 したがって、インターフェイスを実装することで、拡張可能な列挙をエミュレートできます。

3.1. 定数の拡張をエミュレートする

この手法をすばやく理解するために、BasicStringOperation列挙型をMD5_ENCODEおよびBASE64_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. 機能の拡張

インターフェイスを使用して列挙型の拡張定数をエミュレートする方法を見てきました。

さらに、enumの機能を拡張するためのメソッドをインターフェースに追加することもできます。

たとえば、 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

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がスローされます。 検証はstaticブロックにあるため、IllegalStateExceptionExceptionInInitializerErrorの原因になります。

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

したがって、アプリケーションが上記のエラーと原因で起動に失敗したら、 ImmutableOperation を再確認して、すべての定数がマップされていることを確認する必要があります。

5. 結論

列挙型はJavaの特別なデータ型です。 この記事では、enumが継承をサポートしない理由について説明しました。 その後、インターフェイスを使用して拡張可能な列挙型をエミュレートする方法について説明しました。

また、列挙型を変更せずに機能を拡張する方法も学びました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。