java - valueof - 抽象メソッドをオーバーライドするか、列挙型で単一のメソッドを使用しますか?



java enum 存在チェック (6)

以下のenums考えてみましょう。 両方とも全く同じ方法で使用できますが、お互いの利点は何ですか?

1.抽象メソッドをオーバーライドする:

public enum Direction {
    UP {
        @Override
        public Direction getOppposite() {
            return DOWN;
        }
        @Override
        public Direction getRotateClockwise() {
            return RIGHT;
        }
        @Override
        public Direction getRotateAnticlockwise() {
            return LEFT;
        }
    },
    /* DOWN, LEFT and RIGHT skipped */
    ;
    public abstract Direction getOppposite();
    public abstract Direction getRotateClockwise();
    public abstract Direction getRotateAnticlockwise();
}

2.単一の方法を使用する:

public enum Orientation {
    UP, DOWN, LEFT, RIGHT;
    public Orientation getOppposite() {
        switch (this) {
        case UP:
            return DOWN;
        case DOWN:
            return UP;
        case LEFT:
            return RIGHT;
        case RIGHT:
            return LEFT;
        default:
            return null;
        }
    }
    /* getRotateClockwise and getRotateAnticlockwise skipped */
}

編集:私は実際に、特定の主張に対する証拠/出典とともに、合理的で精巧な回答を見たいと思っています。 パフォーマンスに関する既存の回答のほとんどは、証明の欠如のために本当に説得力がありません。

あなたは代替案提案することができます 、それが明示されたものよりどのように優れているか、および/またはどのように悪化しているかが明確でなければならず、必要なときに証拠を提供しなければなりません。

https://src-bin.com


Answer #1

> 2-ary多態性がインターフェイス上で完全な仮想関数呼び出しを強制するため、2番目の変種はおそらく少し速くなります。

最初の形式はオブジェクト指向のアプローチです。

第2の形態はパターンマッチング法である。

このように、オブジェクト指向の最初の形式では、新しい列挙を簡単に追加できますが、新しい操作を追加するのは困難です。 第2の形式は、

私が知っている経験豊富なプログラマーの多くは、オブジェクト指向に対するパターンマッチングの使用をお勧めします。 列挙型が閉じられると、新しい列挙型を追加することは選択肢ではありません。 したがって、私は間違いなく自分自身の後者のアプローチに行きます。


Answer #2

答え:それは依存する

  1. あなたのメソッド定義が単純な場合

    これは非常に簡単なサンプルメソッドの場合です。各enum入力に対してenum出力をハードコードするだけです

    • その列挙値のすぐ隣の列挙値に固有の定義を実装する
    • 「共通領域」のクラスの下部にあるすべての列挙値に共通の定義を実装します。 同じメソッドのシグネチャをすべての列挙値で使用できるが、ロジックの一部または全部が共通する場合は、共通領域で抽象メソッド定義を使用する

    オプション1

    どうして?

    • 可読性、一貫性、保守性:定義に直接関連するコードは、定義のすぐ隣にあります
    • 抽象メソッドが共通領域で宣言されているがenum値領域で指定されていないかどうかをコンパイル時にチェックする

    North / South / East / Westの例は(現在の方向の)非常に単純な状態を表すと見なすことができ、反対方向/ rotateClockwise / rotateAnticlockwiseのメソッドは状態を変更するユーザコマンドを表すとみなすことができることに注意してください。 これは疑問を提起する、あなたは実生活、通常は複雑な状態機械のために何をしますか?

  2. メソッドの定義が複雑な場合:

    状態マシンは、現在の(列挙値)状態、コマンド入力、タイマー、およびかなり多数のルールとビジネス例外に依存して、新しい(列挙値)状態を決定する複雑なことが多い。 他の稀な時代の方法は、計算(例えば、科学/工学/保険格付けの分類)によって列挙値の出力を決定することさえできる。 または、マップなどのデータ構造やアルゴリズムに適した複雑なデータ構造を使用できます。 論理が複雑な場合、特別な注意が必要であり、「共通」論理と「列挙値固有」論理との間のバランスが変化する。

    • 過度のコード量、複雑さ、繰り返しの '切り取りと貼り付け'セクションを列挙値のすぐ隣に置かないようにする
    • 可能な限り多くのロジックを共通領域にリファクタリングしてください。可能であれば、100%のロジックをここに置くことは可能ですが、Gang Of Fourの「Template Method」パターンを使用して共通ロジックの量を最大限にしながら、それぞれの列挙値に対して特定の論理を持つ。 すなわち、可能な限りオプション1、オプション2のほんの少しだけ許可

    どうして?

    • 可読性、一貫性、保守容易性:コードの膨らみ、重複、列挙型の値の間に散在する大量のコードによるテキスト形式の整理を避け、列挙型の値のすべてを素早く見て理解できるようにする
    • Template Methodパターンを使用する場合はコンパイル時のチェック、共通領域で宣言された抽象メソッドは列挙値領域では指定されていない

    • 注:あなたは別のヘルパークラスにすべてのロジックを入れることができますが、私は個人的にこれに利点(保守性/保守性/可読性)はありません。 それはカプセル化を少し壊し、すべてのロジックを一箇所にまとめたら、簡単な列挙体の定義をクラスの先頭に追加するのにどのような違いがありますか? 複数のクラスにまたがるコードを分割することは別の問題であり、適切な場合は奨励することです。


