すべてのオブジェクトに共通のメソッド

Effective Java 第 3 版の個人的メモ

  1. 項目 10 equals の override は一般契約(general contracts)に従うべし
  2. 項目 11 equals をオーバーライドする場合は hashcode もオーバーライドせよ
  3. 項目 12 常に toString をオーバーライドせよ
  4. 項目 13 clone をオーバーライドするときは注意せよ
  5. 項目 14 Comparable を実装することを考慮せよ

1. 項目 10 equals の override は一般契約(general contracts)に従うべし

1.1. どういう時にオーバーライドする必要があるのか?

クラスが単なるオブジェクトの同一性とは異なる論理的等価性という概念を持っていて、他のインスタンスと比較する必要があるとき

ただし、スーパークラスが equals をオーバーライドしていないこと。

1.2. どうやってオーバーライドすれば良いのか?

一般契約(反射性, 対照性, 推移性, 整合性, 非 null 性)に従うこと

以下のツールを使うと簡単に一般契約に従ったコードにできる。

  1. Annotation Processor(Java 6 から追加)
  • lombok@Dataなど
  • 欠点:Annotation Processor を 2 つ利用していて、@Dataとは別の Annotation がsetter()を要求している場合、コンパイルエラーとなる場合がある。 @Dataが後で処理されるようになっていると、setter()がないからである。この場合はどうしようもない。。

Annotation Processor は、コンパイル時に処理される。lombok@Dataの場合は、コンパイル時に、setter/getter/equals/hashCode/toStringメソッドのバイトコードを.classに突っ込んでくれる。

  1. Eclipse や Intellij の自動生成機能
  • GUI で簡単に生成可能
  • 欠点:メンバ変数を追加した際には、その都度、GUI で実行しないといけないから手間である。

2. 項目 11 equals をオーバーライドする場合は hashcode もオーバーライドせよ

2.1. hashcode もオーバーライドしないとどうなるのか?

論理的に等しいが、hashCodeの値は異なることになってしまう。

論理的に等しいのに異なってしまうのはおかしいので、hashCodeも同じであるべきである。

  1. 論理的に等しい(フィールド値が同じである)ので、その値を使った Hash 関数を作成しましょう。

こちらも以下のツールを使いましょう

  1. Annotation Processor(Java 6 から追加)

  2. Eclipse や Intellij の自動生成機能

PhoneNumber.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public final class PhoneNumber {
        private final short areaCode;
        private final short prefix;
        private final short lineNumber;
        .
        .
        .
        @Override
        public int hashCode() {
            int result = 17;
            // Hash関数については研究が進んでいるが、当分は以下が良いコードらしいです。
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            return result;
        }
}

2.2. 【余談】volatile 修飾子について

項番 78「共有される mutable なデータには、同期したアクセスをすべし」で説明されるらしいですが、、この章に出てきたので。

2.2.1. volatile 修飾子とは?

volatile修飾子をつけると、「必ずメモリに書く」ようになる。

並列処理などで、あるインスタンスAメンバ変数Xを更新する際、通常はスレッドキャッシュに書き込まれた後、メモリに書き込まれる。 これをスレッドキャッシュへの書き込みをせず、メモリ書き込みを最初からさせるようにするのが、volatile修飾子である。

ただし、「volatile で排他制御出来ると、いつから思ってた? atomic や synchronized 使うべし」に記載があるように、volatile修飾子では排他制御はできない。。

そのため、結局は、atomicsynchronizedを使って、排他制御することになる。

2.2.2. volatile修飾子の使い所について

項番 78 を見たところ、以下の double-checked locking の話が出ているが、double-checked locking と Singleton パターンを読んだところ、結局、volatile修飾子は使えず、同期化を受け入れるか、static field を使うようである、、、つまりvolatile修飾子は使われないよう、、、いつ使うねん。。笑

Singleton.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {
      instance = new Singleton();
    }
  }
  return instance;
}

項番 78「共有される mutable なデータには、同期したアクセスをすべし」まで、とりあえずは放置しておくことにする。

3. 項目 12 常に toString をオーバーライドせよ

3.1 まとめ

  • toString には、フィールド値を含んだ情報を提示しましょう。
    • Eclipse や Intellij でtoString()を自動生成できる。

ToStringSample.java

1
2
3
4
	@Override
	public String toString() {
		return String.format("toStringSample [text=%s, id=%s, value=%s]", text, id, value);
	}
  • BigIntegerBigDecimalなどのように数値を扱うクラスは、toString(), valueOf()で相互変換できるようにしておきましょう。

    • その方が使いやすくて便利。
  • toString()以外でもフィールド値などの情報を得られるようにしておきましょう。

    • 利用者側が、無理やりtoStringから値を取得するようなことになってしまうので、、、

