Java列挙クラス-動作モードのベストプラクティス


詳細
以下の主な内容は『Effective Java』第2版第30条を読んだ後の見方とまとめです.
 
大編の叙述的な知識点の紹介に直面する時、往々にして退屈すぎて、重点をつかむことができなくて、甚だしきに至っては少し力が足りないと感じます.対比の学習方式を採用して、どちらが優れているのか、どちらが劣っているのか、肝心な特性は何なのかを明らかにすることができます.第30条列挙についての紹介は、私にいくつかの感触を与えます.
同僚と定数の使い方について話し合ったことがありますが、public static finalを使ったことがあると言って、列挙を知らないで、ああ(′▽`)
本題に戻ると、まずint列挙モードを紹介します.
//The int enum pattern - severely deficient
public static final int APPLE_FUJI = 1;
public static final int APPLE_PIPPIN = 2;
public static final int APPLE_GRANNY_SMITH = 3;

public static final int ORANGE_NAVEL = 1;
public static final int ORANGE_TEMPLE = 2;
public static final int ORANGE_BLOOD = 3;

 
この方法には多くの不便があります.APPLEが必要です.FUJIのところでORANGEを使うNAVELはコンパイルや実行時の異常を起こさず、定数値が変化するとクライアントが再コンパイルしなければならない.混用の場合、実行時の動作は再コンパイルしても確定できない.また、データの伝達や使用時に見られるのはmagic numberであり、int列挙定数を遍歴しても信頼できる方法はないことが多い.
 
String列挙モードもあり、文字列の比較操作に依存するため、主にこの方法では性能の問題がある.
     
Integerのような方法がありますMAX_VALUE,Integer.MIN_VALUE,MATH.PIなどは,整数数の最大最小値と円周率をそれぞれ表し,特定のクラスに1つの属性または特定の意味を持つ定数値を関連付けて表すのが適切である.
   
Java列挙タイプの背後にある基本的な考え方は非常に簡単です.これらは、共通の静的finalドメインを介して列挙定数ごとにインスタンスを導出するクラスです.これらは単例の汎用化であり,本質的には単元素の列挙である.したがって、列挙を用いて単一の例を実現することは有効な方法である.int列挙モードに対してJava列挙タイプには以下の利点がある:1、列挙タイプは自分のネーミングスペースを持ち、同じ名前の定数(異なるネーミングスペース)を許可できます.2、クライアントの再コンパイルを必要とせずに列挙タイプの定数を増やして並べ替えることができます.(新規定数は自然に使用できません)3、toStringは定数のフォント値を取得できます.4、定数は任意にメソッドとフィールドを追加できます.
以下は主に本文で述べた列挙定数の方法の運用をめぐって説明するが,本文で述べたこれらの使用方式は設計モードの動作モードと少し似ていることが分かったので,列挙の動作モードと呼ぶ.
 
一、列挙定数の共通行為
太陽系の8大惑星を例にとると、各惑星には質量と半径があり、この2つの属性によって表面重力を計算することができ、それによって物体の質量によってある惑星での重量を得ることができ、例では定数の2つのパラメータを列挙してそれぞれ惑星の質量と半径を表す.
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
    private final double mass;           // In kilograms
    private final double radius;         // In meters
    private final double surfaceGravity; // In m / s^2
 
    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;
 
    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
 
    public double mass()           { return mass; }
    public double radius()         { return radius; }
    public double surfaceGravity() { return surfaceGravity; }
 
    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;  // F = ma
    }
}

 
次に、表面重量を計算する主な方法を示します.
public class WeightTable {
	public static void main(String[] args) {
		double earthWeight = Double.parseDouble(args[0]);
		double mass = earthWeight / Planet.EARTH.surfaceGravity();
		for (Planet p : Planet.values())
		System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
	}
}

 
結果は次のとおりです.
Weight on MERCURY is 66.133672
Weight on VENUS is 158.383926
Weight on EARTH is 175.000000
Weight on MARS is 66.430699
Weight on JUPITER is 442.693902
Weight on SATURN is 186.464970
Weight on URANUS is 158.349709
Weight on NEPTUNE is 198.846116
 
動作解析:
各惑星はその表面重量を計算しなければならないが、惑星が表面重力を計算する公式は変わらないので、各定数のこの行為は統一されており、抽象的に一つの方法でよいが、巧みな点は定数を初期化する際に表面重力値を一緒に計算し、計算時に直接値を取ればよい.
 
二、列挙定数の異なる行為
 
操作コードを例にとると、加算減算の実装は異なる.まず、最初の実装方法(悪い方法):
// Enum type that switches on its own value - questionable
public enum Operation {
	PLUS, MINUS, TIMES, DIVIDE;
	// Do the arithmetic op represented by this constant
	double apply(double x, double y) {
		switch(this) {
		case PLUS: return x + y;
		case MINUS: return x - y;
		case TIMES: return x * y;
		case DIVIDE: return x / y;
		}
		throw new AssertionError("Unknown op: " + this);
	}
}

 
このコードは実行可能に見えるが、脆弱で、新しい定数を追加し、switchに判断条件を追加するのを忘れた場合、コンパイルに問題はなく、実行時に異常が報告されます.また,オブジェクト向けプログラミングの観点から,大量のswitch case文やif else文に直面すると,一般的には改善の余地がある.
「(特定の定数メソッド実装)constant-specific method implementations」という方法が提案されています.これは、列挙タイプに抽象メソッドを宣言し、定数で実装する方法です.コードは次のとおりです.
// Enum type with constant-specific method implementations
public enum Operation {
	PLUS { double apply(double x, double y){return x + y;} },
	MINUS { double apply(double x, double y){return x - y;} },
	TIMES { double apply(double x, double y){return x * y;} },
	DIVIDE { double apply(double x, double y){return x / y;} };
	abstract double apply(double x, double y);
}

 
コンパイラが注意するので、新しい定数を追加しても方法を忘れません.さらに改善されたのは、特定の定数値と組み合わせて、toStringの便利な印刷算術式を利用することです.
// Enum type with constant-specific class bodies and data
public enum Operation {
	PLUS("+") {
	double apply(double x, double y) { return x + y; }
	},
	MINUS("-") {
	double apply(double x, double y) { return x - y; }
	},
	TIMES("*") {
	double apply(double x, double y) { return x * y; }
	},
	DIVIDE("/") {
	double apply(double x, double y) { return x / y; }
	};
	private final String symbol;
	Operation(String symbol) { this.symbol = symbol; }
	@Override public String toString() { return symbol; }
	abstract double apply(double x, double y);
}
public static void main(String[] args) {
	double x = 2;
	double y = 4;
	for (Operation op : Operation.values())
	System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}

 
印刷結果:
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000/4.000000 = 0.500000
文字列定数を列挙定数に変換するfromString()メソッドも推奨されます.以下は参照できる方法であり、Mapによって文字列から列挙定数への変換を容易に実現する.
// Implementing a fromString method on an enum type
private static final Map stringToEnum = new HashMap();
static { // Initialize map from constant name to enum constant
	for (Operation op : values())
	stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
	return stringToEnum.get(symbol);
}

 
三、戦略に基づく行為の実現
3.1
上記の方法「constant-specific method implementations」には、コードを共有できない(コードを共有するのも難しいようだ)という欠点があります.次に、コードを共有できる例を示します.
給与計算を例にとると、5営業日、8時間以外は残業(T_T)、残業給与を計算します.以下はswitchで実現する方法です.
// Enum that switches on its value to share code - questionable
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
	private static final int HOURS_PER_SHIFT = 8;
	double pay(double hoursWorked, double payRate) {
		double basePay = hoursWorked * payRate;
		double overtimePay; // Calculate overtime pay
		switch(this) {
			case SATURDAY: case SUNDAY:
				overtimePay = hoursWorked * payRate / 2;
			default: // Weekdays
				overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
				break;
		}
		return basePay + overtimePay;
	}
}

 
上のコードを見て、問題が発見されたかどうか分かりませんが、週末残業のコードにbreakが入っていません.コードのメンテナンスの観点から見ると、このコードは危険です.新しい列挙値(例えば病気休暇)を追加したら、switch文を修正するのを忘れて、給料を計算するのは間違いに違いありません.
オブジェクト向けの観点から見ると、上記のコードはもちろん改善できますが、本文では「ポリシー列挙」の方法で給与計算を実現することを提案しています.ポリシーモデルに似ていますね.コードは以下の通りです.
// The strategy enum pattern
enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
	private final PayType payType;
	PayrollDay(PayType payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}
	// The strategy enum type
	private enum PayType {
		WEEKDAY {
			double overtimePay(double hours, double payRate) {
			return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
			}
		},
		WEEKEND {
			double overtimePay(double hours, double payRate) {
			return hours * payRate / 2;
			}
		};
		private static final int HOURS_PER_SHIFT = 8;
		abstract double overtimePay(double hrs, double payRate);
		double pay(double hoursWorked, double payRate) {
			double basePay = hoursWorked * payRate;
			return basePay + overtimePay(hoursWorked, payRate);
		}
	}
}

 
 
PayTypeはポリシーの列挙で、給与の計算を担当し、Switchがなく、平日タイプを増やし、計算ポリシーを選択するとコード修正が容易になります.
3.2
もう1つの方法は、列挙クラスの拡張に基づいて、ポリシーモデルとそっくりである.列挙された拡張はインタフェースによって実現される.例えば、賃金の支払いを例にとると、すでに平日と週末の支払い方式があり、国慶節残業賃金の支払い方式を追加したいと思っています.インタフェースと実現は以下の通りです.
//      
public interface ShallPay {
	public double pay(double hours, double payRate);
}

 
//         
public enum PayType implements ShallPay {
	...
}

 
//      
public enum PayTypeHoliday  implements ShallPay {
	TRIPLE {
		public double pay(double hour, double payRate) {
			return 3*hour*payRate;
		}
	};
}

 
//       
public enum PayrollDay {
	public enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND),
	HOLIDAY(PayTypeHoliday.TRIPLE);
	
	private final ShallPay payType;
	PayrollDay(ShallPay payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}	
}

 
PayrollDayは賃金を支払う必要がある場合のコンテキスト環境であり,ShallPayはポリシーインタフェースであり,PayType,PayType Holidayの定数は具体的なポリシーの実装クラスと見なすことができる.
このように,従来の列挙クラスに基づいて,独自の列挙クラスを拡張することで,新しい列挙タイプを直接追加して同じインタフェースを実現することができ,使い勝手がよい.
 
注意:
上記の方法を使った後、switch文は列挙にとって役に立たないのではないでしょうか.外部の制御されていない列挙に対してswitchを使用するのは適切です.例えば、Operation列挙は、あなたの制御を受けません.演算子の逆演算を返す方法があることを望んでいます.以下の方法で使用できます.
// Switch on an enum to simulate a missing method
public static Operation inverse(Operation op) {
	switch(op) {
		case PLUS: return Operation.MINUS;
		case MINUS: return Operation.PLUS;
		case TIMES: return Operation.DIVIDE;
		case DIVIDE: return Operation.TIMES;
		default: throw new AssertionError("Unknown op: " + op);
	}
}