clone、およびCloneableについて(主に欠陥について)、詳しく記載しています。
正直、記載内容は全部は読まなくても良いのではないかと思います・・・
Cloneableの是非に記載した内容は、EffectiveJavaでは項目の末尾に記載されていますが、
一番重要な内容であるため、最初に記載しました。
その内容を呼んでも、Cloneableについて知りたい方は、その続きを読んでください。
興味が無い方は、考察まで飛んでください。
記載内容
Cloneableの是非
clone()について複雑なルール、注意点を記載するが、そこまでclone()によるコピーが必要な状況はまれである。
残念ながら、すでにCloneableを実装しているクラスを拡張するのであれば、
正しいclone()を実装する以外の選択肢はほとんどない。
それ以外の状況であれば、オブジェクトのコピーを行う、代替手段を提供する方が良い。
オブジェクトをコピーする良い方法は、
コピーコンストラクタ、コピーファクトリメソッドを提供することである。
Cloneableの多くの問題を考えると、新たなインタフェースはCloneableを拡張するべきではないし、
新たな拡張可能なクラスはCloneableを実装するべきではない。
finalのクラスがCloneableを実装することに害はないが、パフォーマンスに対する最適化とみなし、
それが正当化されるまれな場合のみにするべきである。
Cloneableの欠陥
Cloneableは、クラスが複製であることを示すことを意図しているが、その目的は果たせていない。
最大の欠陥は、Cloneableインタフェースがclone()を定義しておらず、Object.clone()がprotectedなことである。
このため、リフレクションを使わないと、Cloneable変数からclone()を呼び出すことが出来ない。
Cloneableの意味は、実装することでObject.clone()の動作を変更することである。
Object.clone()は、
クラスがCloneableを実装している場合、自身のコピーを返し、
実装していない場合、CloneNotSupportedExceptionをスローする。
これはインタフェースの異常な使い方であり、真似するべきではない。
clone()の一般契約は、Cloneableではなく、Object.cloneのJavaDocに記載されている。
(ここでは割愛)
Cloneableを実装しているクラスは、適切に動作するpublicなclone()を実装することが期待されているが、
そのためには、そのクラスとすべてのスーパークラスが、
複雑で、強制するのが不可能で、ドキュメントにほとんど記載されていない規約に従わなければならい。
clone()の実装方法
前提として、親クラスが正しく動作するclone()を提供している場合、
最初にsuper.clone()を呼び出すべきである。
クラスが可変オブジェクトを参照するフィールドを保持していない場合
そのクラスのすべてのフィールドが基本データ型か、不変クラスへの参照であれば、
super.clone()の戻り値をキャストして、clone()の戻り値とすればよい。
ただし、クラスが、インスタンスごとに一意なIDをフィールドとして保持しているような場合、
clone()はただフィールドをコピーするだけでななく、そのフィールドを修正する必要が有る。
クラスが可変オブジェクトを参照するフィールドを保持している場合
もし、クラスが可変オブジェクトを参照するフィールドを保持しており、
その可変オブジェクトを複数のインスタンスで共有できない場合、
解決方法は、その可変オブジェクトに対して再帰的にclone()を呼び出し、フィールドをclone()の戻り値で更新することである。
ただし、この方法は可変オブジェクトを参照するフィールドがfinalの場合は、うまくいかない。
これは根本的な問題であり、final修飾子を外す必要が有るかもしれない。
クラスが可変オブジェクトを要素とする配列を保持している場合
クラスが配列を保持しており、その配列の各要素を共有できない場合、
配列に対するclone()呼び出しでは不十分であり、全配列要素に対してdeep copyが必要になる。
最後の手段
複雑なオブジェクトを複製する最後の手段は、super.clone()を呼び出し、
返されたオブジェクトの全フィールドを初期状態に戻し、
複製元のオブジェクトの状態を再現するために、高いレベルのメソッドを呼び出すことである。
しかし、この方法は、複製先のフィールドを直接更新する方法と比べると遅く、
また、Cloneableアーキテクチャの基本となる、フィールドの自動的なコピーとは、正反対の方法である。
clone()実装時のその他の注意事項
もしクラス自体が不変クラスであれば、clone()を提供するべきではない。
clone()は、無意味なコピーを行うだけである。
clone()は、コンストラクタと同様に、生成中の複製先に対して、オーバーライド可能なメソッドを呼び出してはならない。
これは、そのメソッドがオーバーライドされた場合に、複製先が不正な状態でサブクラスのメソッドが呼ばれることになり、
複製先と複製元の破損につながる可能性が高くなるためである。
Object.clone()は、CloneNotSupportedExceptionをスローすると宣言されているが、
オーバーライドしているメソッドでは、throws節を削除するべきである。
(実際にはCloneNotSupportedExceptionをスローすることはなく、使用側はthrows節が無い方が使いやすいため)
スレッドセーフなクラスでは、clone()も排他制御が必要になる可能性がある。
継承されるクラスに対するCloneableの扱い
継承されるようにクラスを設計する場合、以下の選択肢がある。
(どちらを選ぶ場合でも、継承元のクラスはCloneableを実装するべきではない)
適切に機能する、protectedのclone()を実装する
このclone()では、最初にsuper.cloneを呼び出し
(サブクラスがCloneableを実装していない場合は、この時点でCloneNotSupportedExceptionがスローされる)
可変オブジェクトのフィールドのコピー等を適切に行う。
サブクラスのclone()実装を不可能にする
以下の様なclone()を提供することで、サブクラスがclone()を実装することを防ぐ
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
考察
長い項目ですが、ひたすらCloneableの複雑さ、デメリットについて書いています。
CloneableとSerializable
この2つは、初期のJavaの設計における大きな失敗と思います。
(Serializableについても、項目85で盛大にディスっています)
どちらも、クラスツリーの上位で実装(拡張)してしまうと、
そのクラスツリー全体が汚染されてしまい、末端のクラスまで性質を引き継ぐ必要が出てしまいます。
もはや、クラスの拡張で機能追加を図ること自体が時代遅れとは思いますが、
既存のクラスがCloneableやSerializableを実装している場合、拡張時の大きな制約になります。
配列の防御コピー
唯一といってよい、clone()のありがたみがあるケースと思います。
コンストラクタ等で、引数の防御コピーを行う場合、
配列に対してclone()を呼ぶのが簡単で良いです。
(要素が不変クラスである場合に限りますが)
コピーコンストラクタ、コピーメソッド
記載してある通り、
可変クラスに対して、コピーを作成する必要が有る場合、クラスにこれらの機能を追加するのが良いです。
ただ、「項目17 可変性を最小限にする」にある通り、可能な限り値の保持を不変クラスで行えば、
インスタンスのコピー自体が、多くの場合不要と思います。
サブクラスがcloneを実装不可能にする
ここまでやる必要は無いのではないかと思いますが、
利用者を信頼できないのであれば、実施するべきなのかもしれません。
業務での開発でここまでやる必要はないと信じたいですが・・・
いつの間にか、サブクラスでclone()を実装されるのを防ぐには
必要かもしれませんが、
そこまでしたところで、このclone()を削除されたら意味が無いわけで・・・