Answer #3

おそらく最初のバージョンははるかに高速です。 Java JITコンパイラは、 enumが最終的なので、積極的な最適化を適用できます(すべてのメソッドもfinalです)。 コード:

Orientation o = Orientation.UP.getOppposite();

実際に(実行時に)次のようになります。

Orientation o = Orientation.DOWN;

つまり、コンパイラはメソッド呼び出しのオーバーヘッドを取り除くことができます。

デザインの観点から見ると、OOでこれらのことを行う適切な方法です。それを必要とするオブジェクトの近くで知識を移動します。 だから、UPはそれが反対であることを知っているべきであり、他のコードではそうではない。

2番目の方法の利点は、すべての関連するものがグループ化されているため(つまり、「反対」に関連するすべてのコードがここでビットではなく1つの場所にあるため)、読みやすくなります。

EDIT私の最初の議論は、JITコンパイラがいかにスマートであるかに依存します。 問題の私の解決策は次のようになります:

public enum Orientation {
    UP, DOWN, LEFT, RIGHT;

    private static Orientation[] opposites = {
        DOWN, UP, RIGHT, LEFT
    };

    public Orientation getOpposite() {
        return opposites[ ordinal() ];
    }
}

このコードは、JITが何をすることができるかに関係なく、コンパクトで高速です。 それは意思を明確に伝え、序章の規則が与えられれば、それはいつもうまくいくでしょう。

また、enumの各値に対してgetOpposite()を呼び出すときに、常に異なる結果が得られ、結果がnullないことを確認するテストを追加することをお勧めしnull 。 そうすれば、すべてのケースを確実に得ることができます。

残っている唯一の問題は、値の順序を変更するときです。 この場合の問題を回避するには、各値にインデックスを割り当て、配列内の値またはOrientation.values()で値をルックアップするために使用します。

これを行う別の方法がここにあります:

public enum Orientation {
    UP(1), DOWN(0), LEFT(3), RIGHT(2);

    private int opposite;

    private Orientation( int opposite ) {
        this.opposite = opposite;
    }

    public Orientation getOpposite() {
        return values()[ opposite ];
    }
}

私はこのアプローチが嫌いです。 読むのが難しい(あなたの頭の中の各価値の指数を数えなければなりません)、間違ってしまうのは簡単です。 列挙型およびメソッドごとに単価テストを行う必要があります(あなたのケースでは4 * 3 = 12)。


Answer #4

この比較のパフォーマンスについては忘れてください。 2つの方法論の間に意味のあるパフォーマンスの差異があることは、本当に大規模な列挙を必要とします。

