メソッド

Effective Java 第 3 版の個人的メモ

  1. 項目 49 パラメータの正当性を検査する
  2. 項目 50 必要な場合、防御的にコピーする
  3. 項目 51 メソッドのシグネチャを注意深く設計する
  4. 項目 52 オーバーロードを注意して使う
  5. 項目 53 可変長引数を注意して使う
  6. 項目 54 null ではなく、空コレクションか空配列を返す
  7. 項目 55 オプショナルを注意して返す
  8. 項目 56 すべての公開 API 要素に対してドキュメントコメントを書く
  9. 参考URL

1. 項目 49 パラメータの正当性を検査する

1.1. 結論

  • 入力値のチェックをしましょう
  • Javadocに入力値の説明と例外の説明は記入しましょう

1.2. 良い例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	/**
	 * 値が(this mod m)であるBigIntegerを返します。
	 * このメソッドは、remainderメソッドとは異なり、
   * 常に負でないBigIntegerを返します。
	 * 
	 * @param m 正でなければならない
	 * @return this mod m.
	 * @throws ArithmetricException m <= 0の場合.
	 */
	public BigInteger mod(BigInteger m) {
		if(m.signum() <= 0) {
			throw new ArithmeticException("Modulus <= 0: " + m);
		}
		// 計算を行う
	}

最初に入力値チェックをすることで、以下を防げる。

  • メソッドの処理部で例外が発生してしまうこと(後から解析が必要になる。)
  • 処理で例外が出ずに、変な値が返却されてしまうこと(これは最悪の場合)

余談:@throws ArithmetricException m <= 0の場合.@throwsはメソッドにthrowsがついていないので忘れがちかもしれないなと思いました。(IDEで自動的に付加されないので。)

1.3. アサーションの紹介

privateメソッド(外部には公開しない)の場合は、そのパッケージの作成者として、どのような状況でメソッドが呼び出されるのかを アサーションを用いて入力値の検査をすべき。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
    int price = Integer.parseInt(args[0]);
    int num = Integer.parseInt(args[1]);

    if (price > 0 || num > 0) {
        int sum = sum(price, num);
        System.out.printf("%d + %d = %d%n", price, num, sum);
    } else {
        System.out.println("価格と数量には正の値を指定してください");
    }
}

private static int sum(int price, int num) {
    assert price > 0 : "price: " + price;
    assert num > 0 : "num: " + num;

    return price * num;
}
1
2
3
4
5
6
7
8
// 第1引数は0より大きな数である必要がある
java -ea EJ38 0 3
/**
* 実行結果
* Exception in thread "main" java.lang.AssertionError: price: 0
* at com.example.takuto_no.effective_java.chapter8.EJ38.sum(EJ38.java:39)
* at com.example.takuto_no.effective_java.chapter8.EJ38.main(EJ38.java:29)
*/

-ea(-enableassertions)をつけた場合のみassertは有効になる。 -eaを付けなければ、assertは有効にならないので、実運用時にコストは発生しない。

2. 項目 50 必要な場合、防御的にコピーする

2.1. 結論

  • クライアントが信頼できない場合は、防御的にコピーしましょう
  • クライアントが信頼できて、コピーのコストが非常に高い場合は、Javadocに記載しても良いです。

2.2. 防御的コピー

クライアントが、クラスの不等式を破壊するために徹底した努力をすると想定しましょう。

2.2.1. 悪い例

Dateはmutable(フィールドを変更可能なオブジェクト)です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 start() {
		return start;
	}

	public Date end() {
		return end;
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
// Attack 1
// Period内部のendと、endが同じアドレスを指しているため、これで、Period内部を変更できてしまう。
end.setYear(78);

// Attack 2
// Period#end()で、periodのendを取得できるため、Period内部を変更できてしまう。
p.end().setYear(78);

2.2.2. 良い例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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) {
		// Attack 1の対策
		// 元のパラメータのコピーを保存する
		this.start = new Date(start.getTime());
		this.end = new Date(end.getTime());

		if (start.compareTo(end) > 0)
			throw new IllegalArgumentException(start + " after " + end);
	}


	public Date start() {
		// Attack 2の対策
		// コピーを返却する
		return new Date(start.getTime());
	}

	public Date end() {
		return new Date(end.getTime());
	}
}

もしDateのコピーが非常にコストの高い行為であり、クライアントが信頼できる場合は、
“影響を受ける要素を変更しないこと"をクライアントの責任であると、Javadocに示すことで対応しても良い。

3. 項目 51 メソッドのシグネチャを注意深く設計する

