読者です 読者をやめる 読者になる 読者になる

@ゲー単走部

ローグライク雑記。変愚蛮怒、DCSSなど。

Java clone()メソッドによる深いコピーの方法

Java Python プログラミング

Javaでclone()メソッドの挙動を調べていたがかなり面倒くさいことがわかった。
スクリプト言語なら一行書いて終わりだぞ・・・↓

import copy

class Person:
    def __init__(self, name):
        self.name = name

class Members:
    def __init__(self, name):
        self.person = Person(name)

    def change(self, name):
        self.person.name = name

    def show(self):
        print(self.person.name)

if __name__ == '__main__':
    m1 = Members('taro')
    m2 = copy.deepcopy(m1)

    m1.change('jiro')
    m2.show()

大前提の説明。オブジェクトのコピー法として、
・m2 = m1
・m2 = copy.copy(m1)
・m2 = copy.deepcopy(m1)
の3種類がある。

m2 = m1 は、参照型変数(という呼び方はPythonではあまりしないが、実態として参照型変数、ポインタ変数なのでここではそう書いておく)m2に、参照型変数m1が参照するオブジェクトの参照値を代入するので、
結果的にm2とm1は同じオブジェクトを参照する。
だから、m1を操作すればm2にも変更が及ぶ。

m2 = copy.copy(m1)は、m1の参照するオブジェクトをコピーして、m2にその新しいオブジェクトの参照値を代入する。
それぞれ別のオブジェクトなのだから、m1の変更はm2には及ばないはずである。
だが、オブジェクトm1にはメンバー変数としてm1.personが存在し、この変数にはPerson型オブジェクトへの参照値が代入されている。
コピーの際はこの参照値をコピーするので、m1のメンバー変数m1.personとm2のメンバー変数m2.personは同じオブジェクトを参照する。
なので、m1.personを操作するとm2.personも変わってしまい、見かけ上m1の変更がm2に及んでしまう。
(m1.personが指すオブジェクトを新しいオブジェクトに変更する、なら、m2.personは変わらない)

最後のdeepcopyは完全なコピーであり、m1.personを操作しようとm2.personには影響がない。なので、taroが出力される。

で、Javaでこれを実現するには、clone()メソッドを使って実現するのだが、これがまた面倒くさい。
clone()メソッドはそのままでは浅いコピーしか実現しないので、深いコピーを実現するようにオーバーライドしてやる必要があるというのだ。

// Person.java
class Person implements Cloneable {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    public Person clone() {
        Person clone = null;
        try {
            clone = (Person) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

// Members.java

class Members implements Cloneable {
    Person p;

    Members(String name) {
        this.p = new Person(name);
    }

    void change(String name) {
        this.p.name = name;
    }

    void show() {
        System.out.println("name = " + this.p.name);
    }

    @Override
    public Members clone() {
        Members clone = null;
        try {
            clone = (Members) super.clone();
            clone.p = this.p.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }

    public static void main(String[] args) {
        Members m1 = new Members("taro");
        Members m2 = m1.clone();
        m1.change("jiro");
        m2.show();
    }
}

あらゆるクラスのご先祖クラスであるObjectクラスには、予め浅いコピーを実現するclone()メソッドが定義されている。
clone対象のオブジェクトのクラスはjava.lang.Cloneableインターフェースをimplementsしていなければならない(そうでない場合、検査例外、すなわちtry...catch必須のCloneNotSupportedExceptionが投げられる)。
また、戻り値はObject型で定義されているため、元の型の変数に代入するにはわざわざダウンキャストしてやる必要がある。

深いコピーの実現法としては、まずオブジェクトを浅いコピーしてから、各フィールドのうち参照型変数についても、再度浅いコピーをしてやる。そうすることで全部コピーされるよという理屈。

理屈はわかったし、上のコードで意図した動作にもなったのだが、他に楽な書き方がないのかどうかさっぱりわからない。

そもそもPerson.javaでclone()再定義しないといけないのか?
しなかったらなんかclone()はObjectでprotectedアクセスされますとかいう変なエラーが出るし。はぁ、なんでpublicじゃないんだ?


この言語ほんとただただめんどくさい。