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(); }