3.1. 結論

  • メソッド名を注意深く選ぶべし
  • 便利なメソッドを提供しすぎないようにするべし
  • 長いパラメータのリストを避けるべし
  • パラメータ型に関しては、クラスよりインタフェースを選ぶべし
  • booleanパラメータよりは2つの要素を持つenum型を使用するべし

3.2. メソッド名を注意深く選ぶべし

APIを使う人が誤解なく使えるようにするために、以下をすること

  • 標準命名規約に従う
  • パッケージ内のほかの名前と矛盾のない名前にする
  • 広範囲のコンセンサスと矛盾がない名前を選ぶ
    • getHogeならhogeを返すなど

3.3. 便利なメソッドを提供しすぎないようにするべし

  • 提供するか迷ったらやめるべき
  • 【理由】多くのメソッドを用意すると、学習、使用、文書化、テスト、保守を困難にするから

3.4. 長いパラメータのリストを避けるべし

  • 特に同じ型のパラメータが続くことは避けるべき
    • 【理由1】利用者は多くのパラメータを覚えることができないので、ドキュメンテーションを常に参照しなければならなくなるから
    • 【理由2】同じ型のパラメータに対して順序を間違うと、コンパイルエラーにならず、利用者の期待した動作をしないから

長いパラメータリストを避けるテクニックは3つある

  1. 必要最低限の(細かな)単位でメソッドを作成する
  • 例:サブリストから指定された要素が最初に検出された位置のインデックスを返すindexOfSubList(Object o, int fromIndex, int toIndex)がある場合、
    次のように、複数のメソッドに分割する。
    list.subList(fromIndex, toIndex).indexOf(o)
  1. パラメータ間に繋がりがある場合はまとめる
  • 例:トランプの数字と種類をパラメータに取るメソッドhogemethod(int num, TrampSuit suit)があった場合、
    数字と種類をまとめたヘルパークラスを作成し、パラメータにヘルパークラスをとるようにする
    hogeMethod(Tramp tramp)
  1. 引数が多いコンストラクタの引数が多い場合は、ビルダーパターンを使う。

3.5. パラメータ型に関しては、クラスよりインタフェースを選ぶべし

  • パラメータの型は実装クラスよりもインタフェースを使用するべき
    • 例えば、HashMapではなくMapにする
  • 【理由】制約をゆるくするため。

3.6. booleanパラメータよりは2つの要素を持つenum型を使用するべし

enum型を使用したほうが利用者にとってわかりやすいから。また、新たな状態を追加しやすいから。

  • 例:Thermometer型があったとして、Thermometer.newInstance(true)より、Thermometer.newInstance(TemperatureScale.CELSIUS)の方が、分かりやすい。
    また、簡単にTemperatureScale.KELVINを追加できる。

4. 項目 52 オーバーロードを注意して使う

4.1. 結論

  • パラメータの数が同一のオーバーロードされたメソッドを提供するべきでない

4.2. オーバーロード

4.2.1. コードと実行結果_その1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TypeClassifier {
	public static String classify(int a) {
		return "int";
	}
	public static String classify(char a) {
		return "char";
	}
	public static String classify(double a) {
		return "double";
	}

	public static void main(String[] args) {
		System.out.println(classify(1));
		System.out.println(classify('a'));
		System.out.println(classify(1.0));
		// 実行結果
		/*
		   int
		   char
		   double
		*/
	}
}

4.2.2. コードと実行結果_その2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CollectionClassifier {
	public static String classify(Set<?> s) {
		return "Set";
	}

	public static String classify(List<?> s) {
		return "List";
	}

	public static String classify(Collection<?> s) {
		return "Collection";
	}

	public static void main(String[] args) {
		Collection<?>[] collections = { new HashSet<String>(), new ArrayList<String>(),
				new HashMap<String, String>().values() };

		for (Collection<?> c : collections) {
			System.out.println(classify(c));
		}
		// 実行結果
		/*
		   Collection
		   Collection
		   Collection
		*/
	}
}

オーバーロードされたどのメソッドが呼び出されるかの選択はコンパイル時に行われるため。
上の例では、3回のループで、パラメータのコンパイル時の型はCollection<?>であるので、classify(Collection<?> s)が呼び出される。

4.3. オーバーライド

4.3.1. コードと実行結果_その3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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";
	}
}

class Overriding {
	public static void main(String[] args) {
		List<Wine> list = List.of(new Wine(), new SparklingWine(), new Champagne());

		for (Wine wine : list) {
			System.out.println(wine.name());
		}
		// 実行結果
		/*
		   wine
		   sparkling wine
		   champagne
		*/
	}
}

