適切な Java コーディングのために心がけること

良いコードを書くために心がけることを箇条書き。


Close 処理を finally で行う

ダメな例

InputStreamReader reader = new HogeInputStreamReader(...);
doSomething(reader);
reader.close();

doSomething(reader); で Exception が発生した場合に close されない。
close 処理は必ず finally 節で行い、確実に close されるようにする。

良い例

InputStreamReader reader = null;
try{
    reader = new HogeInputStreamReader(...);
    doSomething(reader);
} finally {
    if( reader != null )
        reader.close();
}

close 処理に限らず、dispose 等でも同様に finally 節を使う。
リソースリークは発見・再現が困難なバグなので、確実に防ぐことを心がける。


equals メソッドをオーバーライドした場合は hashCode メソッドもオーバーライドする

これを守らなければ悲惨なことになる。

HashMap を例に考える。
まず下記のようなクラスを作る。

// hashCode メソッドをオーバーライドしていない
public class BadClass {
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

ではこの BadClass の挙動を確認。

public static void main(String[] args) throws Exception {
    BadClass bad1 = new BadClass();
    BadClass bad2 = new BadClass();
    System.out.println(bad1.hashCode());
    System.out.println(bad2.hashCode());
    System.out.println(bad1.equals(bad2));
}

// 実行結果
623839641
857681633
true

以上、ハッシュコードが異なる等しいオブジェクトができた。

さてこの BadClass のインスタンス bad1, bad2 を用いて HashMap を利用してみよう。

Map<BadClass, String> map = new HashMap<BadClass, String>();
map.put(bad1, "a string value");

System.out.println(map.get(bad2));

このコードを実行すると、何が表示されるか?
bad1.equals(bad2) は true なので、 "a string value" と表示することを期待するかもしれない。だがそれは間違いだ。map.get(bad2) は null を返す。これは、HashMap 内で bad1 の hashCode が挿入に使われ、次に map.get(bad2) で検索される bad2 の hashCode は bad1 の hashCode とは異なるからだ。
つまり、equals をオーバーライドする時は下記の二点に注意すること。
・equals をオーバーライドする時は hashCode もオーバーライドすること
・等しいオブジェクトは等しいハッシュコードを返すこと


引数の型を Object にしない

本当に全てのオブジェクトに対して操作可能なメソッドなら良いが、そうでない場合、つまりいくつかの受け入れ可能な型があり、それらの共通のクラスが Object であるような場合は、受け入れ可能なクラスの数だけメソッドをオーバーロードすること。

例えば javax.imageio.ImageIO の createImageInputStream(Object) メソッドを見てみよう。

public static ImageInputStream createImageInputStream(Object input) throws IOException
指定された Object から入力を取得する ImageInputStream を返します。

http://java.sun.com/javase/ja/6/docs/ja/api/javax/imageio/ImageIO.html#createImageInputStream(java.lang.Object)

この getImageReaders は InputStream, File, RandomAccessFile を渡せば適切な ImageInputStream が返されるが、シグニチャだけではその情報は得られない。これらのクラスのインスタンスではないオブジェクトを渡すと null が返される。これは適切な API では無い。引数を見て何を渡せば良いのかが明らかであること。それが適切なAPIだ。


メソッドのシグニチャで throws Exception しない

上記と似たような話だが、throws Exception はあまりに範囲が広いので、もう少し型を限定し、API使用者に情報を提供する。

例えば下記のようなコードではどのような例外が返されるかわからない。

get(long id) throws Exception 

それが下記のように、SQLException が返されるのが明らかであれば、このメソッドはデータベースに接続してデータを取得することが明らかだ。

get(long id) throws SQLException

このように、throws 節はメソッドの中身を推測できる情報なので、安易に throws Exception と書いてはならない。


throw new RuntimeException(e) しない

これも同様。throws 節を書くのが面倒だとか、適切な RuntimeException のサブクラスを作るのが面倒だとかという理由で安易に RuntimeException で包んではならない。
必ず適切な例外クラスを作成すること。

try{
    ...
} catch( Exception e) {
    // これをやってはいけない
    // 適切な例外クラスを作成すること。
    throw new RuntimeException(e);
}

引数の配列を直接フィールドに格納しない、直接返さない

配列オブジェクトの参照をクラス外部でも保持していることのメリットはほとんど無く、デメリットが多い。不変性を破られたり不正な状態になったりなど、想定外の挙動をしてしまうことがある。配列オブジェクトに限らず全てのオブジェクトなら同様であるが、特に配列オブジェクトに関しては気づかずに不正な状態を作ってしまうことが多い。
配列のやり取りはコピーを伴うこと。


Throwable を catch しない

明確な理由が無い限り、Throwable は catch しない。
Exception と異なり Error は致命的な場合があり、Throwable で十把一絡げに処理してはならない。
せめて Error と Exception は異なる catch 節で受け取り、Error を catch する理由をコメントに記述しておくこと。


Null チェックを行う

null になりうるものは null チェックを行う。
引数で渡されたオブジェクトの null チェックを怠り、別のメソッドにそのまま渡してそのメソッド先で NullPointerException が発生した場合はどの時点で null だったのかを判別しにくい。そのため、できるだけ入念に Null チェックを行い、NullPointerException 発生時には即座に原因を特定出来るようにすること。

また、特に例外処理時には入念に null チェックを行うこと。何らかの例外を適切に処理したつもりでもそこで NullPointerException が出てしまったら目も当てられない。必ず null チェックを行うか、null が来ないことを確かめて例外処理を行なうこと。


Collection を返すメソッドは null を返さず、空 Collection を返す

「処理失敗時には null を返す」ような規約がない場合、空 Collection を返すこと。
また、null を返さないことが保証出来る場合、クライアント側の null チェックを省くようドキュメント化すること。

型パラメーターへのキャストを行わない

型パラメーターへのキャストは型安全性を崩壊させるので行なってはいけない。
参考:型パラメーターへのキャストは絶対に行ってはいけない


プリミティブラッパークラスを new しない

パフォーマンスのために自分で new しない。valueOf メソッドを使用する。valueOf メソッドを用いて得たインスタンスは、値によってはキャッシュ済みのインスタンスを返すので不要なオブジェクトを作らなくて済む。

new した例

Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println(i1 == i2);

// 実行結果
false

valueOf を使う例

Integer i1 = Integer.valueOf(1);
Integer i2 = Integer.valueOf(1);
System.out.println(i1 == i2);

// 実行結果
true

現在の Java の実装では、-128 〜 +127 までの範囲をキャッシュしているようだ。


文字列を == で比較しない

文字列もれっきとしたオブジェクトなので等価比較ではなく同値比較すること。文字列は不変オブジェクトなので等価比較したい場合は(おそらく)存在しない。equals メソッドや、より安全に apache commons 等の StringUtil を使用して比較すること。