代わりに保守性に焦点を当てましょう。 Direction列挙型のコーディングが終了し、最終的にはより有名なプロジェクトに移行したとします。 一方、別の開発者には、 Directionを含む古いコードの所有権が与えられていますDirectionと呼ぶことにしましょう。

ある時点で、Jimmyは2つの新しい方向性を追加しなければならないという要件があります: FORWARDBACKWARD 。 ジミーは疲れて過労しており、これが既存の機能にどのように影響するかを完全に研究するのは煩わしいことではありません。 今起こることを見てみましょう:

1.抽象メソッドをオーバーライドする:

Jimmyはすぐにコンパイラエラーを取得します(実際には、おそらく、enum定数宣言のすぐ下のメソッドオーバーライドを検出したでしょう)。 どちらの場合でも、問題はコンパイル時に検出され、修正されます

2.単一の方法を使用する:

Jimmyは、あなたのswitch既にdefaultケースを持っているので、コンパイラエラー、または彼のIDEからの不完全なスイッチ警告を取得しません。 その後、実行時にコードの一部がFORWARD.getOpposite()呼び出し、 nullが返されnull 。 これにより予期しない動作がNullPointerExceptionし、すばやくNullPointerExceptionがスローされます。

バックアップして、将来的にいくつかの将来の校正を追加したふりをしましょう:

default:
    throw new UnsupportedOperationException("Unexpected Direction!");

それでも問題は実行時まで発見されません。 うまくいけば、プロジェクトは適切にテストされています!

さて、 Direction例はかなりシンプルなので、このシナリオは誇張されているかもしれません。 実際には、列挙型は他のクラスと同様にメンテナンスの問題に発展する可能性があります。 複数の開発者がいるより大きな古いコードベースでは、リファクタリングに対する耐性が正当な関心事です。 多くの人がコードの最適化について話していますが、開発者の時間も最適化する必要があることを忘れています。これには、間違いを防ぐためのコーディングも含まれています。

編集: JLSの例§8.9.2-4の下の注釈は同意しているようです:

定数固有のクラス本体は、動作を定数に結びつけます。 [この]パターンは、基本型のswitch文を使うよりもはるかに安全です。新しい定数の振る舞いを追加することを忘れる可能性があるからです(enum宣言はコンパイル時エラーを引き起こすため)。


Answer #5

私は実際には違うことをしています。 あなたのソリューションには欠点があります。オーバーライドされた抽象メソッドは非常に多くのオーバーヘッドをもたらし、switchステートメントは維持するのが非常に難しいです。

私はあなたの問題に適用される次のパターンを提案します:

public enum Direction {
    UP, RIGHT, DOWN, LEFT;

    static {
      Direction.UP.setValues(DOWN, RIGHT, LEFT);
      Direction.RIGHT.setValues(LEFT, DOWN, UP);
      Direction.DOWN.setValues(UP, LEFT, RIGHT);
      Direction.LEFT.setValues(RIGHT, UP, DOWN);
    }

    private void setValues(Direction opposite, Direction clockwise, Direction anticlockwise){
        this.opposite = opposite;
        this. clockwise= clockwise;
        this. anticlockwise= anticlockwise;
    }

    Direction opposite;
    Direction clockwise;
    Direction anticlockwise;

    public final Direction getOppposite() { return opposite; }
    public final Direction getRotateClockwise() { return clockwise; }
    public final Direction getRotateAnticlockwise() { return anticlockwise; }
}

そのようなデザインであなたは:

  • 方向を設定することを忘れないでください。なぜなら、コンストラクタによって強制されるからです( caseによっては)

  • メソッドコールオーバーヘッドはほとんどありません。メソッドは最終的なものであり、仮想メソッドではないからです

  • きれいで短いコード

  • あなたは一方向の値を設定することを忘れることができます


Answer #6

第1の変形は、方位自体が定義されている方向のすべての特性が記述されるので、より速く、おそらくより保守的である。 それにもかかわらず、非自明のロジックを列挙型にすることは、私にとって奇妙に見えます。





enums