Effective Javaを読むチャレンジ-項目11の2-

前回の続きで項目11 cloneの話を読んでいきます。

正しく機能するcloneの実装

cloneの一般契約

Objectクラスのcloneメソッドのマニュアルから抜粋しました。

実質、そのオブジェクトと別のオブジェクトが生成されればそれでいい、ということになります。複製するクラスがequalsメソッドを実装していないと、x.clone() != xのときに必ずx.clone().equals(x)もが成り立たないからだと思います。また、Objectクラスのclone自体は何をするかというと、複製先のオブジェクトのフィールドに対して、複製元オブジェクトの対応するすべての各フィールドの内容で初期化します。これは代入と同じということです。フィールドからフィールドへの代入と同じということは、コンストラクタは呼び出されないことになります。この一般契約がいろいろ問題ありということです。では正しく機能するcloneとは何なのでしょう、というのがここから先の話です。

cloneメソッドをオーバーライドするならば、super.cloneにより得られたオブジェクトを返すべき

これは実際にやってみるまで何を言っているか分かりませんでした・・・。こんな簡単なモデルを作ってみます。

値クラスData1は、コンストラクタを返すようにcloneを実装しています。そのサブクラスであるData2は、super.clone()を呼び出しています。この状態で以下のコードを実行してみます。

この結果は、以下となります。

コンストラクタを返すようなcloneメソッドがスーパークラスとなった場合、そのcloneメソッドが提供するオブジェクトは、x.clone().getClass()== x.getClass()が破綻します。cloneの結果がスーパークラスのインスタンスにしかならないので、クラスも異なるしequalsも等値にならないのは当然の結果ですね。スーパークラスData1のcloneメソッドをsuper.clone()に修正すると、上記コードの結果はすべてtrueになります。このことを本書では説明しています。

また、@Override public Object clone()というように、cloneメソッドの可視性をpublicにしている点も重要です。適切に機能するcloneメソッドはpublicで提供することが期待されていると本書でもいわれています。

J2SE 5.0以降であれば、上記のcloneメソッドは次のように書けます。

これは共変戻り値型といって、メソッドをオーバーライドしたとき、戻り値の型はスーパークラスで定義されている戻り値の型のサブクラスでもいいという仕様です。この利点は呼び出し側でcloneメソッドを呼び出すたびにクラスキャストしなくていい点です。

すべてのフィールドが基本データ型か、不変オブジェクトへの参照であるならば、super.clone()以外の処理は必要ない

これはObjectクラスのcloneメソッドがフィールドからフィールドへの代入しか行わないことに起因します。代入するということは、オブジェクトが可変であるとき、同じ参照を代入します。上記のData1に、可変オブジェクトのフィールドを1つ追加します。

そして、こんなコードを書いてみます。

結果はこうなります。

super.clone()で呼び出しを連鎖させると、最終的にObject#clone()が呼び出されます。そのとき、Object#cloneは複製先に参照の代入を行うため、可変フィールドは複製元のフィールドと同じ参照を持ってしまいます。これを回避するには、cloneメソッドで可変オブジェクトの内容をコピーする必要があります。このとき、可変フィールドがfinal修飾子で変更不可になっていた場合、cloneメソッドはオブジェクトの内容をコピーできず、正しく機能しません。

本書ではStackとHastTableを例に、この可変フィールドの深いコピーについて説明しています。その方法をざっくりまとめると以下のようになります。

可変フィールドの深いコピーは、super.clone()を呼び出した後で行うといいそうです。だいぶ長くなっているので、cloneについてのまとめなどについては次回とします。

広告
  • LINEで送る