Java中級者以上の必須本である、Effective Java 第3版に Kindle版が出たので、まとめる。
前:Effective Java 第3版 第5章ジェネリックス
次:Effective Java 第3版 第7章ラムダとストリーム
項目34 intの代わりにenumを使う
- enumは、public static finalフィールドを通して、各列挙定数に対して一つのインスタンスを公開しているクラス。
- enumはアクセス可能なコンストラクタを持っていないので、事実上final。
- enum型は、基本的に単一要素のenumであるシングルトンを汎用化したもの。
- enum型には、任意のメソッドやフィールドを追加できる。
データと振る舞いを持つenum型
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);
// 省略
private final double mass; // 質量
private final double radius; // 半径
private final double surfaceGravity; // 表面重力
private static final double G = 6.67300E-11; // 万有引力定数
// enum定数に、データを関連づけるコンストラクタ
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 getSurfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
// 呼び出し例
public static void main(String[] args) {
// 地球上で60kgは他の惑星ではどれくらいの重さになるか?
double earthWeight = Double.parseDouble("60.0");
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p: Planet.values()) {
System.out.printf("Weight on %s is %f%n", p, p.getSurfaceWeight(mass));
}
}
}
// 結果
Weight on MERCURY is 22.674402
Weight on VENUS is 54.303060
Weight on EARTH is 60.000000
Weight on MARS is 22.776240
- 定数固有クラス本体と定数固有データを持つenum型
定数固有クラス本体と定数固有データを持つenum型
public enum Operation {
PLUS("+") {
// applyをオーバーライドする
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
@Override
public String toString() {
return symbol;
}
// コンストラクタ
Operation(String symbol) {
this.symbol = symbol;
}
// abstractでメソッドを定義すれば、各euum定数はメソッドのオーバーライドが義務付けられる
public abstract double apply(double x, double y);
// 呼び出し例
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
for (Operation op : Operation.values()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
}
項目35 序数の代わりにインスタンスフィールドを使う
- enum型には、自動的にint値が割り当てられるが、、欠番や重複が出た時に対応できないので使うべきではない。int値を使う場合は、明示的に割り当てる。
インスタンスフォールドに値を保存する
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(0), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
// 数値を持たせたいときは、コンストラクタに定義する
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int getNumberOfMusicians() {
return numberOfMusicians;
}
// 呼び出し例
public static void main(String[] args) {
for (Ensemble e : Ensemble.values()) {
System.out.printf("%s %s %n",e.name(), e.numberOfMusicians);
}
}
}
// 実行結果
SOLO 1
DUET 2
TRIO 3
QUARTET 4
QUINTET 5
SEXTET 6
SEPTET 7
OCTET 8
DOUBLE_QUARTET 8
NONET 0
DECTET 10
TRIPLE_QUARTET 12
項目36 ビットフィールドの代わりにEnumSetを使う
- ビットフィールドとは、a=1、b=2、c=4、d=8 とした場合、abcを1+2+4=7と表現するやり方。
- EnumSetは複数のenum型を持てるSetである。
EnumSetの例
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE STRIKETHROUGH }
// SetでEnumSetを受け取る
public void applyStyles(Set<Style> styles) { ... }
}
// EnumSetの使用例
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC);
項目37 序数インデックスの代わりにEnumMapを使う
- EnumSetとは、enumをキーとして使うように設計された高速なMapの実装。
EnumSetの使用例
public class Plant {
enum LifeCycle {ANNUAL, PERENNIAL, BIENNIAL;}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
// 呼び出し例
public static void main(String[] args) {
var garden = List.of(new Plant("annual", LifeCycle.ANNUAL),
new Plant("biennial1", LifeCycle.BIENNIAL),
new Plant("biennial2", LifeCycle.BIENNIAL));
// EnumMapのインスタンスを作成する
Map<LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
// キー:enum型LifeCycle、値が空のHashSetを持つMapを追加する
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
// MapのHashSetに値を追加する。
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
}
}
// 出力結果
{ANNUAL=[annual], PERENNIAL=[], BIENNIAL=[biennial1, biennial2]}
項目38 拡張可能なenumをインタフェースで模倣する
- enum型は拡張できないが、インタフェースをenum型で実装することは可能である。
インタフェースをenumで実装する
// インタフェースの定義
public interface Operation {
double apply(double x, double y);
}
// インタフェースを実装する
enum BasicOperation implements Operation {
PLUS("+") {
// applyを実装する
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
// コンストラクタ
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
// 拡張enum型
enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
// コンストラクタ
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
// 呼び出し例
class Main {
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
text1(BasicOperation.class, x, y);
text1(ExtendedOperation.class, x, y);
text2(Arrays.asList(BasicOperation.values()), x, y);
text2(Arrays.asList(ExtendedOperation.values()), x, y);
}
// enumかつOperationのサブタイプとして渡す
private static <T extends Enum<T> & Operation> void text1(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
// 境界ワイルドカード型を使って渡す方法
private static void text2(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
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
2.000000 ^ 4.000000 = 16.000000
2.000000 % 4.000000 = 2.000000
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
2.000000 ^ 4.000000 = 16.000000
2.000000 % 4.000000 = 2.000000
項目39 命名パターンよりもアノテーションを選ぶ
- メソッド名などに命名パターンによる特別な性質を持たせることは欠点があるため、アノテーションを使うべきである。(例:testSafetyOverrideはtestから始まるのでテストメッド、は悪い例)
- Testアノテーションの定義
// @Testアノテーションの定義
@Retention(RetentionPolicy.RUNTIME) // Testアノテーションが実行時に保持されるべき
@Target(ElementType.METHOD) // メソッド宣言に対してのみ許される
@interface Test {
}
// テスト対象クラス
class Sample {
@Test
public static void m1() {
}
public static void m2() {
}
@Test
public static void m3() {
throw new RuntimeException("Boom");
}
public static void m4() {
}
@Test
public static void m5() {
}
public static void m6() {
}
@Test
public static void m7() {
throw new RuntimeException("Crash");
}
public static void m8() {
}
}
/// テストアノテーション処理クラス
class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("sec39.Sample");
// テスト対象内の全メソッドをループする
for (Method m : testClass.getDeclaredMethods()) {
// @Testアノテーションが付いているか?
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
// テスト対象メソッド呼び出し
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception e) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
// 実行結果
public static void sec39.Sample.m7() failed: java.lang.RuntimeException: Crash
public static void sec39.Sample.m3() failed: java.lang.RuntimeException: Boom
Passed: 2, Failed: 2
項目40 常にOverrideアノテーションを使う
- オーバーライド時に、
@Override
アノテーションを付けることは、オーバーライドされていなかったことを検知してくれる。 -
@Override
アノテーションを付けないでオーバーライドしようとすると、IDEは警告を出してくれるので、意図しないオーバーライドを避けることができる。
項目41 型を定義するためにマーカーインタフェースを使う
- マーカーインタフェースとは、メソッド宣言を含んでいない(空っぽの)インタフェースで、そのインタフェースを実装しているクラスは何らかの特性を持っていることを指定(マーク)する。
- 関連づけられた新たなメソッドを持たない型を定義したいなら、マーカーインタフェースであるべき。
- クラスとインタフェース以外のプログラム要素をマークしたい、あるいはアノテーション型をすでに多用しているフレームワーク内にマーカーを適合させたいなら、マーカーアノテーションを選ぶべき。