View
1.048
Download
6
Category
Preview:
DESCRIPTION
Citation preview
EFFECTIVE JAVA 第7章 メソッド
滝波 景
項目38 パラメータの正当性を検査する
メソッド、コンストラクタを実装するときは
できるだけ早めにエラーを検出できるようにする
public static List<Integer> returnList(Integer[] numbers) {
//このチェックがないと呼び出し側でエラーになって、デバッグが大変に。
if (numbers == null) { throw new NullPointerException(); } //...//配列をリストに変換する処理
あとになればなるほどエラーの原因が発見しづらい。
最悪なのは、メソッドは正常にリターンしてるのにいくつかのオブジェクトが不正のまま後になって、全く関係ないところでエラーになる。
メソッド、コンストラクタを実装するときは
publicなメソッドに対してはJavadocの@throwsタグを
使って文章化
/** * 値が(this mod m)であるBigIntegerを返します。このメソッドは、 * remainderメソッドとは異なり、常に負でないBigIntegerを返します。 * * @param m 正でなければならない * @return this mod m
* @throws ArithmeticException m <= 0の場合 <=チェック内容を文章化 */ public BigInteger mod(BigInteger m) { if (m.signum() <= 0) { throw new ArithmeticException(); } ...//計算を行う }
メソッド、コンストラクタを実装するときは
privateなメソッドはアサーションを用いて検査する
// 再起的ソートのためのprivateのヘルパー関数 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; ...//計算を行う
}
publicなメソッドと違ってどんな状況でそ
のメソッドが呼ばれるか管理できるからOK
実行時に-eaオプションでアサーションを
有効にしない限りはアサーションの効果かもないし、コストもかからない。
ただし
例外もある
例えば
Collections.sort(List)
このListの中身は比較可能なオブジェクトでないと、
ClassCastExceptionがスローされる。
これはsortメソッドが行うことで、実装者がチェックすることではない。
なので、sortの前に比較可能のチェックを実装する必要はない。
チェックのコストが高い場合
現実的でない && チェックが暗黙に行われる場合
項目38 必要な場合には、防御的にコピーする
Javaは安全な言語だけど、きちんと書かないと他のクラスから守ることができない。
防御的なコードを書こう
//一見問題ないコード 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; } //残りは省略 }
// Periodインスタンスの内部を攻撃 Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear( 77); // pの内部を変更する!!
一見すると、期間の開始と終わりの後には絶対こないように見えるけどDateは可変なので、
簡単に終わり(end)を変えられる。
対策 コンストラクタで可変なパラメータのコピーしちゃう。
(ここでは新しいDateオブジェクトを作成)
// 修正されたコンストラクタ - パラメータの防御的コピーをする public Period(Date start, Date end) { this start = new Date(start.getTime); this end = new Date(end.getTime); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException(start + " after " + end); } }
さっきみたいにend.setYear(78); ってやっても
参照しているオブジェクトが違うから変更されない。
注意
ちなみにオブジェクトをコピーするときはcloneを使ってはダメ。
Dateはfinalなクラスではないので、cloneメソッドがjava.util.Dateを返すとはかぎらない。
逆にいうと
finalでないクラスはcloneしてはいけない!!
※http://www.jpcert.or.jp/java-rules/obj04-j.html
待って!!
// Periodインスタンスの内部への第2の攻撃 Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.getEnd.setYear(78); // pの内部を変更する!!
実はまだ、Periodクラスは攻撃から上手く防御できてない。
setterで変更できちゃう
対策2 さっきの修正
+
getterもコンストラクタの値を元に新しいDateを返す
これで、Periodは本当に不変
public Date getStart() { return new Date(start.getTime); } public Date getEnd() { return new Date(end.getTime); }
public class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(); } this.start = start; this.end = end; } public Date getStart() { return start; } public Date getEnd() { return end; } //残りは省略 }
public class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(); } this start = new Date(start.getTime); this end = new Date(end.getTime); } public Date getStart() { return new Date(start.getTime); } public Date getEnd() { return new Date(end.getTime); } //残りは省略 }
※ただ一番いいのはDateではなく、Date.getTimeで取得される基本型のlong値を使う方法。
変更前 変更後
項目38 メソッドのシグニチャを注意深く設計する
メソッドを設計する上でのポイント
1.メソッド名を注意深く選ぶ
・標準命名規則に従う
・理解可能で同じパッケージ内の他の名前と
矛盾がないこと
2.便利なメソッドを提供し過ぎしないようにする
・あんまり多いと学習、使用、文章化、テスト、
保守のコストがかかる
・メソッドの多いインターフェイスは使用者を混乱させる
(よくある無駄に多いUtiltyクラスのメソッドが当てはまるのかな?)
3.パラメータ型に関しては、クラスよりインターフェイ
スを選ぶ
・例えば、引数としてHashMapを使う場合でも、Mapを使う。
そうすると、HashTable,HashMap,TreeMap....といった感じで
どんなサブマップが使える。
OK public static void sample(Map<String, String> map)
NG public static void sample(HashMap<String, String> map)
4.booleanパラメータより2つの要素を持つenum型を
使用する。
・コードが読みやすくなるし、IDEの保管機能があるとより
書きやすい。
例)public enum TemperatureScale { FAHRENHEIT, CELSIUS }
Thermometer.newInstance(true)よりは
Thermometer.newInstance(TemperatureScale.CELSIUS)
3.長いパラメータのリストは避ける
・4個以下が目標
・長いパラメータリストは覚えられないし、
その場合はドキュメントを都度参照しないといけない。
・同じ型が続くのは特に有害。
順番が違ってもコンパイルできて実行できちゃう。
対策1
メソッドを複数のメソッドに分割
例)java.util.List #indexOf メソッド
indexOf(検索対象オブジェクト, fromIndex, toIndex)
というメソッドではなく、
「subList(fromIndex, toIndex)」
「indexOf(検索対象オブジェクト)」
に分割されている。
対策2
パラメータの集まりを保持するクラスを作成
public class Sample { public doSomething(String firstName, String lastName,String address, int number, Sting district) { //残りは省略 }
public class Sample { private String firstName; private String lastName; private String address; private int number; private String district; //残りは省略 } public class Sample { public doSomething(User user) { //残りは省略 }
対策3 ビルダーパターン
// ■オブジェクト側 public final class Commodity { private final int id; // 必須 private final String name; // 必須 private final long lowestPrice; // オプショナル public static class Builder { private final int id; private final String name; private long lowestPrice; public Builder(int id, String name, long catalogPrice) { // 必須項目は Builder のコンストラクタで指定 this.id = id; this.name = name; } public Builder lowestPrice(long lowestPrice) { this.lowestPrice = lowestPrice; return this; } public Commodity build() { // パラメータ間の整合性は build メソッドで解決 if (lowestPrice > 100) throw new IllegalStateException(); return new Commodity(this); } } private Commodity(Builder builder) { id = builder.id; name = builder.name; lowestPrice = builder.lowestPrice; } // getter 省略 } // ■クライアント側 Commodity commodity = new Commodity.Builder(10, "hoge").lowestPrice(100).build();
詳しいことはこっちで
項目41 オーバーロードに注意して使用する
class CollectionClassifier { public static String classify(Set<?> s) { return "Set"; } public static String classify(List<?> lst) { return "List"; } public static String classify(Collection<?> c) { return "Unknown Collection"; } public static void main(String args[]) { Collection<?>[] collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String, String>().values() }; for (Collections<?> c : collections) { System.out.println(classify(c)); } } }
一見、問題なく Set,List,Unknown Collection
と表示されそう。
でも、実際はUnknown
Collectionを3回表示。
どのオーバーロードされたメソッドが呼ばれるかは、コンパイル時に決まる。 パラメータのコンパイル時は Collection<?> なので、classify(Collection<?>)で実行されてしまう。
for (Collections<?> c : collections) {
System.out.println(classify(c)); }
public static String classify(Collection<?> c) { return c instanceOf Set ? "Set" : c instanceOf List ? "List" : "Unknown Collection" }
修正するなら、instanceOfで検査
オーバーロードするときのポイント
1.利用者を困惑させるようなオーバーロードを使用しない
2.同一数の引数を持つオーバーロードを行わない
※例外として引数の型が明らかに違う場合はOK
例)ArrayList
・ArrayList(int initialCapacity)
・ArrayList(Collection<? extends E> c)
3.オーバーロードでなく、別にメソッドとして提供する
例)ObjectOutputStream
writeメソッドがあるが、このメソッドをオーバーロードするのでなく
writeBoolean(boolean), writeInt(int), writeLong(long) を提供
メリットとしては、対応する
readBoolean(boolean), readInt(int), readLong(long) が提供できること
オートボクシングによる問題
public class Sample { public static void main(String args[]) { Set<Integer> set = new TreeSet<Integer>(); List<Integer> list = new ArrayList<Integer>(); for (int i = -3; i < 3; i++) { set.add(i); list.add(i); } for (int i = 0; i < 3; i++) { set.remove(i); list.remove(i); } System.out.println(set + " " + list); } }
一見、問題なく list,set共に[-3,-2,-1,0,1,2]と
setされて それぞれ[0,1,2]がremoveされた
[-3,-2,-1][-3,-2,-1] が表示されそう
でも、実際は
[-3,-2,-1][-2,0,2] が表示される
原因 ・ set.remove(i)はオーバーロードしているremove(E)が選択される (iがintからIntegerへオートボクシングされる) ・list.remove(i)はオーバーロードしているremove(int i)が選択されて
“位置”を削除する
※ちなみにListの中身 E remove(int index) リストの指定された位置にある要素を削除します。
boolean remove(Object o) 指定された要素がこのリストにあれば、その最初のものをリストから 削除します。
オートボクシングできるようになってからは、 さらにオーバーロードに気をつける必要あり。
項目38 可変長引数を注意して使用する
static int sum(int args... ) {
int sum = 0;
for (int arg : args) { sum += arg; } return sum; }
可変長引数メソッドの例
(0個の場合もOK)
static int sum(int args... ) {
if (args.length == 0) { throw new IllegalArgumentException("Too few arguments"); }
int min = args[0];
for (int i = 1; i < args.length; i++) { if (args[i] < min) { min = args[i]; } }
return min; }
可変長引数メソッドの例
(0個の場合はNG、 必ず1つ以上)
問題点
・引数なしてこのメソッドを呼んだ場合、コンパイル時でなく、実行時に失敗する。
・メソッドが見づらい
・argsに対して明示的なチェックをしないと行けない
・minをInteger.MAX_VALUEへ初期化しないと、for-eachが使えない
static int sum(int firstArg, int remainingArgs... ) { int min = firstArg; for (int arg : remainingArgs) { if (arg < min) { min = arg; } } return min; }
解決策
・引数が0の場合はfor分に入らないのでOK
・前より見やすい
項目43 nullでなく、空配列か空コレクションを返す
private final List<Cheese> cheesesInStock = ...; /** * @return 店にある全てのチーズを含む配列、もしくは、 * 変えるチーズがなければnull * * @return */ public Cheese[] getCheeses() { if (cheesesInStock.size() == 0) { return null; } }
よくないコード
チーズがない場合に対してわざわざnullを返却しなくていい
呼び出し側はnullを処理するための余分なコードを書かないといけない
Cheese[] cheeses = shop.getCheeses(); // nullチェックしないとダメ
if (cheeses != null && Arrays.asList(cheeses).contains(Cheese.STILTON)) { System.out.println("Jolly good, just the thing"); }
呼び出し側の面倒な処理 ~nullチェックしないといけなくなる~
~containsでいきなりチェックもできない~
Cheese[] cheeses = shop.getCheeses();
if (Arrays.asList(cheeses).contains(Cheese.STILTON)) { System.out.println("Jolly good, just the thing"); }
// コレクションから配列を返す正しい方法 private final List<Cheese> cheesesInStock = ...; private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; /** * @return 店にある全てのチーズを含む配列 */ public Cheese[] getCheeses() { return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY); }
解決策
public List<Cheese> getCheeseList() { if (cheesesInStock.isEmpty()) { return Collections.emptyList(); //常に同一のリストを } else { return nre ArrayList<Cheese>()cheesesInStock; } }
1.配列を返す場合
2.コレクションを返す場合
つまり、配列やコレクションを返すメソッドは空配列、
空コレクションの代わりにnullを返すべきでない
項目44 全ての公開API要素に対して ドキュメントコメントを書く
すべての クラス、インターフェイス、コンストラクタ、
メソッド、フィールド宣言の前に
ドキュメントコメント(javadoc)を書く
メソッド 1.メソッドがどのように処理をしているかでなく、
何をしているかを書くこと
・事前条件と事後条件を列挙するべき
事前条件:呼び出すときに成立していないといけないこと
(@throwsタグにより暗黙的に記述)
事後条件:呼び出しが正常に完了した後に成立していないと
いけないこと
2.副作用も書く
・メソッドがバックグラウンドでスレッドを開始していることとか
3.@param @return @throwsもきちんと書く
HTMLへの変換 1.JavaDocはHTMLに変換されるけど、
HTMLテーブルを作るとかまではしなくてOK
2.1.5より前はHTMLのメタ文字はエスケープ
しないといけなかったけど今は{@code}でOK 例) {@code index < 0 || index >= this.size()}
3.複数行からなる場合は<pre>タグを使わないで
{@code}タグを使うこと。 <pre>{@code で始めて、 文字}</pre>
4.”・<,>,&”とかのHTMLメタ文字を生成するときは{@literal}を使う
{@code}と似ているけど、テキストをコードフォント
で表示しない点が違う
例) The triangle inequality is {@literal |x + y| < |x| + |y|}
コメントの最初の文は概要説明を書く 1.クラス、インターフェイス内の2つのメンバーとか
コンストラクタが同じ概要説明を持ってはダメ。
特にオーバロードは同じ内容になりやすいので、注意。
2.ピリオドに注意 例)A college degree, such as B.S., M.S. or Ph.D.
後に空白、タブがあると、そこで概要説明が終わってしまう
解決策としては{@literal}で囲むことで、ピリオドの後に空白が続かない。
/** * A college degree, such as B.S., {@literal M.S.} or Ph.D. * College is a fountain of knowledge where may go to drink. */ public class Degree {...}
メソッドとクラスとで、書き方を変える 1.クラス、インターフェイスの場合
クラス、インターフェイスのによって表される自柄を説明している
名詞句であるべき
例) TimerTask -- A task that can be scheduled for one-time or repeated execution by a Timer
Math.PI -- The double value that is closer than any other to pi,the ratio of the
circumference of a circle to its diameter
2.メソッド、コンストラクタの場合
メソッドやコンストラクタが行う処理を説明している動詞句であるべき
例) ArrayList(int initialCapacity)-- Constructs an empty list with the specified initial capacity
Collection.size()-- Returns the number of elements in this collection
ジェネリックス、enum、アノテーション
1.ジェネリックス ジェネリック型、ジェネリックメソッドの場合は
全ての型パラメータを文書化すること
/** * .... * @param <K> the type of keys maintained by this map * @param <V> the type of mapped values */ public interface Map<K,V> { ....// 残りは省略 }
2.enum型 enum型の場合は型とすべてのpublicメソッド
だけでなく、定数も文章化すること
/** * .... */ public enum OrchestraSection { /** Woodwinds, such as flute,clarinet, and oboe. */ WOODWIND, /** Brass instruments, such as french horn and trumpet. */ BRASS, /** Percussion instruments, such as timpaani and cymbals. */ PERCUSSION }
3.アノテーション アノテーションを文書化する場合は、その型自信
だけでなく、全てのメンバーを文章化すること
/** * .... */ @Retntion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Exception { /** * The exception that the annotaed 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(); }
その他 ・Javadocはメソッドのコメントを{@inheritDoc}で
スーパークラスとかインターフェイスから
継承できる
・コメントの中の間違いを探すのはHTM検証チェッカー
が便利。
Recommended