Effective Javaを勉強します【第7章】
項目38. パラメータの正当性を検査する
コンストラクタやメソッドの引数(パラメータ)に関して何らかの制約(参照がnull
ではいけないなど)がある場合は、
文書化し、メソッドの初めに検査をすることでその制約を強制すべきである。
上記を行わない場合、意図せぬ例外を引き起こしたり、オブジェクトが不正な状態のまま離れた場所でエラーを引き起こし、デバッグが困難になる恐れがある。
public
のメソッドの場合
スローされる例外を、Javadocの@throws
タグを使用して文書化する。
/** * 値が(this mod m)であるBigIntegerを返します。このメソッドは、 * remainderメソッドとは異なり、常に負でないBigIntegerを返します。 * * @param m 正でなければならないモジュラス * @return this mod * @throws ArithmeticException m <= 0の場合 */ public BigInteger mod(BigInteger m) { ... }
public
でないメソッドの場合
アサーションを用いてパラメータをチェックする。
private static void sort(long a[], int offset, int length) { assert a!= null; assert offset >= 0 && offset <= a.length; assert length >= 0 && length <= a.length - offset; ... }
項目39. 必要な場合には、防御的にコピーする
クライアントからオブジェクトを受け取るメソッドやコンストラクタを書くときはいつでも、 クライアントが提供するオブジェクトが可変である可能性を考慮すべきである。
オブジェクトを受け取った後に変更があった場合に、クラスに問題が生じる可能性がある場合は、 受け取ったオブジェクトをそのまま利用するのではなく、防御的にコピーして利用しなければならない。
public class Period { private final Date start; private final Date end; /** * @param start 期間の開始。 * @param end 期間の終わり。開始より前であってはならない。 * @throws IllegalArgumentException start が end の後の場合。 * @throws NullPointerException start か end が null の場合 */ public Period(Date start, Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + " after " + end); } this.start = start; this.end = end; } public Date getStart() { return start; } public Date getEnd() { return end; } }
上記には2つの問題がある。
1つ目はコンストラクタで、start
とend
をそのまま受け取っている点である。
Date
クラスは可変であるため、以下のように操作することでクラスの制約を破ることが可能。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); //endの時間を操作可能
上記はコンストラクタを以下のように修正することで解決する。 コピーに対して正当性検査を行うことで、検査までの間にパラメータを不正に変更されることから保護している。
public Period(Date start, Date end) { // 防御的コピー this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + " after " + end); } }
2つ目の問題は、アクセッサーで内部のstart
とend
をそのまま返却している点である。
受け取ったstart
とend
は可変なので、以下のように操作することでクラスの制約を破ることが可能。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78); //endの時間を操作可能
この点については、以下のようにアクセッサーで防御的コピーを返すように修正することで解決する。
public Date getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); }
一番良いのは、可能な限り不変オブジェクトを使い、不必要に可変オブジェクトを使わないこと。
項目40. メソッドのシグニチャを注意深く設計する
API設計時は以下の点を考慮すること。
メソッド名を注意深く選ぶ
- 理解可能で、パッケージ内の他のメソッド名やjavaの標準ライブラリと矛盾が生じないようなメソッド名を選ぶ
便利なメソッドを提供しすぎない
- 多くのメソッドを定義したインタフェース/クラスは学習、使用、文書化、テスト、保守などを困難にする
- 多くの役割を与えすぎないこと
長いパラメータのリストは避ける
- 4個以下が望ましい(それ以上は大抵覚えられない)
- パラメータのリストを短くする方法は例えば以下
- メソッドを分割する
- パラメータの集まりを保持するヘルパークラスを作成する(
static
なメンバークラスなど) - ビルダーパターンをオブジェクト生成からメソッド呼び出しに適用する
パラメータ型に関しては、クラスよりインタフェースを選ぶ
- 例えば、
HashMap
の代わりにMap
を使用すれば、HashTable
やHashMap
、TreeMap
などどれでも渡すことができるため、柔軟かつ拡張性も高い
- 例えば、
boolean
パラメータより2つの要素を持つenum
型を使用するboolean
よりもパラメータの意味が理解しやすく、可読性が向上するため
項目41. オーバーロードを注意して使用する
オーバーロードしたメソッドの選択は静的(コンパイル時に決定)であるため、ユーザーを混乱させる可能性があるため。
オーバーロードの利用
以下の例を考える。
public class CollectionClassifier { public static String classify(Set<?> a) { return "Set"; } public static String classify(List<?> a) { return "List"; } public static String classify(Collection<?> a) { return "Unknown Collection"; } public static void main(String[] args) { Collection <?>[] collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String, String>().values() }; for (Collection<?> c : collections) { System.out.println(Overload.classify(c)); } } }
上記のプログラムを実行した場合、"Set" → "List" → "Unknown Collection" と表示すると期待するかもしれないが、 実際は "Unknown Collection" が3回表示される。
これは、オーバーロードされたメソッドの選択はコンパイル時に行われるためである。
コンパイル時には、3回のループでclassify
メソッドに渡されるパラメータの型は全てCollection<?>
である。
したがって同じ数のパラメータを持つ複数のシグニチャでメソッドをオーバーロードすることは、基本的に控えるべきである。
例えばObjectOutputStream
クラスは複数のwrite
メソッドを持っているが、
writeBoolean(boolean)
、writeInt(int)
、writeLong(long)
といったように異なったメソッド名を用いているため、
ユーザーは混乱しない。
またコンストラクタの場合、異なる名前を使用することができないため、staticファクトリーメソッドの利用を検討する。
オーバーライドの利用
オーバーライドしたメソッドの選択は動的に行われる。
class Wine { String name() { return "wine"; } } class SparklingWine extends Wine { @Override String name() { return "sparkling wine"; } } class Champagne extends SparklingWine { @Override String name() { return "champagne"; } } public class Overriding { public static void main (String[] args) { Wine[] wines = { new Wine(), new SparklingWine(), new Champagne() }; for (Wine wine: wines) { System.out.println(wine.name()); } } }
上記を実行すると、 "wine" → "sparkling wine" → "sparkling wine" と表示される。 オーバーライドされたメソッドの呼び出しには、オブジェクトのコンパイル時の型は影響がない。
項目42. 可変長引数を注意して使用する
可変長引数メソッド
指定された型の0個以上の引数を受け付けるメソッドを、可変長引数メソッドと呼ぶ。 可変長引数の仕組みは、呼び出した時点で渡された引数の数と同じ大きさの配列を最初に生成し、 その配列に引数値を入れて、最後のその配列をメソッドに渡す。
例えば、複数のint
変数の合計を返す可変長引数メソッドは以下。
static int sum(int ... args) { int sum = 0; for (int arg: args) { sum += arg; } return sum; }
ある型の引数を0個以上ではなく1個以上必要とする場合は、第1引数を通常の引数とし、第2引数を可変長引数とするとよい。
static int min(int firstParam, int... params) { int min = firstParam; for (int param: params) { if (param < min) { min = param; } } return min; }
可変長引数メソッドへの変更時の注意
可変長引数は最終的に配列としてメソッドに渡されるため、最後の引数が配列である既存のメソッドを可変長引数メソッドに 変更することは容易である。しかし、むやみに可変長引数メソッドに変更すべきではない。
例えば Arrays.asList
メソッドは、1.4までは Object
型の配列を受け取って、List
型に変換していた。具体的には、配列の各要素を取り出し、List
に格納し返却している。下記のコードでは、配列を List
に変換することで、配列の各要素を確認することができている(配列に対して直接 toString
を呼び出しても、ハッシュ値が表示されて役に立たない)。
System.out.println(Arrays.asList(myArray));
上記の myArray
が int
型の配列の場合、コンパイルエラーとなる。なぜなら java.lang.Object[]
と int[]
の間に継承関係はないためである。
しかし、1.5から Arrays.asList
メソッドは、Object
型の配列ではなく、Object
型の可変長引数を受け取るようになった。この時 myArray
が int
型の配列の場合、Object
型に int[]
は渡すことができるため、コンパイルエラーとはならないが、int[]
の myArray
のみを要素とする List<int[]>
が返却される。すると、要素は int[]
であるため、ユーザーの意図する各要素の値ではなく配列のハッシュ値が表示されてしまう。
このように、可変長引数メソッドを乱用するとユーザーを困惑させることになりかねないため、注意が必要である。
項目43. null ではなく、空配列か空コレクションを返す
配列やコレクションを返すメソッドが、空配列や空コレクションの代わりに、null
を返すべきではない。
null
を返却すると、クライアントで null
を処理する余分なコードを書かなければならないため。
もしチェックを忘れると NullPointerException
が発生する恐れもある。
null
を返す場合と比較して、空配列を返す方が配列を割り当てるコストがかかるため、好ましくないと言われることがあるが、
これは正しくない。なぜなら、このレベルでパフォーマンスが問題となることはほとんどない。
また、どうしても空配列を返却したい場合は、次のように同一のオブジェクトを使いまわせば良い。 下記イディオムはコレクションが空の場合、空配列を新規に生成しないため、空配列生成のコストは問題とならない。
private final List<Cheese> cheeses = ...; private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; public Cheeses[] getCheeses() { return cheeses.toArray(EMPTY_CHEESE_ARRAY); }
項目44. すべての公開API要素に対してドキュメントコメントを書く
Javadoc を利用することで、ソースコードから自動的にAPIドキュメンテーションを生成することができる。 APIを適切に文書化するために、全ての公開されているクラス、インタフェース、コンストラクタ、メソッド、フィールド宣言の前に ドキュメントコメント(Javadoc)を書くべきである。
Javadoc を書く際は以下の点に気をつける。
メソッドに関するドキュメントコメントには、メソッドがどのように処理を行なっているかではなく、何を行なっているかを記述すべき
- 事前条件と事後条件をすべて列挙すること
- すべての副作用を文書化すること
- スレッド安全性について記述すること
- 上記を記載するために
@param
タグ、@return
タグ、@throws
タグを記載すること
/** * An object that maps keys to values. A map cannot contain * duplicate keys; each key can map to at most one value. * * @param <K> the type of keys maintained by this map * @param <V> the type of mapped values */ public interface Map<K, V> { ... }
enum
型を文書化する際には、型とすべてのpublic
のメソッドだけでなく定数も文書化すること
/** * An instrument section of a symphony orchestra. */ public enum OrchestraSection { /** Woodwinds, such as flute, clarinet, and oboe. */ WOODWIND, /** Brass instruments, such as french horn and trumpet. */ BRASS, ... }
- アノテーションを文書化する際には、その型自身だけでなく、全てのメンバーを文書化する
/** * Indicates that the annotated method is a test method that * must throw the designated exception to succeed. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { /** * the exception that the annotated test method must throw * in order to pass. (The test is permitted to throw any * subtype of the type described by this class object.) */ Class<? extends Throwable> value(); }
Effective Javaを勉強します【第6章】
項目30. int定数の代わりにenumを使用する
一年での季節、太陽系の惑星、トランプの役など、固定数の定数からその値が成り立つ型を列挙型と呼ぶ。Java4以前で列挙型を表現する際には int enum パターンが用いられていたが、Java 5以降では enum 型を用いて列挙型を表現すべきである。
int enum パターン
以下は int enum パターンの例である。
public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY_SMITH = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLAE = 1; public static final int ORANGE_BLOOD = 2;
上記は以下のような欠点を持つ。
- 型安全性を提供しない。オレンジを期待するメソッドにアップルを渡したり、オレンジとアップルを
==
演算子で比較しても何のエラーにもならない。 - 名前空間を提供しない。
APPLE_
等の接頭辞で名前の衝突を防ぐ手間がかかる。 - int enum 定数を変更する度に再コンパイルの必要がある。再コンパイルされない場合、動作はするが結果がどうなるかはわからない。
- 利便性が低い。デバッガ等で表示しても文字情報は取得することができず、定数をイテレートするなどの操作手段もない。
int 定数の代わりに String 定数が用いられる場合もあるが、文字列比較のコストやハードコートした際の誤字によるバグを考慮すると、さらに望ましくない。
enum 型
上記で説明した int enum 型の欠点を回避するのが enum 型である。以下はその例である。
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }
enum 型は内部的にはクラスであり、クラス同様パッケージに属する。基本的には1ファイルに火取るの enum 型を定義し、型名と同じファイル名のファイルに記述する。内部クラスのように他のクラスの内部に定義することも可能。
上記の Apple
を定義した Apple
クラスを javap
コマンドで逆コンパイルすると、以下のように表示される。
Compiled from "Apple.java" public final class Apple extends java.lang.Enum<Apple> { public static final Apple FUJI; public static final Apple PIPPIN; public static final Apple GRANNY_SMITH; public static Apple[] values(); public static Apple valueOf(java.lang.String); static {}; }
これにより以下のようなことが分かる。
- enum 宣言された型は
java.lang.Enum
を継承している - 個々の列挙定数は
public static final
であり、自身が宣言された型のインスタンスである - enum 型はアクセス可能なコンストラクタを持っていないため事実上 final であり、クライアントは enum 型のインスタンスの生成や拡張は不可能である(シングルトン)
enum は型安全性を提供している。たとえば型 Apple
で引数を宣言した場合、null
でないオブジェクト参照は必ず FUJI / PIPPIN / GRANNY_SMITH
のいずれかの値であることが保証される。また、異なる enum 型の値を渡そうとしたり、異なる enum 型の値同士を ==
で比較しようとするとコンパイル時エラーになる。
また、各 enum 型は独自の名前空間を提供するため、同一名の定数が共存可能である。加えて toString
メソッドにより文字列へ変換することも可能である。
enum型 へのフィールド・メソッドの追加
enum 型に任意のフィールド・メソッドを追加することにより、定数にデータを関連づけることが可能である。
例えば、太陽系の惑星について考えてみる。各惑星は、質量と半径を持ち、それらから表面重力や惑星表面上での物体の重さが計算可能である。
これらを enum で表現すると以下のようになる。
public enum Planet { MERCURY (3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6), JUPITER (1.9e+27, 7.1492e7), SATURN (5.688e+26, 6.0268e7), URANUS (8.686e+25, 2.5559e7), NEPTUNE (1.024e+26, 2.4746e7); private final double mass; private final double radius; private final double surfaceGravity; public static final double G = 6.67300E-11; Planet(double mass, double radius) { this.mass = mass; this.radius = radius; this.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; } }
enum にデータを関連づけるためには、インスタンスフィールドを宣言し、データを受け取るコンストラクタを書き、そのフィールドにデータを保存すれば良い。enum は不変であるため、すべてのフィールドは final
とすべきである。
enum の値セットを取得するにはvalues()
メソッドを用いる。これにより、各惑星上での物体の重さを計算するプログラムを以下のように記述することができる。
double earthWeight = 1.0; double mass = earthWeight / Planet.EARTH.surfaceGravity(); // 地球上で重さ1.0の物体の各惑星上での質量を計算する for (Planet p : Planet.values()) System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
定数固有メソッド実装
Planet
クラスの例では、定数ごとのフィールド値が異なってもメソッドの振る舞いは同じだったが、各定数にメソッドの振る舞いを変えたい場合がある。そのような時には定数固有メソッド実装を利用する。以下はその例。
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); }
このように enum 内で抽象メソッドを宣言し、各定数ごとにオーバーライドすることで振る舞いを変化させることができる。仮に実装を忘れたとしても、コンパイル時エラーとなりそのまま動作することはない。
また、定数固有メソッドは定数固有データを同時に利用することができる。
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; } abstract double apply(double x, double y); }
fromString メソッドの実装の検討
enum はデフォルトで、定数の名前を定数自身へ変換する valueOf(String)
メソッドを持っている。(上記の Operation
の場合、返り値の型は Operation
)
もし enum 型で toString
メソッドをオーバーライドする場合、それに対応する fromString
メソッドの実装を検討すべきである。 fromString
メソッドは、引数で渡されたカスタム文字列を定数自身へ変換する。
private static final Map<String, Operation> stringToEnum = new HashMap<>(); // toString() で定義したカスタム文字列を Map で記憶する static { for (Operation op : values()) { stringToEnum.put(op.toString(), op); } } // toString() でカスタム文字列を定義 @Override public String toString() { return symbol; } // toString に対応する fromString() public static Operation fromString(String symbol) { return stringToEnum.get(symbol); }
パフォーマンス
一般的に、パフォーマンスに関して enum は int 定数と比べても遜色ない。小さな差ではあるが、enum 型のロードと初期化には空間的・時間的コストが発生する。しかし資源制約の厳しいデバイス等でなければ、そのコストが顕著になることはない。
項目31. 序数の代わりにインスタンスフィールドを使用する
enum は ordinal()
というメソッドを持っている。このメソッドは enum 型内の各 enum 定数の整数位置を返す。
public enum Ensemble { SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET; // Ensemble.SOLO.ordinal() であれば 1 を返す public int numberOfMusicians() { return ordinal() + 1; } }
上記は動作するが、以下の理由より保守性が非常に低い。
- 定数が並び替えられた場合、
numberOfMusicians
は動作しなくなる - 複数の定数の同じ整数値を割り当てることができない
- 例えばダブルカルテット(4人×2)の場合、オクテット(8人)との区別ができない
そもそも ordinal
メソッドは、EnumSet や EnumMap などの汎用の enum をベースとしたデータ構造の実装を手助けする手段として提供されている。したがって普通の開発者は ordinal
メソッドを使うべきではない。
代わりに、下記のようにインスタンスフィールドを用いて、整数値を関連づける。これにより上記の保守性の問題はすべて解決する。
public enum Ensemble { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12); private final int size; Ensemble(int size) { this.size = size; } public int numberOfMusicians() { return this.size; } }
項目32. ビットフィールドの代わりにEnumSetを使用する
enum が集合で用いられる場合、従来はビットフィールドが利用されてきたが int enum パターンと同様の欠点を持つ。 代わりに EnumSet を用いることで、ビットフィールドの簡潔性とパフォーマンスを維持しつつ、欠点を解決することが可能である。
ビットフィールド
ビットフィールドとは、列挙型の各定数に異なる2の累乗を割り当てて、int enum パターンを使用する方法である。
public class Text { public static final int STYLE_BOLD = 1 << 0; // 1 public static final int STYLE_ITALIC = 1 << 1; // 2 public static final int STYLE_UNDERLINE = 1 << 2; // 4 public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 // パラメータは、0個以上のSTYLE_定数のビット public void applyStyles(int styles) { ... } }
ビットフィールドとして知られている複数の定数を1つの集合にまとめるために、 以下のようにビット和操作を行う。
// 以下はtext変数がボールドかつイタリックであることを指す(引数は 0011)
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
ビットフィールドは和集合や共通集合などの操作を効率的に行うが、int enum 定数の短所を全て持っている。
EnumSet
ビットフィールドの代わりに、EnumSet が利用可能である。 EnumSet は Set インタゲースを実装しており、豊富な機能、安全性、他の Set 実装との相互運用性が提供されている。 またパフォーマンスも、ビットフィールドと比べても遜色ない。
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // どのようなSetでも渡せるが、EnumSet が明らかに最善 public void applyStyles(Set<Style> styles) {...} }
各属性は以下のように設定可能である。
// 以下はtext変数がボールドかつイタリックであることを指す
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
項目33. 序数インデックスの代わりにEnumMapを使用する
下記のようなハーブを表現したクラスがあるとする。 ハーブは3種に分類可能であり(一年生植物(annual)、多年生植物(perennial)、越年性植物(biennial))、 それらを enum で表現している。
public class Herb { enum Type { Annual, Perennial, Biennial } private String name; private String type; public Herb(String name, String type) { this.name = name; this.type = type; } }
ここで、ハーブを種類ごとに Set などのコンテナに格納したいとする。 この時、種類の序数でインデックスされた配列で格納先の Set を区別してはいけない。 このような時は EnumMap を用いるのが適切である。
序数インデックスを用いた配列による分類
種類の序数でインデックスされた配列に各種ハーブを格納する場合、以下のようなコードになる。
// 種類の混在したハーブの集合 Herb[] all = new Herb[] { ... }; // 3種類のハーブに対応した3つの Set を配列にする Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; for (int i = 0; i < herbsByType.length; i++) { herbsByType[i] = new HashSet<Herb>(); } // ハーブを種類ごとに分類 for (Herb h: all) { herbsByType[h.type.ordinal()].add(h); }
この方法は以下の問題を抱える。
- 配列はジェネリックスと互換性がないため、無検査キャストを必要とし、コンパイル時には警告が出る
- 配列のインデックスからハーブの種類を読み取ることができない
- 配列のアクセス時に、意図した種類と対応したインデックスが利用されるかはクライアント次第(型安全でない)
EnumSet を用いた分類
EnumSet を用いてハーブを分類する場合、以下のようなコードになる。
// 種類の混在したハーブの集合 Herb[] all = new Herb[] { ... }; // ハーブの種類をキー、値を Set としたMapを作成 Map<Herb.Type, Set<Herb>> map = new EnumMap<>(Herb.Type.class); for (Herb.Type type: Herb.Type.values() { map.put(herb.type, new HashSet<Herb>()); } // ハーブを種類ごとに分類 for (Herb herb: all) { map.get(herb.type).add(all); }
このプログラムは序数インデックスを用いる方法と比べて
- より短く、より明瞭、より安全
- パフォーマンスも遜色ない
- 型安全
多次元関係
2つの enum からの対応付けを表す時も同様であり、序数を用いるよりEnumSetを用いた方が良い。 例えば水の相転移を表現したいとする。(液体(liquid)から個体(solid)は凍結(fleezing)、液体から気体(gas)は沸騰(boiling))
以下は序数を用いる例。
public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; // 状態遷移を配列で表現 Transition[][] TRANSITIONS = { { null, MELT, SUBLIME}, { FREEZE, null, BOIL}, { DEPOSIT, CONDENSE, SUBLIME} }; // 状態遷移時の現象を取得する public static Transition from(Phase src, Phase dst) { return TRANSITIONS[src.ordinal()][dst.ordinal()]; } } }
上記は、ハーブの分類を序数を用いて行った時と同様に、以下の欠点を持つ
- 序数と配列のインデックスの関係をコンパイラは知らないため、転移表に誤りがあったりした場合、実行時エラーとなる
- null が増えると空間が無駄になる
ハーブの例と同様に、EnumMap を用いることでこれらの問題は解決する。
新たにプラズマという相を追加する場合であっても、Transition
と Phase
に定数を追加するだけで良い。
public enum Transition { MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID); private final Phase src; private final Phase dst; public Transition(Phase src, Phase dst) { this.src = src; this.dst = dst; } // 二重 Map private static final Map<Phase, Map<Phase, Transition>> map = new EnumMap<Phase, Map<Phase, Transition>>(Phase.class); static { for (Phase phase: Phase.values()) { map.put(phase, new EnumMap<Phase, Transition>(Phase.class)); } for (Transition transition: Transition.values()) { map.get(transition.src).put(transition.dst, transition); } } public static Transition from(Phase src, Phase dst) { return map.get(src).get(dst); } }
項目34. 拡張可能なenumをインタフェースで模倣する
基本の enum
型に伴うインタフェースを書いて、そのインタフェースを実装することで拡張可能な enum
型を模倣することが可能。
オペコード
オペコードは拡張可能な列挙型を使わざるをえない場面の1例。enum
型が任意のインタフェースを実装できるという事実を利用して、
オペコードに関するインタフェースを定義し、それを実装した enum
を定義してやればよい。
interface Operation { double apply(double x, double y); } public enum BasicOperation implements Operation { PLUS("+") { @Override public double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override public double apply(double x, double y) { return x - y; } }; private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } } // Enumは拡張不可だが、共通インタフェース Operation を利用して拡張した操作を定義している public 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; } }
項目35. 命名パターンよりアノテーションを選ぶ
命名規則とは、JUnitのように「テストケースメソッドの先頭には"test"をつける」といったルールのこと。
命名規則の代わりにアノテーションを利用することには以下のようなメリットがある。
- 命名の際の誤字などをコンパイラでチェックできる
- パラメータを関連づけることができる
- クラスやメソッドなど、利用箇所を限定することができる
アノテーション
JavaにはOverride
アノテーションなど、いくつかのアノテーションが用意されているが、自分でアノテーションを定義することも可能。
その際は@interface
を用いる。
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { }
また@Target(ElementType.METHOD)
のように、定義時に別のアノテーションを付与することもできる(メタアノテーション)。
@Retention(RetentionPolicy.RUNTIME)
はTest
アノテーションがメソッド宣言にのみ付与できることを示している。
@Target(ElementType.METHOD)
はコンパイラによってクラスファイルに記録し、実行時にVMに保持することを示している。
マーカーアノテーション
Test
アノテーションのように、パラメータを付与された要素を「マーク」するためのアノテーションを指す。
public class SampleTest { @Test public static void m1() {} public static void m2() {} @Test public static void m3() {} }
上記の場合Test
アノテーションが付与されたm1()
とm2()
のみがテストとして実行される。
パラメータを持つアノテーション
ある特定の例外がスローされた場合のみ成功するテストを表すアノテーションを定義することを考える。 そのためには、アノテーションに特定の例外が何か教えなければならない。
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest{ Class<? extends Exception> value(); }
上記のように定義することで、ExceptionTest
アノテーションに、特定の例外を表すパラメータを与えることが可能となる。
以下のように例外を期待するテストに利用する。
public class SampleTest2 { @ExceptionTest(ArithmeticException.class) public static void m1() { int i = 0; i = i / i; } }
項目36. 常にOverrideアノテーションを使用する
スーパークラスの宣言をオーバーライドしている全てのメソッド宣言に対して
Override
アノテーションを使用すべき。
Override
アノテーションを使用することで、例えば以下のようなオーバーライドミスを
コンパイラが教えてくれる。
public class Bigram { private final char first; private final char second; public Bigram(char first, char second) { ... } // 以下は間違い。正しくは public boolean equals(Object o) {} public boolean equals(Bigram b) { ... } public int hasCode() { ... } }
Bigram
クラスでは equals` メソッドのオーバーライドに失敗し、誤ってオーバーロードしている。
このようなミスは見つけづらいが
以下のように Override
アノテーションを利用することでコンパイラがエラーメッセージを出力してくれる。
@Override public boolean equals(Bigram b) { ... }
項目37. 型を定義するためにマーカーインタフェースを使用する
マーカーインタフェースとは、メソッド宣言を含まないインタフェースのことであり、 そのインタフェースを実装しているクラスが何らかの特性を持っていることを示す。
例として Serializable
インタフェースが挙げられる。これはメソッドを1つも持たないが、
ObjectOutputStream
に利用できるという特性を示す。
マーカーインタフェースは型を定義できるため、コンパイル時のエラーチェックが可能であるという点で マーカーアノテーションより優れている。
ただし、クラスやインタフェース以外のプログラム要素をマークしたい場合は、マーカーアノテーションを使う必要がある。 (マーカーインタフェースはクラス以外をマークすることができない。)
Effective Javaを勉強します【第5章】
項目23. 新たなコードで原型を使用しない
ジェネリックとは
1つ以上の型パラメータを宣言に持つクラスやインターフェースのこと。
例)
public interface List<E> extends Collection<E> { ... }
List<E>
:ジェネリック型E
:仮型パラメータList<String>
:パラメータ化された型String
:実型パラメータList
:原型
原型を使用すべきでない
以下はリリース1.5以前の原型を用いたコレクション型の例。 コンパイルエラーにもならず、コレクションから要素を取り出し利用するまでエラーに気づくことができない。
// Stampsインスタンスだけを含むことを想定 private final Collection stamps = ...; // 誤ってCoinインスタンスを代入してしまう stamps.add(new Coin(...)); // 要素を取り出す for (Iterator i = stamps.iterator(); i.hasNext(); ) { Stamp s = (Stamp) i.next(); // ClassCastExceptionが発生する }
以下はジェネリックスを用いたコレクション型の例。
stamps
はStamp
インスタンスだけを含んでいることがコンパイラにより保障される。
また要素取り出しの際のキャストも不要である。(コンパイラが自動でキャストしてくれる)
// 型安全 private final Collection<Stamp> stamps = ...; // 以下のように書いた場合はコンパイルエラーになる stamps.add(new Coin(...)); // 要素の取り出し(型安全) for (Stamp s : stamps) { // キャスト不要 ... } for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); ) { Stamp s = i.next(); // キャスト不要 }
List<Object>
List
などの原型は使用すべきでないが、任意のオブジェクトの挿入が可能なList<Object>
を利用することは問題ない。
前者と後者の違いは、後者はコンパイラにより型検査が行われるということ。
例えば、以下のように原型を用いた場合はコンパイル時に警告は出るが実行は可能であり、ClassCastException
がスローされる。
public static void main (String[] args) { List<String> strings = new ArrayList<String>(); unsafeAdd(strings, new Integer(42)); String s = strings.get(0); //ClassCastException } private static void unsafeAdd(List list, Object o) { list.add(o); }
unsafeAdd
の宣言をprivate static void unsafeAdd(List<Object> list, Object o)
に書き換えた場合、コンパイルエラーとなる。
これはList<String>
はList<Object>
のサブタイプではないためである。(ジェネリックス型に継承関係はない)
strings
がList<Object>
型であればInteger
も格納可能である。
非境界ワイルドカード型
ジェネリックスを利用したいが、実際の型パラメータが分からない、または気にしたくない、という場合は非境界ワイルドカード型を利用する。
非境界ワイルドカード型では、型パラメータの代わりに?
記号を用いる。
非境界ワイルドカード型はすべてのパラメータ化された型パラメータのスーパータイプである。
例:List<?>
はList<Object>
、List<String>
のスーパークラス
以下は非境界ワイルドカード型の利用例である。
static int numElementsInCommon(Set<?> s1, Set<?> s2) { int result = 0; for (Object o1:s1) { if (s2.contains(o1)){ result++; } } return result; }
原型のリストと非境界ワイルドカードのリストの違いは型安全性である。
原型のリストにはいかなる要素も代入可能だが、非境界ワイルドカード型のリストにはnull
を除いた通常のオブジェクトは代入不可能である。
例外的に原型を使うべき場合
- クラスリテラルを使う場合
List<String>.class
やList<?>.class
は不可
instanceof
と一緒に使う場合
// Setであるかを確認して処理する if (o instanceof Set) { // 事前にSetであることを検査しているので警告は出ない Set<?> m = (Set<?>) o; ... }
項目24. 無検査警告を取り除く
ジェネリックスの利用時は以下のようなコンパイラの警告をよく目にする。
- 無検査キャスト警告
- 無検査メソッド呼び出し警告
- 無検査ジェネリック配列生成警告
- 無検査変換警告
無検査警告への対処は以下。
- 基本的に取り除くことが可能なすべての無検査警告を取り除く
- 警告を取り除くことができず、かつ該当箇所が型安全であると明示できる場合、
@SuppressWarnings("unchecked")
アノテーションで警告を抑制してよい- できるだけ最小のスコープに対して使用すること(クラス全体に使用することは絶対にしない)
- 型安全であると明示できる理由をコメントに残すこと
項目25. 配列よりリストを選ぶ
ジェネリックスは不変である
配列型Sub[]
はSuper[]
のサブタイプであるが、List<Sub>
とList<Super>
の間に継承関係は存在しない。
以下のような場合、配列を利用すると実行時にエラーがスローされるまで間違いに気づかない可能性があるが、 リストの場合コンパイルエラーで知ることができる。
Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; // ArrayStoreExceptionのスロー List<Object> ol = new ArrayList<Long>(); // 互換性のない型でコンパイルエラー ol.add("I don't fit in");
ジェネリックスはイレイジャで実装されている
配列は実行時にその要素型を知っており、それを強制する。 一方でジェネリックスはコンパイル時のみ型制約を強制し、実行時には型情報が廃棄(erase)されるため、柔軟性が高く既存コードとの相互運用も容易である。
配列とジェネリックスが混在している時は、大抵は配列をリストに置き換えた方が良い
配列とジェネリックスは上手く調和しないので、混在している時は大抵は配列をリストに置き換えた方が良い。
項目26. ジェネリック型を使用する
以下のスタック実装を例に、ジェネリック型の書き方を学ぶ。
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; return result; } public boolean isEmpty() { return size == 0; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
上記の問題点は、スタックから取り出した値をクライアント側でキャストしなければならない点である。 したがって以下のようにジェネリックスを用いた形に修正する。
public class Stack<E> { //修正 private E[] elements; //修正 private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正 } public void push(E e) { //修正 ensureCapacity(); elements[size++] = e; } public E pop() { //修正 if (size == 0) { throw new EmptyStackException(); } E result = elements[--size]; //修正 elements[size] = null; return result; } ... }
上記ではelements = new E[DEFAULT_INITIAL_CAPACITY];
でコンパイルエラーとなる。
これはE
など具象化不可能型の配列の生成はできないため。
したがって以下のいずれかの方法で回避する。
1. Object
配列を生成して、ジェネリック配列型へキャストする
elements = new E[DEFAULT_INITIAL_CAPACITY];
をelements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
にする。
これだけでは警告が出るので@suppressWanings("unchecked")
アノテーションを使って、警告を抑制する。
この時、項目24で述べた通りできる限り狭いスコープで抑制する必要がある。今回はコンストラクタ内に無検査配列生成の処理のみ含まれているため、
コンストラクタ全体で警告を抑制する。
// elements配列はpush(E)からのEインスタンスのみ挿入されるため問題なし @suppressWanings("unchecked") public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; //修正 }
2. フィールドelements
の型をE[]
からObject[]
に変更する
private E[] elements;
をprivate Object[] elements;
にして、
E result = elements[--size];
をE result = (E) elements[--size];
にすると
方法1同様に警告が発生するため、@suppressWanings("unchecked")
アノテーションを使って、警告を抑制する。
public E pop() { //修正 if (size == 0) { throw new EmptyStackException(); } // pushは要素が型Eであることを要求しているため問題なし @suppressWanings("unchecked") E result = (E) elements[--size]; elements[size] = null; return result; }
型パラメータへの制限
以下のように境界型パラメータを利用することで型パラメータに制限を設けることができる。
例) 実型パラメータがjava.util.concurrent.Delayed
のサブタイプでなければならないことを要求
class DelayQueue<E extends Delayed> implements BlockingQueue<E>;
これにより、明示的なキャストの必要性やClassCastException
の危険性なくDelayQueue
の要素に対してDelayed
のメソッドが利用可能。
項目27. ジェネリックメソッドを使用する
staticのユーティリティメソッドはジェネリックメソッドにできないか検討すべき良い候補である。
ジェネリックstaticファクトリーメソッド
ジェネリックコンストラクタでは、変数宣言の左右で型パラメータを指定しなければならず煩わしい。
Map<String, List<String>> map = new HashMap<String, List<String>>();
ジェネリックstaticファクトリーメソッドを利用すれば、上記を簡潔にできる。
// ジェネリック static ファクトリーメソッド public static <K, V> HashMap<K, V> newHashMap() { return new HashMap<K, V>(); } Map<String, List<String>> map = newHahMap();
が、これはJava7のダイヤモンド演算子で解決された。
// ダイヤモンド演算子によりコンストラクタの実型パラメータは省略可能に Map<String, List<String>> map = new HashMap<>();
ジェネリックシングルトンファクトリー
ジェネリックスを利用することで、多くの異なった型に適用可能な不変オブジェクトを生成することができる。
例) 恒等関数を作成する場合
恒等関数とは何らかの型T
の値を受け取って、その型の値を返す関数のこと。
内部状態は持たない(型が違うのみ)ためインスタンスを複数生成するのは無駄である。
したがってジェネリックスを用いた不変オブジェクトを返すとよい。
// ジェネリックシングルトンファクトリーパターン private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() { public Object apply(Object arg) { return arg; } }; // IDENTITY_FUNCTIONは状態を持たず、その型パラメータは非境界なので // すべての型に対して1つのインスタンスを共有するのは安全 @SuppressWarnings("unchecked") public static <T> UnaryFunction<T> identityFunction() { return (UnaryFunction<T>) IDENTITY_FUNCTION; }
上記は無検査キャスト警告が発生するが、IDENTITY_FUNCTION
は単に引数を返すだけなので安全であるため、
例外を抑制することができる。
再帰型境界
型パラメータがその型パラメータ自身が関係するなんらかの式で制限されていることを、再帰型境界と呼ぶ。
例) Comparableインタフェース
Comparableインタフェースは何らかの型T
と比較できることを示すインタフェースであり、
多くの場合、このインタフェースを実装する型は自分自身の型とのみ比較可能である。
public interface Comparable<T> { int compareTo(T o); }
リストのソート、検索、最小値/最大値の計算をするために、Comparable
を実装した要素のリストを受け取る
多くのメソッドがあるが、これを行うためにはリスト内の個々の要素同士の比較が可能である必要がある。
相互比較可能であることを示すジェネリックスは以下のように記述する。
// 再帰型境界 public static <T extends Comparable<T>> T max(List<T> list) { ... }
これはリストの要素T
はComparable<T>
のサブタイプであることを要求している。
すなわちリストの要素T
は自身の要素T
と比較可能であることという相互比較可能性を要求している。
項目28. APIの柔軟性向上のために境界ワイルドカードを使用する
不変性の扱いづらさ
不変性によりList<Object>
とList<String>
の間に継承関係が存在しないことはジェネリックスの良さではあるが、
故に扱いづらい時がある。
例として以下のスタックAPIを考える。
public class Stack<E> { public Stack() {} public void push(E element) { ... } public E pop() { ... } public boolean isEmpty() { ... } }
このスタックにInteger
型の要素を追加する場合、以下のように記述することになる。
Stack<Number> numbers = new Stack<Number>(); numbers.push(new Integer(1));
Integer
はNumber
のサブタイプであるため、上記は動作する。
ここで複数の要素をまとめて挿入するpushAll
メソッドを以下のように新しく実装するとする。
public void pushAll(List<E> src) { for (E e: src) { this.push(e); } }
この時、先ほどと同様にInteger
型の要素を挿入しようとした場合、コンパイルエラーとなってしまう。
Stack<Number> numbers = new Stack<Number>(); List<Integer> integers = ... ; numbers.pushAll(integers);
エラーとなる原因はジェネリックスの不変性により、List<Number>
とList<Integer>
の間に継承関係が存在しないためだが、
これはAPIとしては直感的でない。
同様に、スタックからInteger
型の要素を取り出す場合を考える。単一の要素を取り出す場合、以下のように記述することになる。
Stack<Number> numbers = new Stack<>(); numbers.add("1"); Object obj = numbers.pop();
Object
はInteger
のスーパータイプであるため、上記は動作する。
次に、引数として渡したリストにすべての要素を追加してくれるpopAll
メソッドを以下のように新しく実装するとする。
public void popAll(List<E> dst) { while (!isEmpty()) { dst.add(pop()); } }
この時、List<Object>
型のリストを渡そうとした場合、コンパイルエラーとなってしまう。
Stack<Number> numbers = new Stack<Number>(); numbers.add("1"); numbers.add("2"); List<Object> objects = ...; numbers.popAll(objects);
エラーとなる原因はジェネリックスの不変性により、List<Number>
とList<Object>
の間に継承関係が存在しないためだが、
やはりこれはAPIとしては直感的でない。
境界ワイルドカード型
上記のような問題を解決するためには境界ワイルドカード型を使う。
pushAll
メソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのサブタイプのリスト」とすべきであり、
これは境界ワイルドカード型を利用して、以下のように記述することができる。
public void pushAll(List<? extends E> src) { for (E e: src) { this.push(e); } }
popAll
メソッドの場合、引数の型は「Eのリスト」ではなく「Eの何らかのスーパータイプのリスト」とすべきであり、
これは境界ワイルドカード型を利用して、以下のように記述することができる。
public void popAll(List<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
上記のように記述することで、コンパイルエラーなく下記のコードを実行することが可能となる。
Stack<Number> numbers = new Stack<Number>(); List<Integer> integers = ...; numbers.pushAll(integers); List<Object> objects = new ArrayList<Object>(); numbers.popAll(objects);
PECS原則
境界ワイルドカード型のうち、extends
とsuper
のどちらを使えば良いか判断するための基本原則がPECS原則である。
Producer -extends and Consumer -super
pushAll
メソッドの場合、引数src
はスタックの要素を生成する Producer であった。
一方でpopAll
メソッドの場合、引数dst
はスタックの要素を消費する Consumer であった。
明示的型パラメータ
PECS原則を用いて、以下のunion
メソッドを書き換えることを考える。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; }
引数s1
とs2
を用いて、新たなSet
を生成するため、どちらも Producer である。
したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。
この時、戻り値にはワイルドカード型を使用しないことに注意する。戻り値にワイルドカード型を利用すると クライアント側でワイルドカード型の使用を強制することになる。
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; }
修正したunion
メソッドを以下のように使用することができるように思えるが、コンパイルエラーになってしまう。
Set<Integer> integers = new HashSet<>(); Set<Double> doubles = new HashSet<>(); Set<Number> numbers = union(integers, doubles);
上記エラーの原因は、Javaの型推論の制限によるものである。(ジェネリックの理論的には問題ないように思える) この時以下のように、明示的に型パラメータを記述することでエラーを解決することができる。
Set<Number> numbers = Union.<Number>union(integers, doubles);
上記Union
はunion
メソッドの宣言されたクラスである。
明示的な型パラメータの宣言はコードの冗長性を増すので頻繁に書くべきでないし、大抵は書く必要はない。
Comparableに対するPECS
PECS原則を用いて、以下のmax
メソッドを書き換えることを考える。
public static <T extends Comparable<T>> T max(List<T> list) { Iterator<T> it = list.iterator(); T result = it.next(); while (it.hasNext()) { T t = it.next(); if (t.compareTo(result) > 0) { result = t; } } return result; }
引数list
を用いてresult
を生成するため、list
は Producer である。
この時イテレータit
の型も修正することに注意する。
次に再帰型境界であるComparable
について考える。
<T extends Comparable<T>>
はTと比較可能な(Comparable
を実装した)何らかの型という意味だが、
Tインスタンスを「比較」という形で消費している Consumer と考えることができる。
したがってPECS原則に従うと、宣言を以下のように書き換えることが可能である。
public static <T extends Comparable<? super T>> T max(List<? extends T> list) { Iterator<? extends T> it = list.iterator(); ... (以下変更なし) ... }
コンパレータは Consumer であることを覚えておけば良い。
項目29. 型安全な異種コンテナーを検討する
異種コンテナーとは
ジェネリックスは通常、Set<Integer>
やList<String>
のように、特定の型の要素を持つコンテナーに対して利用する。Map<Integer, String>
のように複数のキーを持つ場合であっても、型は固定されている。
ここでいうコンテナーとは、List
やSet
のように複数の値やオブジェクトの格納先となる入れ物のこと。
本項目では、「クラス」をキー、「オブジェクト」を値とするマップとすることで、様々な型の値を 格納することができる異種コンテナーを実装する。
Classクラス
「クラス」をマップのキーとして扱うためにClass
クラスを利用する。
Class
クラスはJavaの「クラス」自体の情報を保持するためのクラスである。
利用例は以下のとおり。
String str = "Hello World!"; Class stringClazz = str.getClass(); // getName()メソッドでクラス名を取得 // この場合 java.lang.String stringClazz.getName();
クラスリテラル
Object.getClass()
メソッド以外にクラス名.class
(クラスリテラル)でもクラスの情報が取得できる。
Class stringClazz = String.class; // 同様にgetName()メソッドでクラス名を取得可 // この場合も java.lang.String stringClazz.getName();
Class
クラスはジェネリッククラスなので、型パラメータを与えることで型安全になる。
// OK Class<String> stringClazz = String.class; // コンパイルエラー Class<Integer> integerClazz = String.class;
ただしクラスリテラルはジェネリックスなど具象化不可能型では利用できない。
Class<List<String>> clazz; // リテラルを使うとコンパイルエラー Class<List<String>> clazz = List<String>.class;
型安全な異種コンテナー
Class
クラスを利用することで、「クラス」をキーとしたマップ(異種コンテナー)を
実装できる。インターフェースは以下。
public interface Favorites { public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); }
Favorites
コンテナーの利用例は以下のとおり。
Favorites favorites = new Favorites(); favorites.putFavorite(String.class, "Java"); favorites.putFavorite(Integer.class, new Integer(1)); String favorite = favorites.getFavorite(String.class);
上記のように、コンパイル/実行時の型情報を渡すために利用されるクラスリテラルを「型トークン」と呼ぶ。上記はジェネリックスにより型安全性が保証されているため、キーと値のクラスが異なることは起こり得ない。このように複数の型のオブジェクトを保持する型安全なコンテナーを「型安全異種コンテナー」と呼ぶ。
型安全異種コンテナーの実装は以下。
public class Favorites { private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>(); public <T> void putFavorite(Class<T> type, T instance) { if (type == null) { throw new NullPointerException("Type is null"); } favorites.put(type, instance); } public <T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type)); } }
上記のputFavorite
メソッドでは原型を利用することで、警告を無視することにはなるが
無理やりキーと型の異なる値を格納することが可能である。
そのような時はClass.cast
を用いると、実行時にClassCastException
を発生させることができる。
public <T> void putFavroite(Class<T> type, T instance) { // Class.castメソッドにより実行時に型チェックが可能 favorites.put(type, type.cast(instance)); }
境界型トークン
上記の異種コンテナーでは型トークンに関して特に制限はなかったが、境界型トークンを用いることで
制限が可能。下記の場合、型パラメータT
はAnnotation
クラスのサブクラスであると要求されている。
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Effective Javaを勉強します【第4章】
項目13. クラスとメンバーへのアクセス可能性を最小限にする
情報隠蔽のメリット
実装とAPIを分離し、他のモジュールとはAPIを介して通信を行い、詳細な実装は外部へ公開しないことを 情報隠蔽という。メリットとしては、以下のような点が挙げられる。
- 他モジュールとの依存性が低いため、モジュールの個別開発が可能であり開発スピードが向上する
- モジュールの理解がしやすく、他モジュールへの影響を心配せずデバッグが可能
- パフォーマンスに問題があるモジュールが特定できれば、他モジュールへの影響を心配せず最適化が可能
- ソフトウェアの再利用を促進する
- 大規模システムの開発において、個々のモジュールから作り込むことができる
情報隠蔽とアクセス修飾子
アクセス修飾子を用いて、各クラスやメンバーをできる限りアクセスできないようにすべき。
トップレベルのクラスやインタフェースの場合:
以下の2種の修飾子が利用可能。公開APIに含まれない(情報隠蔽可能)ので基本的にパッケージプライベートにすべき。パッケージプライベートのクラス/インタフェースを呼び出すクラスが1つならば、そのクラスにネストしてアクセス範囲を絞ることを検討すべきである。
- パッケージプライベート
- パッケージ内からのみアクセス可能
- public修飾子をつけなければパッケージプライベートになる
- public
- どこからでもアクセス可能
- 公開APIとなるため、互換性維持のために永久的にサポートする必要が生じる
メンバーの場合:
以下の4種の修飾子が利用可能。基本的にprivateにすべき。そうでないメンバーが多くなるのであれば設計を見直すべき。
- private
- 宣言されたクラス内でのみ利用可能
- 基本的にprivateにすべき
- パッケージプライベート
- 宣言されたパッケージ内のどのクラスからでもアクセス可能
- protected
- 宣言されたクラスのサブクラスからと、宣言されたパッケージ内のどのクラスからでもアクセス可能
- public
- どこからでもアクセス可能
メソッドがスーパークラスのメソッドをオーバーライドする場合、スーパークラスでメソッドが持つアクセスレベルより低いアクセス修飾子を指定することはできない。スーパークラスより低いアクセスレベルを設定すると、オーバーライドしたメソッドからスーパークラスのメソッドが呼び出せない状況が生じうるため。
インスタンスフィールドはpublic
にしない
インスタンスフィールドをfinal
で宣言した場合であっても、インスタンスフィールドへの参照のみを保持するので、インスタンスフィールド内で保存する値を制限することができないため。
同様に配列をフィールドとして持つ場合も、配列内の値ではなく配列への参照を保持するのみであるため、長さが0でない配列は常に可変であると考える。public static final
の配列フィールドやアクセッサーを持つのは、セキュリティホールのもとである。
項目14. publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う
public
のフィールドにはどこからでもアクセス可能となるため、カプセル化に反する。またフィールド変更時の補助的な処理も提供することができない。そのためフィールドは基本的にprivate
とし、public
のセッターとゲッターを提供するようにすればよい。
ただし、パッケージプライベートまたはprivate
のネストしたクラスであれば、public
のフィールドとしたほうが可読性などの観点から望ましい場合もある。
項目15. 可変性を最小限にする
不変オブジェクトのメリット
不変オブジェクトは以下に示すようなメリットを持つため、不必要にオブジェクトは可変とせずに不変オブジェクトの利用を検討すべきである。
不変オブジェクトは単純である:
オブジェクトが生成された状態から変化しないことが保証されるため、クライアントは状態を理解し信頼して利用することが可能である。開発者もクライアントの利用を考慮して何かする必要がない。
不変オブジェクトは本質的にスレッドセーフである:
複数のスレッドから並行してアクセスされても、状態が変化しないため同期を必要としない。すなわち制限なく共有できる。これを生かし、頻繁に利用される不変オブジェクトはpublic static final
の定数として提供したり、staticファクトリーメソッドで提供することが可能である。
また不変オブジェクトは、状態が変化しないため、コピーを行う必要がない。すなわちclone
メソッドやコピーコンストラクタを提供する必要はない。
項目16. 継承よりコンポジションを選ぶ
継承はコードを再利用するための強力な方法だが、不適切に使用されると継承は脆いソフトウェアを生み出す。以下は継承が安全に利用できる場合である。
- サブクラスとスーパークラスの実装が同じプログラマの管理下にあるようなパッケージ内で継承する場合
- 拡張のために設計されてかつ文書化されているクラスを拡張する場合
- クラスがインタフェースを実装したり、インタフェースが他のインタフェースを拡張した場合
一方、パッケージをまたがって普通の具象クラスから継承する場合は危険である。
サブクラスは適切に機能するために、スーパークラスの実装の詳細に依存する。スーパークラスの作成者が特に拡張される目的で設計および文書化を行なっていない限り、サブクラスはスーパークラスと一緒に変わっていかなければならない。
不適切な継承
不適切な継承の例として、HashSet
を使用するプログラムを以下に示す。このクラスには格納している要素の数をカウントする機能を追加している。
public class InstrumentedHashSet<E> extends HashSet<E> { private int addCount = 0; public InstrumentedHashSet() { } public InstrumentedHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } }
上記のコードは問題ないように見えるが、以下のようにaddAll
メソッドで3つの要素を格納した後、getAddCount
メソッドを呼び出すと6が返される。実際は要素数3なので、正しく動作していない。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>(); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); // 3が返ってくるのが正しいが、6が返される s.getAddCount();
これはaddAll
メソッドの内部でadd
メソッドが呼び出されているためである。add
メソッドとaddAll
メソッドの両方で要素数が二重にカウントされている。
この問題はaddAll
メソッドをオーバーライドしないことで解決することは可能である。しかし今後クラスを拡張する場合、HashSet
のaddAll
メソッドがadd
メソッドを使用しているという事実に依存することは変わらない。
また、継承したクラスで要素の追加時に何らかのバリデーションを実施するよう、オーバーライドをしたとする。この場合、実装時は問題が生じないかもしれないが、後のリリースでスーパークラスに要素を追加するための新しいメソッドが追加された場合、サブクラスでオーバーライドされていない新しいメソッドを利用することでバリデーションを回避することが可能である。
また、スーパークラスにサブクラスと同じシグネチャのメソッドが新たに追加された場合、戻り値の型が異なればサブクラスのコンパイルに失敗してしまうし、戻り値の型が同じであれば意図せずサブクラスでオーバーライドをすることで不適切な動作をするかもしれない。
要するに、パッケージを跨ったりドキュメントに記載がない意図されていない継承は、上記のような問題を引き起こしかねないため実施すべきでない。
継承における問題の回避方法
上記問題を全て回避するためには、コンポジションが有用である。コンポジションとは、既存のクラスを拡張する代わりに、既存のクラスのインスタンスを参照するprivate
のフィールドを、新たなクラスに持たせることである。
新たなクラスでは、保持している既存クラスのインスタンスに対してメソッドを呼び出し、その結果を返すようにする。これは転送と呼ばれ、そのメソッドは転送メソッドと呼ばれる。
下記はInstrumentedHashSet
をコンポジションを利用して書き換えた例である。コンポジションを利用すれば、既存のクラスを変更したとしてもそれを保持しているInstrumentedHashSet
には影響がない。
public class InstrumentedHashSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedHashSet(Set<E> s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } } // 再利用可能な転送クラス public class ForwardingSet<E> implements Set<E> { private final Set<E> set; public ForwardingSet(Set<E> set) { this.set = set; } @Override public int size() { return set.size(); } @Override public boolean isEmpty() { return set.isEmpty(); } @Override public boolean contains(Object o) { return set.contains(o); } @Override public Iterator<E> iterator() { return set.iterator(); } @Override public Object[] toArray() { return set.toArray(); } @Override public <T> T[] toArray(T[] a) { return set.toArray(a); } @Override public boolean add(E e) { return set.add(e); } @Override public boolean remove(Object o) { return set.remove(o); } @Override public boolean containsAll(Collection<?> c) { return set.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return set.addAll(c); } @Override public boolean retainAll(Collection<?> c) { return set.retainAll(c); } @Override public boolean removeAll(Collection<?> c) {return set.remove(c); } @Override public void clear() { set.clear(); } }
上記のクラスは頑強さに加えて柔軟性も持ち合わせている。
InstrumentedSet
クラスのコンストラクタには、以下の様にどのSet実装を渡しても問題がない。
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(x)); Set<E> s2 = new InsrumentedSet<E>(new HashSet<E>(capacity));
これはInstrumentedSet
インスタンスが既存のSet
インスタンスをラップしているため、InstrumentedSet
クラスはラッパークラスとも呼ばれる。またデコレータパターンとしても知られる。
項目17. 継承のために設計および文書化する、でなければ継承を禁止する
継承されるクラスは、そのための設計及び文書化がなされていなければならない。これが具体的に何を指すかについて説明する。
オーバーライド可能なメソッドの自己利用について正確に文書化しなければならない
各メソッド/コンストラクタが、どのオーバーライド可能なメソッドをどのような順番で呼び出し、それが後の処理にどのように影響するかを正確に文書化しなければならないということ。一般的に「良いAPIドキュメンテーションは、あるメソッドが何をするのかを記述し、どのように行うかは記述すべきでない」と言われているが、安全にサブクラス化するためには例外的に許される。
賢く選択されたprotected
メソッドの形で、クラス内部の動作へのフックを提供しなければならないかもしれない
フックとは、プログラム中の特定の箇所に、利用者が独自の処理を追加できる様にする仕組み。例えばjava.util.AbstractList
のremoveRange
メソッドは、List実装の最終的なユーザーが直接利用することはないが、開発者はremoveRange
メソッドをオーバーライドすることでclear
メソッドのパフォーマンスを向上させることが可能である。
(removeRange
メソッドはclear
メソッド内から呼び出され、デフォルト実装だと2次のオーダーである。)
どのようなフックを提供すべきかを簡単に知る手段はない。フックの1つ1つは実装の詳細に対する束縛となるため、必要最小限のフックを提供できるよう、開発者は実際にサブクラスを書いてテストを行い、十分に検討する必要がある。
コンストラクタはオーバーライド可能なメソッドを呼び出してはならない
スーパークラスのコンストラクタはサブクラスのコンストラクタより前に実行されるため、サブクラスでオーバーライドしているメソッドはサブクラスのコンストラクタより先に実行されることになる。そのため、オーバーライドしているメソッドがサブクラスのコンストラクタ内の処理に依存している場合、意図しない動作をすることになる。
これと同様の問題がCloneable
インタフェースとSerializable
インタフェースを実装した場合にも生じる。そのため継承のために設計しているクラスでは、Cloneable
インタフェースとSerializable
インタフェースは実装すべきでない。
安全にサブクラス化するための設計及び文書化がされていないクラスのサブクラス化は禁止する
サブクラス化を禁止するには以下の2つの方法がある。
- クラスを
final
と宣言する - すべてのコンストラクタを
private
かパッケージプライベートにして、代わりにpublic
のstaticファクトリ〜メソッドを提供する
項目18. 抽象クラスよりインタフェースを選ぶ
インタフェースは一般的に複数の実装を許す方を定義する最善の方法である。
抽象クラスとインタフェース
Javaでは複数の実装を許すクラスを定義するために、抽象クラスとインタフェースという2つの仕組みが提供されている。違いは以下。
- 抽象クラスはメソッドの実装を含むことが許されるがインタフェースは許されない
- 抽象クラスを実装するためには抽象クラスを継承する必要あり
- インタフェースはクラス階層のどこでも実装可能
Javaでは単一継承のみが許されているため、抽象クラスの利便性を低下させている。なぜなら既に既存の抽象クラスを継承している場合、追加で新たな抽象クラスを実装することはできないため。
一方で新たなインタフェースを実装するように既存のクラスを変更することは容易である。具体的にはクラス宣言にimplements
節を追加し、必要なメソッドを実装すればよい。
ミックスイン
インタフェースはミックスインを定義するには理想的。ミックスインとは、クラスが「本来の振る舞い」に加えて、追加で何らかの任意の振る舞いを提供するために実装するクラスのこと。例えばComparable
インタフェースは、本来の機能に対して「順序付け」という振る舞いを追加するミックスインである。
前述した通り、抽象クラスは既存のクラスへ組み込むことができないため、ミックスインの定義には利用できない。
インタフェースは、階層を持たない型フレームワークの構築を可能にする。例えば歌手を表しているインタフェースとソングライターを表しているインタフェースがあると仮定する。実世界では、歌手でありソングライターであるという人もいて、その間に包含関係(階層)はない。インタフェースを用いれば、 Singer
とSongWriter
の両方を拡張したSingerSongWriter
を以下の様に定義することが可能である。
public interface Singer { AudioClip sing(Song s); } public interface Songwriter { Song compose(boolean hit); } public interface SingerSongwriter extends Singer, Songwrite { AudioClip sing(Song s); Song compose(boolean hit); }
骨格実装
インタフェースはメソッドの実装を含むことはできないが、インタフェースに対応した骨格実装クラスを提供することで、実質的にデフォルト実装を提供することが可能。
以下では無名クラスとしてList
の骨格実装(AbstractList
)が提供されている。
static List<Integer> intArrayAsList(final int[] a) { if (a == null) { throw new NullPointerException(); } return new AbstractList<Integer>() { public Integer get(int i) { return a[i]; // 自動ボクシング } @Override public Integer set(int i, Integer val) { int oldVal = a[i]; a[i] = val; // 自動アンボクシング return oldVal; // 自動ボクシング } @Override public int size() { return a.length; } }; }
補足
Java8からインタフェースがデフォルト実装を持てるようになりました。これにより骨格実装をわざわざ別のクラスに分ける必要がなくなりました。(間違っていたら教えてください。)
項目19. 型を定義するためだけにインタフェースを使用する
インタフェースは型を定義するためだけに使用すべきである。定数を提供するために使用すべきではない。
定数インタフェース
以下はインタフェースの下手な使い方の一例。
// 定数インタフェース public interface PhysicalConstants { // アボガドロ定数(1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // ボルツマン定数(J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // 電子の質量(kg) static final double ELECTRON_MASS = 9.10938188e-31; }
このような用途にはenum
型を利用するのが正しい。
定数ユーティリティクラス
enum
以外の定数の提供方法として定数ユーティリティクラスがある。定数ユーティリティクラスはインスタンス化不可能である。
public class PhysicalConstants { // インスタンス化を防ぐ private PhysicalConstants() {} // アボガドロ定数(1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // ボルツマン定数(J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // 電子の質量(kg) static final double ELECTRON_MASS = 9.10938188e-31; }
項目20. タグ付クラスよりクラス階層を選ぶ
タグ付きクラスが適切な場合は殆どない。タグ付きクラスを書こうとした時はクラス階層で表現できないか考え直すこと。
タグ付きクラス
以下はタグ付きクラスの例。
public class Figure { enum Shape { RECTANGLE, CIRCLE } // タグフィールド - 図の形 final Shape shape; // shape が RECTANGLE の場合だけ、これらのフィールドは使用される double length; double width; // shape が CIRCLE の場合だけ、これらのフィールドは使用される double radius; // 円のためのコンストラクタ Figure(double radius) { shape = Shape.CIRCLE; this.radius = radius; } // 四角のためのコンストラクタ Figure(double length, double width) { shape = Shape.RECTANGLE; this.length = length; this.width = width; } double area() { switch (shape) { case RECTANGLE: return length * width; case CIRCLE: return Math.PI * (radius * radius); default: throw new AssertionError(); } } }
上記ではShape
がタグフィールドである。タグ付きクラスはenum
宣言やタグフィールド、switch
文で可読性が低下する。またタグによっては必ずしも使用されないフィールドがインスタンスに含まれるため、メモリ量も増加する。要するに、タグ付きクラスは冗長で、誤りやすく、非効率である。
タグ付きクラスは以下のようにクラス階層で表現可能。これは上述の欠点を全て克服している。さらに新たなクラスSquare
を追加することも容易であり、柔軟である。
// タグ付きクラスに対するクラス階層の置き換え abstract class Figure { abstract double area(); } public class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * (radius * radius); } } public class Rectangle extends Figure { final double length; final double width; Rectangle(double length, double width) { this.length = length; this.width = width; } @Override double area() { return length * width; } }
項目21. 戦略を表現するために関数オブジェクトを使用する
多くのプログラミング言語では、関数ポインタ、委譲、ラムダ式などの機構を利用して、関数を受け渡すことが可能である。
例えば、Cの標準ライブラリのqsort
関数は、要素をソートするために要素を比較するコンパレータ関数へのポインタを引数で受け取る。異なるコンパレータ関数を渡すことで様々なソート順を得ることができ、これは要素のソートに関する戦略を表していると言える。(戦略パターン)
関数オブジェクトとは
Javaでは、オブジェクト参照を使用することで関数を渡すことができる。以下のように他のオブジェクトに対して何らかの操作を行うためのメソッドを1つだけ公開しているインスタンスは関数オブジェクトと呼ばれる。
class StringLengthComparator { public int compare(String s1, String s2) { return s1.length() - s2.length(); } }
具象戦略クラス
上記のStringLengthComparator
クラスのインスタンスは、文字列の長さに基づいて比較を行い順序づける、文字列比較に対する具象戦略クラスである。
具象戦略クラスの特徴として、フィールド(状態)を持たないことがあげられる。したがって全ての具象戦略クラスのインスタンスは機能的に同じであるため、生成コストを節約するためシングルトンにするのが適切である。
class StringLengthComparator { private StringLengthCoparator() {} public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(String s1, String s2) { return s1.length() - s2.length(); } }
戦略インタフェース
StringLengthComparator
インスタンスをパラメータとして受け取る場合、そのパラメータの型をStringLengthComparator
とするのは不適切である。なぜならその他の具象戦略クラスのインスタンスを受け取ることができなくなるため。
このような場合は代わりにComparator
インタフェースを定義し、そのインタフェースをStringLengthComparator
が実装するようにする。この時Comparator
インタフェースを戦略インタフェースと呼ぶ。
public interface Comparator<T> { public int compare(T t1, T t2); }
無名クラスを用いた具象戦略
具象戦略クラスは、多くの場合以下のように無名クラスを使用して宣言される。しかしこの方法は、呼び出しごとに新しいインスタンスが生成されることに注意すること。もし複数回実行されるのであれば、private static final
のフィールドに保存して再利用することを検討すべき。
Array.sort(array, new Comparator<String>() { public int compare(String s1, String s2) { return s1.length() - s2.length(); } });
補足
Java8からラムダ式が導入されたので、無名クラスを使うよりも少ない記述量で関数オブジェクトを渡すことができるようになりましたね。
項目22. 非staticのメンバークラスよりstaticのメンバークラスを選ぶ
クラス内に定義されたクラスを「ネストしたクラス」と呼び、以下の4種類に分類される。
- static のメンバークラス
- 非 static のメンバークラス
- 無名クラス
- ローカルクラス
ネストしたクラスが、1つ以上のメソッドの外から見える必要がある、またはメソッド内に入れるには長すぎる場合はメンバークラスにする。メンバークラスの個々のインスタンスが、エンクロージングインスタンスへの参照が必要ならは非 static、そうでなければ static にする。
クラスがメソッド内に属しているべきであり、1箇所でのみ使用され、そのクラスを特徴付ける型がすでに存在している場合は無名クラスにする。そうでない場合はローカルクラスにする。
static のメンバークラス
エンクロージングクラスの static のメンバーであり、他の static のメンバーと同じアクセス可能性規則に従う。もし private である場合、エンクロージングクラス内からのみアクセス可能。
static のメンバークラスは、エンクロージングクラスのメンバーのすべて(private のメンバー含む)にアクセスできる。
エンクロージングクラスと一緒に使用すると有用な public のヘルパークラスとして利用されることが多い。
class Calcularor { static public enum Operation { PLUS() {...} MINUS() {...} } }
非 static のメンバークラス
static のメンバークラスとの文法的な違いは、宣言にアクセス修飾子 static があるかないかのみ。
非 static メンバークラスの個々のインスタンスは、エンクロージングクラスと暗黙に関連付けされる。具体的には以下。
- 非 static のメンバークラスのインスタンスメソッドないからエンクロージングインスタンスのメソッドの呼び出し可能
- エンクロージングインスタンスなしで、非 static のメンバークラスのインスタンスの生成不可
一般的な利用方法の1つは、以下のようなアダプターの定義手段として。
public class MySet<E> extends AbstractSet<E> { ...//大部分省略 public Iterator<E> iterator () { return new MyIterator(); } private class MyIterator implements Iterator<E>{ ... } }
エンクロージングインスタンスへアクセスする必要がないメンバークラスを宣言するのであれば、static 修飾子を常につけてstatic のメンバークラスとすべき。
private static のメンバークラスは、エンクロージングクラスが表すオブジェクトの構成要素を表現する時に一般的に利用される。
無名クラス
クラス名を持たず、エンクロージングクラスのメンバーではない。無名クラスは宣言と同時にインスタンス化される。式が許されている場所であれば、どこでも宣言可能。
ただし制限が多い。例えば、無名クラスが宣言された箇所以外で無名クラスをインスタンス化することはできない。また無名クラスのクライアントは、無名クラスがスーパータイプから継承しているメソッド以外を呼び出すことができない。
一般的な利用方法は、関数オブジェクトの生成手段として。(Java8 からはラムダ式の使用が可能となったので、利用シーンは減ったと思われる)
ローカルクラス
4種のネストしたクラスの中で、最も使用頻度が低いと思われる。ローカル変数が宣言できる場所であればどこでも宣言でき、ローカル変数と同じスコープ規則に従う。
Google Apps Script(GAS)で自作アプリを公開する
GASで簡単なWebアプリケーションを作って公開してみます!
作るもの
酒好きの友人と幹事当番制で月1で飲み会を開催していて、開催も20回を超えていろいろ美味しいお店も見つかったので、その飲み会の履歴を記録するページを作ってみようと思います。以下の様な機能を実装したい。
- 飲み会履歴一覧機能
- 飲み会履歴登録機能
- 飲み会履歴編集機能
1つの飲み会履歴は以下のような属性を持つ想定。
- 開催年
- 開催月
- 開催場所
- 店名
- URL
- 参加者
検討事項
今回アプリを作成するに当たって検討が必要だった点は以下。
DBをどうするか
最初はスプレッドシートをDBに採用代わりにしようと思っていました。もともと飲み会の記録はスプレッドシートにメモ程度にとっていたので。
けど仮実装してみると、やはり機能が貧弱すぎる・・・。セルの範囲を処理のたびに指定するのがだるいし、DBみたいにSQLでソートや検索もできないし。
とはいえ、もともと手軽かつ無料でウェブアプリケーションが作れるというところに魅力を感じてGASを触ってみたので、Google Cloud SQLみたいな有料のサービスは使いたくない・・・。
どうしようと思っていたところで、Fusion Tablesというナイスなサービスを見つけました。
https://support.google.com/fusiontables/answer/2571232?hl=en
2017年7月現在試験運用ということですが、今回のアプリのユーザーは限られていますし、何より無料という点が非常に魅力的なので採用することを決定。
UIをどうするか
第三者に公開するということで、UIもある程度綺麗に作りたいところです。
調べてみたところCDNを介してBootStrapの利用が可能なようなので、BootStrapベースで作ることに決めました。
あとサーバー・UI間のデータのやりとりですが、以下の記事のサンプルを参考にしてMVVMライブラリであるknockout.jsを採用することにしました。
http://qiita.com/shibukawa/items/8fee715232e12f183698
実装
完成したものが以下になります(Fusion TablesはDEMO用に分けてあります)。 スマホから見るのを想定していて、PC用のCSSを作り込んでいないのでブラウザで見るとすごく拡大されると思います・・・。 デベロッパーツールとかでスマホ用の画面サイズにしてください・・・。
https://script.google.com/macros/s/AKfycbwBvUNboSuHQwFZ0PDuAUspfJZ5FkClStrMaZ5qIFx7hOjQPdq4/exec
Fusion Tables の作成・有効化
まずデータを格納するFusion Tablesを作ります。
oogleドライブにアクセスして 新規 > その他 > アプリを追加 を選択し、「fusion tables」で検索します。Google Apps Scriptが出てくるので追加し、ファイルを新規作成します。
そうするとNew Tableという新しいテーブルが表示されると思いますので、名前を適当に変えてカラムを追加しましょう。 メニューの Edit > Change columnsから列名の編集・追加が可能です。
Save ボタンを押して上記の操作を確定します。今回は、Year、Month、Location、Name、Member、Urlの6つのカラムを追加しました。
最後に、プロジェクトでFusion Tables APIを有効化する必要があります。 Google Apps Scriptのメニューのリソース > Googleの拡張サービスから Fusion Tables API を見つけ有効化します。
またGoogle API コンソールでも有効化する必要があります。ダイアログ下部のリンクからコンソールを開き、画面上部のAPIを有効にするをクリック、Fusion Table APIを検索して選択し、有効にするを選んでください。
これでFusion Tablesが利用可能になります。
サーバー側処理の実装
サーバー側処理(main.gs)の処理は以下になります。一番上の変数TABLE_IDには、作成したFusion TablesのURL末尾のdocidを指定してください。
例) https ://fusiontables.google.com/hogehoge?docid={ここの文字列}#rows:id=1
Fusion Tables操作のクエリは公式のドキュメントを見てもらえればわかりやすいと思います。 今回はknockout.jsで操作しやすいように、SQLのレスポンスを加工しています。
var TABLE_ID = ""; function doGet(e) { var template = HtmlService.createTemplateFromFile("Index.html"); template.title = "飲み会 archive"; template.data = getAllStores(); return template.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME); } /** レコードの全取得 **/ function getAllStores() { var sql = "SELECT ROWID, Year, Month, Location, Name, Member, Url FROM " + TABLE_ID + " ORDER BY Year DESC, Month DESC", res = FusionTables.Query.sql(sql); return JSON.stringify(toHashArray(res)); } /** レコードの追加 **/ function addStore(year, month, location, name, member, url) { var sql = "INSERT INTO " + TABLE_ID + " (Year, Month, Location, Name, Member, Url) VALUES ('" + year + "','" + month + "','" + location + "','" + name + "','" + member + "','" + url + "')"; FusionTables.Query.sql(sql); } /** レコードの更新 **/ function updateStore(year, month, location, name, member, url, index) { var sql = "UPDATE " + TABLE_ID + " SET Year = '" + year + "', Month = '" + month + "', Location = '" + location + "', Name = '" + name + "', Member = '" + member + "', Url = '" + url + "' WHERE ROWID = '" + index + "'"; FusionTables.Query.sql(sql); } /** knockoutJSでバインドできるようSQLの返り値をhash形式に変換する **/ function toHashArray(res) { var hashArray = [], rows = res.rows, columns = res.columns, hash; for (var i in rows) { hash = {}; for (var j in columns) { hash[columns[j]] = rows[i][j]; } hashArray.push(hash); } return hashArray; }
フロント側処理の実装
HTMLとJavaScript、CSSは以下になります。
bootstrapはCDNで読み込んでいて、ボタンデザインなど一部を上書きしています。
工夫したのは飲み会履歴の追加と更新の処理です。
どちらも共通のモーダルを用いていますが、追加処理の場合は openInsertModal
関数、
更新処理の場合は openUpdateModal
関数をモーダル起動と同時に呼び出し、モーダルのタイトルやボタンラベル、入力フィールドの値を更新しています。またhiddenフィールドに、処理の内容に応じて “add” または “update” という値を設定しています。
サーバーにpostする時は postStore
関数を呼び出します。上記で設定したhiddenフィールドの値を読み、更新時はサーバーの updateStore
関数、新規追加時は addStore
関数を呼び出します。これらの関数呼び出し時に、self.stores
を一度空にしていますが、これは飲み会履歴の一覧を非表示にしユーザーの操作を防ぐためです。self.stores
を空にするだけでUIが非表示となるのは、knockout.jsで監視をしているからです(self.stores = ko.observableArray(JSON.parse(data));
の部分、詳細はknockout.jsのドキュメントを参照してください)。
また、サーバー処理中は「処理中・・・」のメッセージを表示する様にしています。これは通常時は見えない(display: none)メッセージ用のdivのcssを更新し、”display: block”にすることで実現しています。
サーバー処理が成功すれば、受け取ったレスポンスを再度self.stores
に設定します。するとknockout.jsにより自動でUIが更新され、ユーザーには処理後のデータが表示されることになります。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title><?= title ?></title> <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?> </head> <body> <nav class="navbar navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header"> <div class="navbar-brand"><?= title ?></div> </div> </div> </nav> <div class="container-fluid mainpanel"> <div class="row"> <div class="col-xs-12"> <div class="tool"> <button type="button" class="btn btn-success btn-lg btn-circle" data-toggle="modal" data-target="#storeModal" data-bind="click: openInsertModal"> <i class="fa fa-plus fa-2x" aria-hidden="true"></i> </button> </div> <div class="panel panel-default" id="load-message"> <div class="panel-body"></div> </div> <div data-bind="foreach: stores"> <div class="panel panel-default"> <div class="panel-body"> <div class="edit-btn-div"> <button type="button" class="btn btn-primary btn-lg btn-circle" data-toggle="modal" data-target="#storeModal" data-bind="click: $parent.openUpdateModal"> <i class="fa fa-pencil fa-2x" aria-hidden="true"></i> </button> </div> <div> <div><span data-bind="text: Year"></span> 年 <span data-bind="text: Month"></span> 月</div> <div>開催場所: <span data-bind="text: Location"></span></div> <div>店名: <span data-bind="text: Name"></span></div> </div> <div>url: <a data-bind="attr: {href: Url}" target="_blank"><span data-bind="text: Url"></span></a></div> <div>参加者: <span data-bind="text: Member"></span></div> </div> </div> </div> </div> </div> </div> <div class="modal fade" id="storeModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title"></h4> </div> <div class="modal-body"> <div><span class="require">*</span>: 入力必須</div><br> <form> <div class="form-group"> <input type="hidden" id="mode"> <input type="hidden" id="index"> <label for="year-input">年 <span class="require">*</span></label> <select class="form-control" id="year-select"> <option>2015</option> <option>2016</option> <option selected="selected">2017</option> <option>2018</option> <option>2019</option> <option>2020</option> </select> </div> <div class="form-group"> <label for="month-input">月 <span class="require">*</span></label> <select class="form-control" id="month-select"> <option selected="selected">1</option> <option>2</option> <option>3</option> <option>4</option> <option>5</option> <option>6</option> <option>7</option> <option>8</option> <option>9</option> <option>10</option> <option>11</option> <option>12</option> </select> </div> <div class="form-group"> <label for="location-input">開催場所</label> <input class="form-control" id="location-input"/> </div> <div class="form-group"> <label for="name-input">店名 <span class="require">*</span></label> <input class="form-control" id="name-input"/> </div> <div class="form-group"> <label for="url-input">url</label> <input class="form-control" id="url-input"/> </div> <div class="form-group"> <label for="member-input">参加者</label> <input class="form-control" id="member-input"/> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary btn-lg modal-btn" data-bind="click: postStore"></button> <button type="button" class="btn btn-default btn-lg" data-dismiss="modal">キャンセル</button> </div> </form> </div> </div> </div> </div> <script> var data = <?= data ?>; </script> <?!= HtmlService.createHtmlOutputFromFile('JavaScript').getContent(); ?> </body> </html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="https://ajax.aspnetcdn.com/ajax/knockout/knockout-3.1.0.js"></script> <script type="text/javascript"> function storeVM() { var self = this; self.stores = ko.observableArray(JSON.parse(data)); self.openUpdateModal = function(store) { $(".modal-title").text("お店の更新"); $(".modal-btn").text("更新"); $("#mode").val("update"); $("#index").val(store.rowid); $("#year-select").val(store.Year); $("#month-select").val(store.Month); $("#location-input").val(store.Location); $("#name-input").val(store.Name); $("#member-input").val(store.Member); $("#url-input").val(store.Url); }; self.openInsertModal = function() { $(".modal-title").text("お店の追加"); $(".modal-btn").text("追加"); $("#mode").val("add"); $("#index").val(""); $("#year-select").val(""); $("#month-select").val(""); $("#location-input").val(""); $("#name-input").val(""); $("#member-input").val(""); $("#url-input").val(""); }; self.postStore = function() { var mode = $("#mode").val(), index = $("#index").val(), year = $("#year-select").val(), month = $("#month-select").val(), location = $("#location-input").val(), name = $("#name-input").val(), member = $("#member-input").val(), url = $("#url-input").val(); if (!year || !month || !name) { return false; } self.stores(""); self.modalClose(); $("#load-message").css("display", "block"); $("#load-message .panel-body").text("処理中・・・"); if (mode === "add") { google.script.run .withSuccessHandler(self.onPostSuccess) .addStore(year, month, location, name, member, url); } if (mode === "update") { google.script.run .withSuccessHandler(self.onPostSuccess) .updateStore(year, month, location, name, member, url, index); } }; self.onPostSuccess = function() { google.script.run.withSuccessHandler(function(data) { self.stores(JSON.parse(data)); $("#load-message").css("display", "none"); }).getAllStores(); } self.onPostFailure = function() { $("#load-message .panel-body").text("処理に失敗しました。ページを再読み込みしてください。"); } self.loadingView = function(flag) { $('#loading-view').remove(); if(!flag) return; $('<div id="loading-view" />').appendTo('body'); } self.modalClose = function() { $('body').removeClass('modal-open'); $('.modal-backdrop').remove(); $('#storeModal').modal('hide'); } } $(document).ready(function () { ko.applyBindings(new storeVM()); }); </script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"> <script src="https://use.fontawesome.com/a8b1ccabdb.js"></script> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> <style> body,.navbar-brand, .modal-title, .form-control, .modal-footer .btn { font-size: xx-large; } body { padding-top: 50px; } .navbar-brand { font-weight: bold; color: #FFFFFF; } .navbar { padding-top: 30px; padding-bottom: 30px; padding-left: 5px; background-color: #000000; } .mainpanel { padding: 80px 15px; } .edit-btn-div { float: right; } .tool { padding-bottom: 10px; padding-right: 15px; text-align: right; } .year, .month, .location, .name, .member, th { text-align: center; vertical-align: middle !important; } .fa { margin: 10px; } .btn-circle { width: 84px; height: 84px; border-radius: 50px; } .btn-column { width: 46px; } .form-control { height: 60px; } .modal-dialog { width: 90%; } .modal-body { padding: 20px; } .modal-body-footer { text-align: right; } #load-message { display: none; } .require { color: red; }</style>
感想
手軽に無料でWebサービスが作れるのはすごく魅力的です。がしかし、作り込もうとすればするほど物足りない点は出てきます。具体的には以下。
- 基本画面遷移ができないので、アプリケーションで扱うモデル(今回はStoreのみ)が増えるときつい
- 利用者数が多く、DBへのアクセスが多いのであれば、諦めてGoogle Cloud SQLなどを使うべき
- CDN経由でないと他のライブラリが利用できない。最近流行りのフレームワークとか使おうと思っても使えない。
- エディタが使いづらい(全角半角の見分けがつかないとかいろいろ)
要するに本格的にやりたくなったら諦めてお金を出すしかないと思います。 今回やってていろいろ機能追加したいなーと思ったので、そのうちS3, lambda, api gatewayとかのサーバーレス構成でAWSに乗せかえるかも。意外とお金かからないみたいだし。
否定的なことばかり書きましたが、ユーザー数が限られてて、すでにスプレッドシートとかで管理しているコンテンツは手軽にWebに公開できて便利だと思う。内輪のイベントのタイムテーブルの共有とか?せっかく勉強したので何か良い活用方法を探してゆきたい。
Google Apps Script(GAS)を試す
無料で簡単にWebアプリが公開できるGoogle Apps Scriptなるものがあると聞いて試してみました。
Google Apps Script を追加する
まずはGoogleドライブにアクセスします。
新規 > その他 > アプリを追加 を選択し、「script」で検索します。Google Apps Scriptが出てくるので追加し、ファイルを新規作成します。
GASプロジェクトを作成する
新規作成すると、無題のプロジェクトに「コード.gs」が表示されているはずです。 が、メニューのヘルプ > 開始画面 と選択し、左ペイン下部のウェブアプリケーションを選択します。
すると、以下の4ファイルがすでに生成されています。
- コード.gs
- Index.html
- JavaScript.html
- Styleshiit.html
コード.gs がサーバー側処理、それ以外が View になります。 JavaScriptとCSSがhtmlファイルになっているのが気になるかもしれませんが、 GASでは現状このような形でしかJavaScriptとCSSを分離することができません・・・。
プロジェクト名を適当に変えてください。とりあえず「Sample Project」としてみます。
Webアプリケーションを公開する
メニューから 公開 > ウェブアプリケーションとして導入 を選択します。
以下を選択して「導入」ボタンを押しましょう。 承認が必要です、というダイアログが出たら確認して承認してください。
- プロジェクトバージョン: 新規作成
- 次のユーザーとしてアプリケーションを実行: 自分
- アプリケーションにアクセスできるユーザー: 全員(匿名ユーザーを含む)
なんとこれだけでWebアプリケーションが公開できます! 「現在のウェブアプリケーションのURL」が公開したWebアプリケーションのエンドポイントになります。
ちなみにURLをそのままブラウザにコピー&ペーストすると、自分のGoogleドライブのルート階層のファイル一覧が表示されるはずです。
URLパラメータにfolderIdを指定すると、Googleドライブにある特定のフォルダ直下のファイル一覧が見れるようになります。 folderIdというのは、Googleドライブ内のフォルダにアクセスした時のURL末尾についているランダムな文字列のことです。
例) https ://drive.google.com/drive/folders/XXXXXXX ←このXXXXXXXの部分です。
URLパラメータの指定方法ですが、アプリケーションのURLが「https ://example/hogehoge/」だとすると、以下のように末尾に指定します。
例) https ://example/hogehoge/?folderId=XXXXXXX
簡単にコードの解説
コード.gs 内の doGet 関数がアプリケーションのエンドポイントです。
HtmlService.createTemplateFromFile('Index')
で、Index.html からViewを生成して返していますね。
そのあとのif文でリクエストのパラメータをチェックして、folderIdがなければ”root”に設定しています。
function doGet(e) { var template = HtmlService.createTemplateFromFile('Index'); // Retrieve and process any URL parameters, as necessary. if (e.parameter.folderId) { template.folderId = e.parameter.folderId; } else { template.folderId = 'root'; } // Build and return HTML in IFRAME sandbox mode. return template.evaluate() .setTitle('Web App Window Title') .setSandboxMode(HtmlService.SandboxMode.IFRAME); }
getFolderContents 関数でGoogleドライブにアクセスしています。
DriveApp.getRootFolder()
とDriveApp.getFolderById(folderId)
で、フォルダの情報をごそっと取得しています。
で、そこからフォルダ名やら子要素を取り出しているわけです。
function getFolderContents(folderId) { var topFolder; var contents = { children: [] }; if (folderId == 'root') { topFolder = DriveApp.getRootFolder(); } else { // May throw exception if the folderId is invalid or app // doesn't have permission to access. topFolder = DriveApp.getFolderById(folderId); } contents.rootName = topFolder.getName() + '/'; var files = topFolder.getFiles(); var numFiles = 0; while (files.hasNext() && numFiles < 20) { var file = files.next(); contents.children.push(file.getName()); numFiles++; } return contents; }
この getFolderContents 関数を呼び出しているのが、JavaScript.html 内の無名関数です。
リクエスト失敗時と成功時のコールバックが含まれているのでやや複雑になっていますが、
google.script.run.getFolderContents(folderId)
だけでも呼び出すことが可能です。
リクエスト成功時は、updateDisplay 関数でレスポンスから必要な情報を取り出してDOMに挿入しています。
$(function() { // Call the server here to retrieve any information needed to build the page. google.script.run .withSuccessHandler(function(contents) { // Respond to success conditions here. updateDisplay(contents); }) .withFailureHandler(function(msg) { // Respond to failure conditions here. $('#main-heading').text(msg); $('#main-heading').addClass("error"); $('#error-message').show(); }) .getFolderContents(folderId); }); ... function updateDisplay(contents) { var headingText = "Displaying contents for " + contents.rootName + " folder:"; $('#main-heading').text(headingText); for (var i = 0; i < contents.children.length; i++) { var name = contents.children[i]; $('#results').append('<div>' + name + '</div>'); } }
まとめ
ざっくりとした説明ですが、とりあえずここまで。いろいろと制限はあると思いますが、無料でこれだけ簡単に個人でWebアプリを公開できるのはすごいことですよね。
HTMLによる View の生成、JavaScriptによるサーバー処理の呼び出しとUIの更新、Googleドライブとの連携と開発に必要なエッセンスが詰まっていると思います。
あとは公式のドキュメントを見ながら修正していくだけで、それなりのものが作れるでしょう。
公式ドキュメント: https://developers.google.com/apps-script/overview
ちなみに、メニューの 公開 > ウェブアプリケーションとして導入 からテスト用のURLも取得できます。 修正をした後はテスト用のURLで確認が可能ですし、もちろん第三者に公開しているURLには修正内容は見えません。
修正が済んで新しいバージョンとして公開したいときは、プロジェクトバージョンから新規作成を選んで「更新」ボタンを押してください。公開用URLに修正が反映されます。
次はこれを元にオリジナルアプリを作る予定。
Effective Javaを勉強します【第3章】
Objectクラスのequals、hashCode、toString、clone、finalizeはすべて オーバーライドされるように設計されているため、明示的な一般契約がある。
これに従わないと、この契約に依存しているHashMapやHashSetなど他のクラスが 適切に機能しない恐れがあるため、いつ、どのようにオーバーライドすべきか学びましょうと いう話。 (ComparableはObjectではないが同様の特徴をもつため、本章で扱う)
項目8. equalsをオーバーライドする時は一般契約に従う
equals()
をオーバーライドすべきでない場合
以下の条件のいずれかに当てはまる場合は、equals()
をオーバーライドしない方が適切。
- クラスの個々のインスタンスは本質的に一意である
- Threadなどのように、値よりは能動的な実態を表しているクラスの場合
- Objectクラスの提供する
equals()
の振る舞いが正しい- Objectの提供する
equals()
の実装は以下になってる。参照が同じである=等しいと判断される。
- Objectの提供する
public boolean equals(Object obj) { return (this == obj); }
「論理的等価性」検査をクラスが提供するかどうかに関心がない
- 例えばRandomインスタンスがこれから同じ乱数列を生成するか検査をする必要はない
- この場合もObjectの提供する
equals()
の振る舞いが正しい
スーパークラスがすでにequalsをオーバーライドしており、スーパークラスの振る舞いがこのクラスに対して適切である
- 例えばSetならAbstractSet、ListならAbstractList、MapならAbstractMap
クラスがprivateあるいはパッケージプライベートであり、そのequalsメソッドが決して呼び出されないことが確かである
- 万が一呼び出された場合に備えて、以下のように例外をスローするようオーバーライドする
@Override public boolean equals (Object obj) { throw new AssertionError(); }
equals()
をオーバーライドすべき場合
クラスが論理的等価性という概念を持っていて、スーパークラスがequals()
をオーバーライドしていないとき。
- 論理的等価性=論理的に同値であるのかを知りたい
- オブジェクトの同一性(参照先が同じ)を確かめたいのではなく、持つ値が正しいのかを知りたい
equalsをオーバーライドする時は、以下の一般契約を厳守しなければならない
反射性
nullでない任意の参照値xに対して、x.equals(x)
はtrueを返さなければならない。
- オブジェクトがそれ自身と等しくなければならないということ
- これを意図的に破ることは難しい
対称性
nullでない任意の参照値xとyに対して、y.equals(x)
がtrueを返す場合のみ、x.equals(y)
はtrueを返さなければならない。
以下の例のように簡単に破ることが可能なので注意。
public class CaseInsensitiveString { private final String s; public CaseInsensitiveString(String s) { if (s == null) { throw new NullPointerException(); } this.s = s; } // 間違った例 @Override public boolean equals(Object o) { if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) o).s); if (o instanceof String) // 引数がStringの場合、大文字小文字を無視して比較しているが // Stringクラスでは無視しないため対称性が成り立たない return s.equalsIgnoreCase((String) o); return false; } // 正しい例 @Override public boolean equals(Object o) { // クラスが異なる場合はfalseを返すのが正解(Stringは比較しない) return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); } }
推移性
nullでない任意の参照値x、y、zに対して、x.equals(y)
とy.equals(z)
がtrueを返すなら、x.equals(z)
はtrueを返さなければならない。
- インスタンス化可能なあるクラスを拡張して、equals契約を守ったまま値要素を追加したサブクラスを実装する方法はないので注意
- あるクラスに値要素を追加したい時は、継承ではなくコンポジションを用いるべきである(項目16)
例) 2次元座標を保持したPointクラスと、Pointクラスを継承して色情報を付加したColorPointというサブクラスを考える。
// スーパークラス public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (!(o instanceof Point)) { return false; } Point p = (Point) o; return p.x == this.x && p.y == this.y; } } // サブクラス public class ColorPoint extends Point { private Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } }
このときColorPointクラスでequalsをオーバーライドしなければ、ColorPointのequals比較では色情報が無視されてしまうため、 色情報も一致したときにtrueを返すよう以下のように実装することを考える。
// ColorPointの誤ったequals()の例1 // 対称性が守られていない @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) { return false; } return super.equals(o) && ((ColorPoint) o).color == color; }
これは以下のように比較を行った場合、対称性が守られないため不適切。
Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED); p.equals(cp); // => true(色情報が無視される) cp.equals(p); // => false
cp.equals(p)
がtrueを返すよう、以下のようにColorPointクラスとPointクラスの比較の際は色情報を無視するよう実装すると、推移性が守られない。
// ColorPointの誤ったequals()の例1 // 推移性が守られていない @Override public boolean equals(Object o) { if (!(o instanceof Point)) { return false; } // oがPointクラスの場合は、Pointクラスのequals()で色情報を無視した比較を行う if (!(o instanceof ColorPoint)) { return o.equals(this); } return super.equals(o) && ((ColorPoint) o).color == color; }
これは以下のように比較を行った場合、推移性が守られないため不適切。
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE); p1.equals(p2); // => true p2.equals(p3); // => true p1.equals(p3); // => false
以下のようにコンポジションを用いることで、equals契約を守ったままPointクラスに色情報を付加できる。
(ColorPointクラスはPointのサブクラスではないためcp.equals(p)
もp.equals(cp)
もfalseになる)
public class ColorPoint { private Point point; private Color color; public ColorPoint(int x, int y, Color color) { if (color == null) { throw new NullPointerException(); } point = new Point(x, y); this.color = color; } // ビューを返す public Point asPoint() { return point; } @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) { return false; } ColorPoint cp = (ColorPoint) o; return cp.point.equals(point) && cp.color.equals(color); } }
ただし抽象クラスのサブクラスには、equals契約を守ったまま値要素の追加が可能。 (スーパークラスはインスタンス化されないため、サブクラスと比較されて推移性に違反することがないため)
整合性
nullでない任意の参照値xとyに対して、オブジェクトに対するequals比較に使用される情報が変更されなければ、
x.equals(y)
は常に同じ値を返し続けなければならない。
- 信頼できない資源に依存するequalsメソッドを書いてはならない
- 例)
java.net.URL
のequalsメソッドは、比較対象URLに関連づけられたホストのIPに依存している- ホストをIPに変換する時ネットワークへのアクセスが必要であり、常に同じ結果が保証されないため不適切
- メモリ中のオブジェクトのみに依存する処理を行うべき
- 例)
非null性
nullでない任意の参照値xに対して、x.equals(null)
はfalseを返さなければならない。
- nullが渡されたときにNullPointerExceptionがスローされるのは契約違反なので注意(falseを返すようにする)
高品質なequalsメソッドの書き方
- 引数が自分自身のオブェクトであるかどうかを検査する場合は == を利用する
- 引数が正しい型であるかを調べるためには instanceof を使う。
- 正しい型とは、普通はメソッドが定義されているクラスである。場合によっては、このクラスによって実装されているインターフェース。
- 引数を正しい型にキャストする
- そのインスタンスのフィールドと、与えられたオブジェクトのフィールドを検査する
- 基本データ型には
==
を、オブジェクトには再帰的にequals
を用いる - ただし、floatとdoubleには
Float.compare
とDouble.compare
を利用する。 - 配列のフィールドは
Array.equals
が利用できる。
- 基本データ型には
- 「対称性」、「推移性」、「整合性」の性質を満たしたかどうかのテストを書く
- 「反射性」と「非 null 性」は心配しなくても大抵の場合満たされる。
項目9. equalsをオーバーライドする時は、常にhashCodeをオーバライドする
equals
をオーバーライドしているすべてのクラスで、hashCode
をオーバーライドしなければならない。
- 上記を守っていないクラスがHashmap、HashSet、Hashtableを含む、すべてのハッシュに基づくコレクションで用いられると適切に機能しない
守らなければならない契約は以下。
equals
比較で使用されるオブジェクトの情報が変更されない限り、hashCode
メソッドは同じオブジェクトに対して常に同じ整数を返さなければならない- 2つのオブジェクトが
equals
比較により等しければ、2つのオブジェクトの個々に体するhashCode
メソッド呼び出しは、同じ整数を返さなければならない - 2つのオブジェクトが
equals
比較により等しくない場合、2つのオブジェクトの個々に体するhashCode
メソッド呼び出しが別の整数を返さなければならない、ということは要求されない。しかし別の整数を返す場合、ハッシュテーブルのパフォーマンスが改善される可能性がある。
hashCode
をオーバーライドするのを忘れた場合、上記2が破られる。
例) hashCode
をオーバーライドしていないPhoneNumberClass
public class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "areaCode"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } private static void rangeCheck(int arg, int max, String name) { if (arg < 0 || max < arg) { throw new IllegalArgumentException(name + ": " + arg); } } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber phoneNumber = (PhoneNumber) o; return phoneNumber.lineNumber == this.lineNumber && phoneNumber.prefix == prefix && phoneNumber.areaCode == areaCode; } // hashCode がない! }
put
とget
で利用される2つのPhoneNumberクラスのハッシュコードが一致しないため、下記のようにHashMapが正しく動作しない。
Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); m.get(new PhoneNumber(707, 867, 5309)); // => null
以下の手順に従うことで、理想的なハッシュ関数を計算することが可能。
- 何らかのゼロでない定数、例えば 17 を
result
という int 変数に保存する。 - オブジェクト内の
equals
メソッドで考慮されるフィールドf
に対して以下のことを行う。- 各フィールドに対する intのハッシュコード
c
を計算する。- boolean ならば、
(f ? 0: 1)
を計算する。 - byte, char, short, int ならば
(int) f
を計算する。 - long ならば、
(int)(field ^ (f >>> 32))
を計算する。 - float ならば、
Float.floatToIntBits(f)
を計算する。 - double ならば
Double.doublToLongBits(f)
を計算し、結果の long を6と同様にハッシュする。 - オブジェクト参照で、
equals
内でフィールドをequals
の再帰的呼び出しによって比較しているならば、再帰的にhashCode
を呼び出す。フィールドがnull
なら0を返す。 - 配列ならば各要素を別々のフィールドとして取り扱い、各意味のある要素に対して再帰的にこれらの規則を適用してハッシュを計算する。要素全てに意味がある場合、
Arrays.hashCode
メソッドの利用が可能。
- boolean ならば、
- 上記で計算した
c
をresult = 31 * result + c;
の式に代入する。
- 各フィールドに対する intのハッシュコード
result
を返す。- 等しいインスタンスが等しいハッシュコードを持つかどうかを自問し、単体テストを書く。
もしクラスが不変でハッシュコードを計算するコストが高いならば、オブジェクト内にハッシュコードをキャッシュしておくことを検討すべきである。
もしそのクラスのオブジェクトのほとんどがハッシュキーとして利用される場合、以下のいずれかの対策をすべきである。
- インスタンス生成時にハッシュコードを計算する
- 最初に
hashCode
が呼び出された時に、遅延初期化をする(項目71)
項目10. toStringを常にオーバーライドする
toString
メソッドのデフォルト実装は、"クラス名@ハッシュコードの符号なし16進数表現"の文字列を返す。
→ 分かりづらいので、オーバーライドしてオブジェクトに含まれる有益な(ユーザーが興味があるであろう)情報を全て返すようにすべき。
toString
をオーバーライドする時に考慮すべき点として以下が挙げられる。
- ドキュメンテーション(javadoc)で戻り値の形式を明示するかどうか
- 明示することでユーザーに正確な表現形式を伝えることができる
- 一方で、明示した表現形式に依存した利用がされている可能性を考えると、その後の仕様変更がしづらくなるというデメリットもある
- 明示する、しないに関わらず、その意図は正確にjavadocに記載すべき
toString
の戻り値に含まれる全ての情報への、プログラミングによるアクセス手段を提供すべき
項目11. cloneを注意してオーバーライドする
clone
の一般契約は以下の通り。
- 「コピー」の正確な意味はオブジェクトのクラスに依存する
- 大まかな意図は以下が成り立つことだが、絶対的な要件ではない
- 任意のオブジェクト x に対して
x.clone() != x
が true であること - また
x.clone().getClass() == x.getClass()
も true であること x.clone().equals(x)
が true であること- そのクラスの新たなインスタンスを生成することに加え、内部データ構造のコピーも必要な場合がある
- コンストラクタは呼び出されない
- 任意のオブジェクト x に対して
clone
メソッドをオーバーライドするならば、super.clone
により得られたオブジェクトを返すべき
以下の例を考える。
public class Data1 implements Cloneable { ... @Override public Object clone() throws CloneNotSupportedException { return new Data1(x); } ... } public class Data2 extends Data1 { ... @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } ... }
上記の場合、Data2.clone
を呼び出すとsuper.clone
によりData1
のコンストラクタが返却される。
したがって「x.clone().getClass() == x.getClass()
も true であること」が成り立たない。
これはData1
クラスでも同様に、clone
でsuper.clone
を返すようにすることで解決する。
また上記でclone
メソッドのアクセス修飾子が public となっていることも大切である。
本書に書いてある通り、Cloneable
を実装しているクラスは、適切に機能している public のclone
メソッドを提供することが期待されている。
これらを踏まえて、java1.5からは下記のようにclone
メソッドを修正することができる。
public class Data1 implements Cloneable { ... @Override public Data1 clone() { try { return (Data1)super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } } ... } public class Data2 extends Data1 { ... @Override public Data2 clone() { try { return (Data2)super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(); } } ... }
上記の修正したclone
メソッドにおいて、注目すべき点が3点ある。
- 修正した
clone
の戻り値型がData1/Data2
になっている- java1.5から導入された共変戻り値型によるもの(オーバーライドしたメソッドの戻り値型は元のメソッドの戻り値型のサブタイプであってもよい)
super.clone
の戻り値は親クラスであるため、キャストを忘れずに行う(キャストはクライアントでなくライブラリーですべき処理)CloneNotSupportedException
のスロー宣言が削除されている- チェックされる例外をスローしない方が使い勝手が良い(publicの
clone
メソッドはスローしないべき) - 継承されることを想定したクラスで
clone
をオーバーライドする場合、clone
を protected と宣言し、CloneNotSupportedException
のスロー宣言をし、Cloneable
は実装しないようにする(Object.clone
をまねるべき)
- チェックされる例外をスローしない方が使い勝手が良い(publicの
フィールドに可変オブジェクトが含まれる場合は、再帰的にclone
を呼び出す
下記の例を考える。
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } ... }
上記のclone
メソッド内のsuper.clone
では、elementsフィールドの参照がコピーされるだけである。
(浅いコピー。元のオブジェクトとclone
オブジェクトで同一の配列が共有されてしまう。)
したがってelements.clone()
のように、再帰的にclone
を呼び出す必要がある。
しかしelementsフィールドがfinalの場合、新たな値を代入することができないため正しく機能しない。
この場合は、フィールドのfinal修飾子を取り除く必要がある。(clone
のアーキテクチャは可変オブジェクトを参照している final のフィールドと両立しない)
内部クラスを保持するなど、クラスの構造が複雑な場合、上記のように再帰的にclone
を呼び出すだけでは不十分な場合もある。
その時はsuper.clone
を呼び出し、返されたオブジェクトのすべてのフィールドを初期化した上で、オブジェクトの状態を再現するために
「深いコピー」を実現するための処理を呼び出すべき。
一般的に、クラスが基本データ型のフィールドか不変オブジェクトへの参照しか保持していない場合、上記のような「深いコピー」のための処理は不要。
clone
が本当に必要かよく検討する
これまで述べたようにclone
の実装は気をつけるべき点が多く複雑であり、本当に必要なケースは稀である。
(配列のコピー用途以外ではclone
を利用しないという人も多い)
以下のいずれかの手段で代替できないかよく検討する。
- コピーコンストラクタかコピーファクトリーを提供する
java public Yum(Yum yum); //コピーコンストラクタ public static Yum newInstance(Yum yum); //コピーファクトリー
- そもそもオブジェクトの複製機能を提供しない
項目12. Comparableの実装を検討する
Comparable
を実装することで、インスタンスが自然な順序を持っていることを明示する。
→アルファベット順、数値順、年代順など、明らかに自然な順序を持つ値クラスを書く場合、Comparable
を実装しcompareTo
メソッドを提供する。
compareTo
メソッドの一般契約は以下。これを守らないと、compareTo
に依存しているTreeSet
やTreeMap
、検索とソートアルゴリズムを含む
Collections
やArrays
が正しく機能しない。
(sgn(expression)
は、expression
が負の場合 -1、ゼロの場合 0、正の場合 1を返す関数)
- すべての x と y に関して
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
を保証しなければならないx.compareTo(y)
が例外をスローする( y のクラスは x と比較できない)場合のみ、y.compareTo(x)
は例外をスローしなければならない
(x.compareTo(y) > 0 && y.compareTo(z) > 0)
がtrueならばx.compareTo(z) > 0
もtrueでなければならない- すべての z に関して
x.compareTo(y) == 0
ならばsgn(x.compareTo(z)) == sgn(y.compareTo(z))
でなければならない (x.compareTo(y) == 0) == x.equals(y)
は強く推奨されるが必須ではない(破る場合は明示すべき)
上記制限はequals
制限と同じく、反射性、対称性、推移性に従わなければならないことを示している。
したがってequals
と同様に、インスタンス化可能なクラスを拡張してcompareTo
契約を守ったまま新たな値要素を追加する方法はなく、コンポジションが推奨される。
compareTo
は以下にしたがって実装すると良い。