- PR -

Object クラスの clone メソッドが protected なので使いづらい

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-03-19 16:39
unibon です。こんにちわ。

Java2 の Collection クラス(List/Set/Map など)を使う場合、
Object クラスの clone メソッドが protected なので、
使いづらいと感じるときがあります。
なぜ、protected なのでしょうか。public ではマズいのでしょうか、
という疑問があります。
#なお、とくに Collection クラスだからというわけではありませんが、一例として。

たとえばつぎのようなサンプルプログラムで、
フィールドに HashSet でも TreeSet でも
どちらでも使えるようにしようと目論んだとします。
そして、深いコピーをサポートしたいとします。

--- ここから(MyClass.java) ---
コード:
import java.util.*;

public class MyClass implements Cloneable {

    private Set set = new HashSet(); // 今のところ HashSet だが将来は TreeSet にもしたい。

    public Object clone() {
        try {
            MyClass o = (MyClass) super.clone();
            // o.set = (Set) set.clone(); // (1) こうしたいがコンパイルエラーになる。
            o.set = (Set) ((HashSet) set).clone(); // (2) これならうまくいくがキャストが必要。
            return o;
        } catch (CloneNotSupportedException ex) {
            throw new InternalError();
        }
    }
    
    public static void main(String[] args) {
        MyClass a = new MyClass();
        MyClass b = (MyClass) a.clone();
    }
}


--- ここまで ---

本当は上記(1)のようにしたいのですが、コンパイルエラー、
"The method clone() from the type java.lang.Object is not visible"
になってしまいます。
そのため上記(2)のようにせざるを得ないのですが、
フィールドに格納する実行時の型を HashSet から TreeSet に変更する場合は、
この MyClass クラスの clone メソッドの中のコードを
いちいち書き換える必要が出てきます。
Object クラスの clone メソッドが public ならすべてよし、
のような気がするのですが、なぜそうなっていないのでしょうか。
また、Java のこの仕様のままでやりくりするとした場合
(当然、選択肢はそれしか残っていませんが)、
上記のサンプルプログラムは、どう書くのがキレイなのでしょうか。

なお、コレクションクラスにおける継承の、
Object ← AbstractCollection ← AbstractSet ← HashSet/TreeSet
の関係の中のどこか途中で clone が public でオーバライドされていれば、
フィールドの宣言の型にそれが使えて良いのかもしれませんが、
あいにくそうなっていません。

インターネット上で検索すると、
http://java-house.jp/ml/topics/topics.html#language-clone
などがヒットしましたが、
結局なぜ protected である必然性があるのかは良く分かりませんでした。

漠然とですが、とくに、equals メソッドなどは public なのに、
clone がそうでないのは、釣り合いがとれていないように感じます。
clone に代替するものを clone 以外で実現できないのだから、
clone はいつでも使えるように public になっていたほうが良いと思うのですが。
asip
ベテラン
会議室デビュー日: 2001/12/27
投稿数: 77
投稿日時: 2003-03-19 17:27
結論からいうとあなたの主張は間違っています。
cloneメソッドは常にpublicとしてみえている必要がありますか?
あなたの現在の利用形態では"cloneメソッドはpublicであるべき"かもしれませんが
Objectクラスは全てのクラスの親クラスです。cloneメソッドを利用しないクラスが
大半です。しかもcloneメソッドはデフォルトでは"浅いコピー"しかサポートしません。
作成するクラス全てで"浅いコピー"を回避するためにcloneメソッドをわざわざ
オーバーライドするのは煩わしいです。
前言撤回(たぶん、cloneメソッドがpublicでないのは同じような理由でしょう)。
私はcloneメソッドを使わず、コピーコンストラクタを使っています。
コピーコンストラクタという言葉はJava界隈ではあまり使われませんが、C++界隈では
日常的に使われています。
コード:

import java.util.*;
public class MyClass implements Cloneable {
private Set set;
public Object clone() {
try {
MyClass o = (MyClass) super.clone();
// これでOK
if(set instanceof HashSet){
o.set = new HashSet(set);
}
if(set instanceof TreeSet){
o.set = new TreeSet(set);
}
return o;
} catch (CloneNotSupportedException ex) {
throw new InternalError();
}
}
public static void main(String[] args) {
MyClass a = new MyClass();
MyClass b = (MyClass) a.clone();
}
}



[ メッセージ編集済み 編集者: asip 編集日時 2003-03-19 20:55 ]
t-wata
大ベテラン
会議室デビュー日: 2002/07/12
投稿数: 209
お住まい・勤務地: 東京
投稿日時: 2003-03-19 18:03
http://developer.java.sun.com/developer/JDCTechTips/2001/tt0306.html
このTipsが参考になります
未記入
ぬし
会議室デビュー日: 2002/03/28
投稿数: 255
投稿日時: 2003-03-19 20:20
>結論からいうとあなたの主張は間違っています。
>cloneメソッドは常にpublicとしてみえている必要がありますか?
というより,「Objectクラスのインスタンスはcloneできない」ってのが
正しいかと.cloneできないクラスもありますし,cloneを禁止する
クラスもあります.全てのクラス,インターフェースの共通の
スーパークラスであるObjectクラスがcloneできないのは当然
でしょう.

