[an error occurred while processing this directive]
[an error occurred while processing this directive]
Java の clone() メソッドについて
プログラムを書いてて、オブジェクトをコピーしたくなること、あると思います。
こういう時のために、Java には clone() というメソッドが用意されています。
…なんですが、継承とかインターフェースとか色々考えすぎた結果、初心者にはけっこうとっつきにくい
ものになっています。ここでは、Java の clone() の正しい書き方と、なぜこんな仕様になってしまったのかについて
書いてみます。
clone() の意味
clone メソッドでやりたいことは、「オブジェクトのコピーを作る」ことです。
でもコピーって何をしたらいいんでしょうか?
- ポインタをコピーするだけでいい?
- 配列の場合は? 要素を全部コピーしないといけないの?
- ソケットの場合は? 接続をもう一つ張るの??
これらを、設計者の意図通りに、自由に定義できるのが clone() メソッドです。
お手軽 clone()
例えばお絵かきソフトを作ってて、長方形クラスを作ったとします。
class MyRect {
int x, y, w, h;
public MyRect (int x, y, w, h)
{ this.x = x; this.y = y; this.w = w; this.h = h; }
}
このクラスをコピーするメソッドを書いてみます。
名前はとりあえず myClone() としておきます。
class MyRect {
int x, y, w, h;
public MyRect (int x, y, w, h)
{ this.x = x; this.y = y; this.w = w; this.h = h; }
public MyRect myClone(){
return new MyRect(x, y, w, h);
}
}
まず、返り値は自分と同じ MyRect 型です。中で自分と同じ x, y, w, h を引数に
コンストラクタを呼んで、MyRect 型のインスタンス(コピー)を返しています。
継承を用いないときは、まさに必要十分な実装です。
myClone() だと、どんなときに困るか
ですが、継承を使おうとすると、myClone() はちょっと困ったことが起きます。
例えば、MyRect (長方形)クラスを拡張して、色がついた ColorRect クラスを作ったとします。
class ColorRect extends MyRect {
int col;
public ColorRect(int x, int y, int w, int h, MyColor col){
super(x, y, w, h);
this.col = col;
}
}
ここで、もし ColorRect クラスが myClone() を実装しなかったら何が起こるでしょうか。
親クラス(スーパークラス・MyRect の myClone() は普通に呼び出すことが出来ます。
しかし、これが返すのはあくまで MyRect 型であって、ColorRect() 型ではありません。
つまり、
ColorRect cr = ...;
...
ColorRect cr2 = cr.myClone();
とすると、最後の行でエラーになってしまいます。
clone() の挙動は「自分と同じものを返す」はずなので、これが中途半端に使えたり使えなかったりするのは
とても危険です。
また、場合によってはコンストラクタと clone の処理が違う場合もあるかもしれません。
そのような場合は、スーパークラスの clone() を呼び出す方が自然な処理が出来ます。
このような問題を解決するために、Java の clone は設計されました。
正しい clone() の書き方
それでは、正しい作法に乗っ取った clone() を見てみましょう。
class MyRect implements Cloneable {
public MyRect clone(){
MyRect r;
try {
r = (MyRect)super.clone();
} catch (CloneNotSupportedException ce)
{ throw new RuntimeException();/* 適切なエラー処理 */}
r.set(x, y, w, h);
return r;
}
void set(int x, int y, int w, int h)
{ this.x = x; this.y = y; this.w = w; this.h = h; }
}
ポイントは以下です。
- Cloneable を implement している
- public MyRect clone(){ として定義している
( protected ではだめ、返す型は Object より MyRect のほうがよい)
- 内部で super.clone() を呼んでいる
- CloneNotSupportedException を catch している
一番のポイントは super.clone()を呼んでいるところです。MyRect は見かけ何も継承していないようですが、実は Object 型を継承しています。なので、ここでは Object#clone() が呼ばれています。この Object#clone() は、自分自身と同じ型のインスタンスを作成し、インスタンスの変数を全部コピー (memcpy) してくれます。(詳しくは下で述べます)
次に、MyRect を継承した ColorRect の clone() を見てみます。
class ColorRect extends MyRect {
MyColor col;
...
public ColorRect clone(){
ColorRect r = (ColorRect)super.clone();
r.setColor(col.clone());
return r;
}
void setColor(MyColor col)
{ this.col = col; }
}
基本的に同じですが、先ほどとはいくつか違いがあります。
- Cloneable は、すでに MyRect が implement しているので、明示的に implement する必要はありません
- public ColorRect clone(){ として定義
- 内部で super.clone() を呼んでいる
(ここでの super は、MyRect クラスになる)
- CloneNotSupportedException は MyRect で catch されているので、catch しなくてもよい
やはり一番のポイントは、ここでも super.clone() を呼んでいることです。コンストラクタを呼んではいけません。
生まれるいくつかの疑問
ここまでで、ひとまず clone の使い方の説明は終わりです。
でもこれだけでは、色々と不自然に感じられます。
- super.clone() を呼んだとき、何が起こっているのか
- なぜ CloneNotSupportedException が発生するのか
- clone() メソッドは Object にあるのに、なぜ Cloneable を implement する必要があるのか
このあたりの疑問に答えるには、Java でのオブジェクトのしくみと、Object#clone() の中身を見る必要があります。
Java のオブジェクト
一般に、オブジェクトのインスタンスは
型 + インスタンス変数で出来ています。
オブジェクトのインスタンスがいくつあっても、関数の定義とか、スタティック変数は 1 つで十分です。
これはクラスの定義のところに保持されます。これに対し、メンバ変数はインスタンスごとに変わるので、
新たにオブジェクトのインスタンス(new MyRect() のように)が作られた時に確保されます。
上の図で、「MyRect のクラス定義」と書かれたところがクラス定義です。static 変数や関数の定義はここに入っています。一つ大事なのが、親クラスの定義へのポインタ(ここでは super と書いた) です。MyRect クラスの場合は、Object クラスを指しています。
これらに対し、メンバ変数は上の横長の部分に格納されています。ここでは MyRect のインスタンス、r の領域が確保されています。ここには、まず自分が何の型かという情報が含まれています。(ここでは点線の矢印が MyRect のクラス定義に向かって生えています) その後ろに、x, y, w, h といったインスタンス変数の情報が含まれています。
次に、MyRect を継承した ColorRect の例を示します。こちらもほぼ同じですが、MyColor 型のオブジェクトへの参照が含まれているのが少し違います。メンバ変数にオブジェクトが含まれる場合は、これは直接同じ場所におかれるのではなくて、ポインタとして保持されます。(要はCの構造体のポインタ参照とと同じです)
Object#clone
Java の clone() 最大のポイントは、Object クラスの clone() メソッドと、Cloneable インターフェースです。
まずは、Java の clone() メソッドを見てみます。
このメソッドの定義は以下です。
public class Object {
protected Object clone() throws CloneNotSupportedException {
...
}
}
メソッドの動作は、二つあります。
- このメソッドを呼び出したのと同じ型のインスタンスを生成
- すべてのインスタンス変数を memcpy
(int や double は値コピー、オブジェクトは参照のみがコピーされる)
こちらも絵を書いてみました。
ColorRect のインスタンスから Object#clone () が呼ばれると、まず ColorRect 型の変数が作られ、
さらにすべての変数が memcpy されます。MyColor への参照が含まれていますが、
これも「字面通り」コピーされるので、単にアドレスがコピーされるだけです。
コピーされている様子を見るために、Object#clone() の実装を見てみましょう。
今回は厳密な java ではないのですが、文法的にはJava 互換の Dalvik のソースコードを見てみます。
呼び出し関係:
dalvik/libcore/luni-kernel/src/main/java/java/lang/Object.java
vm/native/java_lang_Object.c :: Dalvik_java_lang_Object_internalClone
vm/alloc/Alloc.c :: vm/alloc/Alloc.c
vm/alloc/Alloc.c
---
Object* dvmCloneObject(Object* obj){
Object* copy;
int size = dvmObjectSizeInHeap(obj);
copy = dvmMalloc(size, flags);
memcpy(copy, obj, size);
return copy;
}
オブジェクトのサイズ分だけ memcpy が行われていることが分かります。
Object#clone() の不思議
しかし、この Object#clone() 、少し不思議な定義になっています。
定義をもう一度載せてみます。
public class Object {
protected Object clone() throws CloneNotSupportedException {...
- protected なので、そのままではインスタンスの外部から呼べない
- 動作がとても明確なのに、CloneNotSupportedException が throw されると定義されている
この理由を探るために、Object クラスのソースを見てみます。
//[dalvik/libcore/luni-kernel/src/main/java/java/lang/Object.java]
public class Object {
...
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class doesn't implement Cloneable");
}
return internalClone((Cloneable) this);
}
private native Object internalClone(Cloneable o);
...
}
Cloneable のインスタンスで無い場合、いきなり "throw" で、例外が投げられて終了していまいます。
つまり、Object#clone() は実装されているにもかかわらず、そのままでは生殺し状態で使えないのです。
clone() を使うには、
- protected を解除
- Cloneable インターフェースを実装
する必要があると分かりました。では、Cloneable インターフェースにはどんなメソッドが含まれているのでしょうか?
Clonebale インターフェースを見てみます。
public interface Cloneable {
}
なんと空っぽです。関数は一つも定義されていません。
結局、Cloneable インターフェースは、「clone() を使う」という宣言をするためだけに存在しているわけです。
このような使い方を「マーカーインターフェース」と呼びます。
ついでに、Object#clone() の実装を見てみる (ただし Dalvik)
ついでに、Obkject#clone() を見てみます。要は、オブジェクトのサイズ分だけ malloc し、memcpy しているだけです。簡単ですね。
vm/alloc/Alloc.c
...
Object* dvmCloneObject(Object* obj)
{
Object* copy;
int size;
// サイズ取得
size = dvmObjectSizeInHeap(obj);
// malloc している
copy = dvmMalloc(size, flags);
// memcpy (コピー)
memcpy(copy, obj, size);
DVM_LOCK_INIT(©->lock);
return copy;
}
なぜこんなことになっているのか
おそらく、Java の clone() を設計した人は、本来ならこうしたかったのだと思います。
- Object クラスは clone() メソッドを持たない
- Cloneable クラスに clone() が public で実装されている
Cloneable を継承しないと、そもそも clone() が存在しないし、protected や変な例外も起こらないし、
とても素直です。
しかし、Java では 多重継承がサポートされていないため、Cloneable をクラスにしてしまうと、とても不便です。
ほかのクラスを継承しつつ clone() を使うことが不可能になってしまいます。
このため、Cloneable はインターフェースにする必要があったのですが、それだと今度は clone() の実装を
書くことが出来ません。また、clone() の操作はオブジェクト管理の根幹に関わることなので、むしろ
Object クラスの中に書くべき…という意図もあるのかもしれません。
まとめ
Java の clone() の体系は、一見不可解に見えますが、背景を知るととても興味深いです。
ただ、文法的なきれいさを求めたため、かえって知らない人の混乱を招いている気もします。
このストイックなのかごちゃ混ぜなのか分からない感じが Java っぽくて好きです。
(でも、例えば Python だともっととっつきやすいですけど…)
[an error occurred while processing this directive]