4. 項目 13 clone をオーバーライドするときは注意せよ

4.1 はじめに

  • Cloneable インタフェースはオブジェクトが複製を許可していることを示すだけの空のインターフェースだよ。

  • java.lang.Object の clone メソッドは protected なので、Cloneable インタフェースを実装するだけでは clone メソッドを呼び出すことができないよ。

  • 可変オブジェクトの参照がある場合は、参照先を「Deep Copy」しましょう。

    • Deep Copyは配列やリストの値を 1 つずつコピーする方法で、対するShallow Copyはアドレスを単にコピーするだけだよ。

4.2 結論

  • 純粋な Java の世界では、cloneメソッドを使わずに、copyFactoryメソッドで複製できるように作る方法がいいよ。

  • Spring の世界では、ApplicationContextがインスタンスを管理しているので、ApplicationContenxt#getBean()で、Prototypeとしてインスタンスを取得する方法がいいよ。

5. 項目 14 Comparable を実装することを考慮せよ

5.1 はじめに

  • compareTo()は、Comparable インタフェースの唯一のメソッド

  • CompareTo()メソッドを持っているクラスは、「自然な順序関係」(abc 順や数値順、年代順など)を持っていることを表しているよ。

    • 自然な順序関係の例=辞書順、数値順
    • 1,2,3,4,..,9,10,11,…,99,100 をソートする時にどの順序関係で扱うかで結果が変わる
      • 数値順=上に記述した通り
      • 辞書順=1,10,100,11,..,2,20,21,...3,30...
        • Java だと、String で sort すると、辞書順ソートになる。
  • compareTo契約(equals の契約と同様)を破っていると、compareTo()メソッドに依存しているクラス(TreeSet, TreeMap, Collections, Arraysなど)が機能しなくなるので気をつけましょう。

5.2 どのように実装するか

  • compareToメソッドを実装している2つの例を確認しましょう。

PhoneNumber_1.java

 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
public class PhoneNumber implements Comparable<PhoneNumber> {

	private int areaCode;
	private int prefix;
	private int lineNumber;

	public int CompareTo(PhoneNumber pn) {
		// 市外局番を比較する
		if (areaCode < pn.areaCode)
			return -1;
		if (areaCode > pn.areaCode)
			return 1;

		// 市外局番は等しく、市内局番の前半を比較する
		if (prefix < pn.prefix)
			return -1;
		if (prefix > pn.prefix)
			return 1;

		// 市外局番と市内局番の前半は等しく、市内局番の後半を比較する
		if (lineNumber < pn.lineNumber)
			return -1;
		if (lineNumber > pn.lineNumber)
			return 1;

		// 全てのフィールドが等しい
		return 0;
	}
}

PhoneNumber_2.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PhoneNumber implements Comparable<PhoneNumber> {

	private int areaCode;
	private int prefix;
	private int lineNumber;

	public int compareTo(PhoneNumber pn) {
		// 市外局番を比較する
		int areaCodeDiff = areaCode - pn.areaCode;
		if (areaCodeDiff != 0) {
			return areaCodeDiff;
		}

		// 市外局番は等しく、市内局番の前半を比較する
		int prefixDiff = prefix - pn.prefix;
		if (prefixDiff != 0) {
			return prefixDiff;
		}

		// 市外局番と市内局番の前半は等しく、市内局番の後半を比較する
		return lineNumber - pn.lineNumber;
	}
}
  • PhoneNumber_1.javaPhoneNumber_2.java と比べて計算量が多くて遅いけれど、確実に動作する。
  • PhoneNumber_2.java は PhoneNumber_1.java と比べて計算量が少なくて速いが、最小値と最大値の差が、int の最大値(2^31 - 1)を超えていたらオーバーフローしてしまう。

5.3 結論

最小値と最大値の差が、int の最大値(2^31 - 1)を超えていたらオーバーフローしてしまう可能性がある、もしくは、数値の比較ではない場合、PhoneNumber_1.java のように実装しましょう。そうでなければ、PhoneNumber_2.java のように実装しましょう。

java 標準のBigDecimal#compareTo()とか、Timestamp#compareTo()は、オーバーフローする可能性がある、もしくは、数値での比較では無いので、PhoneNumber_1.java のように実装しています。

java 標準のByte#compareTo()は、オーバーフローする可能性がないので、PhoneNumber_2.java のように実装しています。