ただし,javaのclone()まわりの設計がイマイチというのは
まあ分かる気はします.理由は多分,全然違いますが.
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-03-20 11:43
unibon です。こんにちわ。

引用:

asipさんの書き込み (2003-03-19 17:27) より:
作成するクラス全てで"浅いコピー"を回避するためにcloneメソッドをわざわざ
オーバーライドするのは煩わしいです。



浅いコピーを回避するためにオーバライドは必須ではないと思います。
浅いコピーを回避したい(結局は意図しない clone をされることを防ぎたい)ならば、
implements Cloneable さえしなければ、
実行時に CloneNotSupportedException が出るので、
それで良いように思います。

たとえばつぎのサンプルプログラムを考えてみました。

コード:
import java.util.*;

public class CloneTester2 /* implements Cloneable */ {

    public static void main(String[] args) {
        try {
            CloneTester2 a = new CloneTester2();
            CloneTester2 b = (CloneTester2) a.clone();
        } catch (CloneNotSupportedException ex) {
            ex.printStackTrace();
        }
    }
}



このとおりに implements Cloneable をコメントアウトのままにしておくと、
実行時には
java.lang.CloneNotSupportedException: CloneTester2
at java.lang.Object.clone(Native Method)
at CloneTester2.main(CloneTester2.java
という例外が発生します。
#(BBコードと重複するのでコロンを空白にしました。)

ちなみに、すこし脇にそれますが、
このサンプルで implements Cloneable を有効にすると、
Object.clone が呼べて、浅いコピーができますが、
これは同じクラス内の static メソッドだからできることらしく、
こうするとあたかも Object.clone が public であるかのように
模倣できていると思っています。
#私のこの理解は正しいでしょうか?

話は戻って、上記のサンプルプログラムのように、
意図しない clone を実行時になら阻止できますが、
やはり、コンパイル時に阻止したいため Object.clone が
protected になっているのでしょうか。
でも、Java のアーキテクチャを見ると、clone 以外でも、
コンパイル時にはエラーが分からず、実行時にならないと分からない、
というものは多いと思いますので、これはさほど強くない理由のように感じます。


引用:

asipさんの書き込み (2003-03-19 17:27) より:
if(set instanceof HashSet){
o.set = new HashSet(set);
}
if(set instanceof TreeSet){
o.set = new TreeSet(set);
}


すみません、例では HashSet と TreeSet の2つだけしか触れませんでしたが、
それ以外に、将来 HogeSet とかも出現してそれを使いたい場合、
これだと、やはりコードに instanceof の判定を追加する必要が出てきますよね。


引用:

t-wataさんの書き込み (2003-03-19 18:03) より:
http://developer.java.sun.com/developer/JDCTechTips/2001/tt0306.html
このTipsが参考になります


拝見したのですが、なぜ protected なのかについてはあまり触れられていなかったように思います。
#私の見落としだったらすみません。


引用:

悪夢を統べるものさんの書き込み (2003-03-19 20:20) より:
というより,「Objectクラスのインスタンスはcloneできない」ってのが
正しいかと.cloneできないクラスもありますし,cloneを禁止する
クラスもあります.全てのクラス,インターフェースの共通の
スーパークラスであるObjectクラスがcloneできないのは当然
でしょう.


最初の質問の際に少し書き足りませんでしたが、
clone の使用が制限されている場合、
clone に代わる実現方法がないことに疑問を持っています。
Object.clone は native であり、フィールドを丸ごと浅いコピーしてくれますが、
これに代わるものを自分で作るとすると、
asip さんが紹介されたコピーコンストラクタのような方法になってしまいます。
しかしインスタンスを new してフィールドをコピーするよりは、
Object.clone で浅いコピーを作るほうが、コストが低く素直なように思います。
#速いから良い、というわけでは必ずしもありませんが。
ocean
会議室デビュー日: 2001/10/07
投稿数: 7
投稿日時: 2003-03-20 13:41
>浅いコピーを回避したい(結局は意図しない clone をされることを防ぎたい)ならば、
>implements Cloneable さえしなければ、
>実行時に CloneNotSupportedException が出るので、
>それで良いように思います。

これだと、サブクラスが Cloneable を implement して、
super.clone() としたときに問題になります。

import java.util.TreeSet;

class IdealObject
{
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}

// クローンして欲しくないから、 Cloneable をインプリメントしないよ!
class IHateClone extends IdealObject
{
private TreeSet set = new TreeSet(); // これは大事な物なのさ・・・
}

// いけないね・・・僕がクローン好きにしてあげよう
class ILikeClone extends IHateClone implements Cloneable
{
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}

class Me
{
public static void main(String[] args) throws CloneNotSupportedException
{
IdealObject b1 = new ILikeClone();
IdealObject b2 = (IdealObject)b1.clone(); // やったね!エラーなしだ
}
}

Java1.5で導入予定のGenericsを使えば、
class MyClass<MySet = java.util.Set> みたいな感じでスマートにいけるのかもしれません。
http://developer.java.sun.com/developer/technicalArticles/releases/generics/

現在だと、
o.set = (Set)set.getClass().getMethod("clone", null).invoke(set, null);
のような方法もありますが、速度が気になる場合は向かないかも。



[ メッセージ編集済み 編集者: ocean 編集日時 2003-03-20 13:42 ]

[ メッセージ編集済み 編集者: ocean 編集日時 2003-03-20 13:46 ]

[ メッセージ編集済み 編集者: ocean 編集日時 2003-03-20 13:47 ]
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-03-20 16:30
unibon です。こんにちわ。

引用:

oceanさんの書き込み (2003-03-20 13:41) より:
>浅いコピーを回避したい(結局は意図しない clone をされることを防ぎたい)ならば、
>implements Cloneable さえしなければ、
>実行時に CloneNotSupportedException が出るので、
>それで良いように思います。

これだと、サブクラスが Cloneable を implement して、
super.clone() としたときに問題になります。


oceanさんが書かれたサンプルプログラムだと、
Object.clone が protected のままでも「問題」が起きているんですよね。
ということはこの問題は Object.clone の protected の問題ではなく
Cloneable の問題であり、protected の問題とは直接の関係はない、
私が Cloneable で制御できると書いたからそれへの反例を示されたまで、
という理解で合っているでしょうか。
#関係ないから議論とは関係ない、という意味ではまったくありませんので。単に自分の頭を整理したいためです。


引用:

oceanさんの書き込み (2003-03-20 13:41) より:
Java1.5で導入予定のGenericsを使えば、
class MyClass<MySet = java.util.Set> みたいな感じでスマートにいけるのかもしれません。
http://developer.java.sun.com/developer/technicalArticles/releases/generics/

現在だと、
o.set = (Set)set.getClass().getMethod("clone", null).invoke(set, null);
のような方法もありますが、速度が気になる場合は向かないかも。


いろいろとありがとうございます。
Generics はまだ使ったことがないので、使い勝手が良く分かっていません。
また、リフレクションは使いこなすのが難しそうですね。
ocean
会議室デビュー日: 2001/10/07
投稿数: 7
投稿日時: 2003-03-20 17:09
こんにちは。
引用:

unibonさんの書き込み (2003-03-20 16:30) より:
この問題は Object.clone の protected の問題ではなく
Cloneable の問題であり、protected の問題とは直接の関係はない、
私が Cloneable で制御できると書いたからそれへの反例を示されたまで、
という理解で合っているでしょうか。


そうです、私も考えを整理するのに時間がかかったのですが、
1.protectedである必然性はない。
2.Cloneableには問題がある。
の両者は独立していると思います。

protectedについては、どうもデフォルトの動作がランタイムエラーなので
親切でコンパイルエラーにしているようにしか見えません。
unibonさんのおっしゃるように、clone()はpublicであるべきだと思います。

これ以下の文章は、protected とは完全に無関係ですが、よろしければ見てください。

考えた末、私はCloneableがインターフェースなのはおかしいと結論しました。
親クラスがクローン不可能なのに、子クラスがクローン可能なのは変です。
むしろ、NotCloneableがインターフェースであるのが自然です。
なぜなら、親クラスがクローン可能なのに子クラスでクローンできなくなるのは
ありうるからです。

私は主にC++を使うのですが、リソースなどコピーできないメンバ変数を
持つクラスは、コピーコンストラクタと代入演算子をprivateに定義して
コピーできなくします。このクラスを継承しても、親クラスがコピー不可能
なので、継承したクラスも自動的にコピー不可能になります。

なので、Cloneableインターフェースは、継承の原則[is a]に反しているように
思います。Javaのような洗練された言語で、こんな設計ミスをするとは思いにくい
のですが・・・

// 望ましいコード?

// 適切なclone()を実装できないクラスでは、これをインプリメントすること。
public interface NotCloneable
{
}

public Object
{
public Object clone() throws CloneNotSupportedException
{
if (this instanceof NotCloneable)
{
throw new CloneNotSupportedException();
}
else
{
return doClone();
}
}

private native Object doClone();
}


[ メッセージ編集済み 編集者: ocean 編集日時 2003-03-20 17:27 ]

[ メッセージ編集済み 編集者: ocean 編集日時 2003-03-20 17:29 ]

スキルアップ/キャリアアップ(JOB@IT)