オーバーライドは実行時に呼び出されるメソッドが決められるため。

4.4. オーバーロードの利用

  • 最も安全な方針は、 パラメータの数が同一のオーバーロードされたメソッドを提供しないこと
    • 回避策の例:ObjectOutputStreamは、write()をオーバーロードするのではなく、writeBoolean(boolean)writeInt(int) writeLong(long)としている。
  • 上記の方針を破らなければならない時は、限定されたオーバーロードをより一般的なオーバーロードに転送させるべき
    • 例えば、String#contentEquals()
      1
      2
      3
      
      public boolean contentEquals(StringBuffer sb) {
          return contentEquals((CharSequence)sb);
      }
      
  • コンストラクタの場合は、異なる名前を使用できないが、staticファクトリーメソッドを提供するという選択肢がある
  • そもそもオーバーロードを使いまくらないことが多いけど、パラメータの数が同一のオーバーロードされたメソッドを作ることもある
    • StringBuilder#append()はオーバーロードを使っている良い例

5. 項目 53 可変長引数を注意して使う

5.1. 結論

すべての引数の型が同じでも、全部可変長ではなく、一部のみにしたほうがいいことがある。

ex) 必須パラメータ と オプションパラメータ で分けたいとき。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

public class Varargs {
    public static void main(String[] args) {
        System.out.println(min1(99));
        System.out.println(min2(1, 2, 3));

    }

    // ugly
    static int min1(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;
    }

    // better
    static int min2(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for (int arg : remainingArgs) {
            if (arg < min) {
                min = arg;
            }
        }
        return min;
    }
}

6. 項目 54 null ではなく、空コレクションか空配列を返す

6.1. 結論

nullよりも、 空オブジェクト を常に使うこと。

6.2. 理由

  • nullが返ってくる -> 使う側は常にnullチェックを強制される。
  • 場合によっては、コレクションが空or要素があるのどちらかを考慮せずコードがかける。
1
2
3
// 要素があれば、そのSetが返却される
// 要素がなければ、空のSetが返却される
list.stream().map(str -> str.upperCase()).collect(Collectors.toSet());

なお、空オブジェクトを返す挙動を 必ず javadocに記述すること。

  • null と 空オブジェクト で意味が異なる場合は、もちろん、両方の意味を定義して使い分けること
  • 古いAPIだと 空オブジェクトで良い場面でnullを使っているケースがある

7. 項目 55 オプショナルを注意して返す

7.1. 結論

java8 から Optional<T> が入ってきた。定義をjavadocから引用すると…

1
2
null以外の値が含まれている場合も含まれていない場合もあるコンテナ・オブジェクトです。
値が存在する場合、isPresent()はtrueを返し、get()は値を返します。

基本これを使うと、NULLセーフできる。

また、Optionalを使うと、以下が改善できるシーンがでてくる

  • if文のブロックを可読性の高いシンプルなコードに置き換えることができる
  • lambdaと相性がよいので、stream処理のmap、filterに自然に組み込むことができる

7.2. 補足

  • Optionalが戻り値であるメソッドからnullを返してはならない。これを行うと、Optionalを導入した意味がなくなる。
    • Intellij は警告してくれる

8. 項目 56 すべての公開 API 要素に対してドキュメントコメントを書く

8.1. 結論

(状況に応じて、できる範囲で)必ず書きなさい。

  • 利用者の範囲
    • OSS
    • 会社内共通
    • プロジェクト共通
    • 個別業務ロジック
  • ライフサイクル
    • 長く維持する
    • 一発
  • 使う場所
    • 製品コード
    • テストコード
    • ツール etc…

必ず、記述粒度は書き始める前に関係者で話しておくこと。

いかのタイミングで重要なタグが追加された。

  • Java9で@index
  • Java8では@implSpec
    • 継承元となる設計をされたクラスは@implSpecタグでメソッドとサブクラスとの決まり事を記述する。
  • Java5では@literalと@code

8.2. 書き方

How to Write Doc Comments https://www.amazon.co.jp/エンジニアのためのJavadoc再入門講座-現場で使えるAPI仕様書の作り方-佐藤-竜一/dp/4798119482

結局、一言でいうと、「制約を順序立てて記述する」ことを念頭に置くと、だいたい大丈夫です。

なにか迷ったら、本家のjavadocをパクる。

  • public, protected は絶対書く
  • 重複している役割のものがある(@throws , @exception / {@code} , <code>)ので、 どれを使うかは、チームで軽く決めるとよい。

意外とわすれがちなのは、スレッドセーフかどうか、シリアライズ関連。 特別な考慮事項があれば記述すること。

9. 参考URL