まくまくJavaノート
clone を注意してオーバライドする
2016-07-08

あるクラスで clone() メソッドをサポートするときには、Cloneable インタフェースを implements した上で clone() メソッドをオーバライドする必要があります。

clone を実装するときのポイント

  • コピーコンストラクタ (public MyClass(MyClass obj)) の導入で済ませられないか検討する。Cloneableimplements したクラスを拡張するケース以外では、コピーコンストラクタやコピーファクトリを作成した方がシンプルに実装できることが多い。
  • clone() を実装するクラスでは、implements Cloneable と宣言し、自分自身のクラス型のオブジェクトを返す public な clone() メソッドを実装する
  • clone() メソッドの実装では super.clone() を使用し、シャロー・コピーで問題となるフィールドがある場合は、そのフィールドをディープ・コピーする

Cloneable インタフェースが意味するもの

Cloneable インタフェースを implements しているクラスは、clone() メソッドの呼び出しをサポートしていることを示します(CloneNotSupportedException 例外をスローせずに、正しくクローン処理が行われるということ)。 clone() メソッドは Cloneable インタフェースで定義されているのではなく、実際には Object クラスで定義されている protected メソッドです。 サブクラスで Cloneable メソッドが implements されているかどうかによって、この Object#clone() の振る舞いが変化するように実装されています。

Object#clone() の実装

class Object {
    ...

    protected Object clone() throws CloneNotSupportedException {
        if (!(this instanceof Cloneable)) {
            throw new CloneNotSupportedException("Class " + getClass().getName() +
                                                    " doesn't implement Cloneable");
        }
        return internalClone();  // シャロー・コピーを実施
    }

    private native Object internalClone();
}

具体的には、Cloneable インタフェースの implements の有無によって、Object#clone() の動作が下記のように変化します。

  1. Cloneable を implements していないクラスで clone() を呼び出した場合は、CloneNotSupportedException を投げる
  2. Cloneable を implements しているクラスで clone() を呼び出した場合は、各フィールドをシャロー・コピーする

このように親クラスの振る舞いを変化させるためにインタフェースを使用するという設計は極めて特殊であり、Cloneable インタフェースが嫌われている理由のひとつになっています。

clone() の仕様/一般契約

Object (Java Platform SE 8) に、clone() は下記のような結果を想定すると記述されています。

  1. x.clone() != x (クローンしたオブジェクトは異なるメモリ領域に確保されているということ)
  2. x.clone().getClass() == x.getClass() (クローンしたオブジェクトは同じ型であること)
  3. x.clone().equals(x) (クローンしたオブジェクトは論理的に同じ値を持つこと)

2 と 3 は絶対要件ではないと記述されているので、clone() メソッドの振る舞いがどうあるべきかは、最終的にはそのクラスの仕様次第ということになります。 「クローン」すると言うからには、通常は上記の条件をすべて満たすように実装されていて欲しいところですが、equals() メソッドを正しく実装するのはなかなか骨の折れる作業ですので、3 に関してはサポートしないケースもあるでしょう(equals() が実装されていない場合は、デフォルトで同じ参照かどうかを調べるだけの振る舞いになりますので、3 の評価結果は偽 (false) となります)。

clone() の実装方法

フィールドにプリミティブ型、あるいは immutable なオブジェクトしか持たないクラスの場合

clone() メソッドを実装する場合、外部から呼び出しを可能にするために、可視性は public にします。 Object#clone() の実装では、オブジェクトのフィールドがシャロー・コピーされるようになっているため、プリミティブな型の値や、immutable なオブジェクトへの参照しかフィールドとして持たないクラスの clone() メソッドは、単純に super.clone() を呼び出すことによって完結できます(Object#clone() の実行にたどり着くために、すべてのスーパークラスが super.clone() を使用して実装しているという前提が必要です)。

public class Stack implements Cloneable {
    private int x = 0;  // Primitive 型
    private int y = 0;  // Primitive 型
    private String text = "hello";  // Immutable なオブジェクト

    ...

    @Override public TextView clone() {
        try {
            return (TextView) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();  // Should not happen
        }
    }
}

また、J2SE 1.5 からは共変戻り値型の仕組みが取り入れられたため、戻り値は自分自身のクラスの型にキャストしてから返すことができます。共変戻り値型というのは、戻り値の型を、その型のサブタイプにしてオーバライドすることのできる仕組みです。これが導入される前は、clone() の戻り値はすべて Object 型で定義しなくてはならず、呼び出し側でキャストして使用しなければいけませんでした。

ディープ・コピーが必要なフィールドを持つクラスの場合

immutable でないオブジェクトへの参照をフィールドで保持しているようなクラスで clone() メソッドをサポートする場合は、それらのフィールドをディープ・コピーする処理を記述する必要があります。

public class Stack implements Cloneable {
    private int size = 0;
    private Object[] elements;

    ...

    @Override public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();  // コピー元と同じ参照を持たないようにする
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();  // Should not happen
        }
    }
}
2016-07-08