The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目11:clone を注意してオーバーライドする

本項目では正しく機能する clone メソッドを、どのように、いつ実装するかを議論する。

Cloneable インタフェース

Cloneable インタフェースはオブジェクトが複製を許可していることを示す。 ただし、Cloneable インタフェース自体は空のインタフェースである。

Cloneable インタフェースを実装したクラスは Object.clone メソッドの振る舞いが変わる。 Cloneable インタフェースを実装していないクラスの clone メソッドを呼び出すと CloneNotSupportedException がスローされる。 一方、Cloneable インタフェースを実装したクラスでは、そのオブジェクトのフィールドをコピーした新たなインスタンスを返す。

このように、インターフェスを実装しているかどうかでメソッドの振る舞いを変える方法は、インタフェースの異常な使い方であり真似をするべきではない。

Cloneable インタフェースを実装するためには、複雑で、強制するのが不可能で、ドキュメントに書かれていないような規約に従う必要がある。

clone の一般契約

clone はオブジェクトのコピーを生成して返すが、「コピー」の正確な意味はクラスに依存している。 この「コピー」の際、クラスのコンストラクタは呼び出されない。

clone の一般契約の意図は、x.clone () != x が true であり、x.clone().getClass() == x.getClass() も true であるインスタンスを生成すること。 ただし、これらの項目は絶対的なものではなく、x.clone().equals(x) は通常 true である。

clone メソッドではクラスが final ならば、オブジェクトをコンストラクタによって生成して clone の値として返すことができる。 ただし、もし、あるサブクラスで super.clone を呼び出したら、返されるオブジェクトはそのサブクラスのインスタンスであるべき。 この場合、コンストラクタによって生成したオブジェクトを返すと、サブクラスのインスタンスを返すことは出来ない。

そのため、final ではないクラスで clone メソッドをオーバーライドするならば、super.clone によって得られたオブジェクトを返すべき。 スーパークラスがこの規則に従うならば、最終的には Object.clone メソッドが呼び出され、正しいサブクラスのインスタンスが返る。

実装方法

項目9の PhoneNumber クラスのような、フィールドがすべて基本データ型か不変オブジェクトの参照のような場合は以下の様に実装する。

  • protected の Object.clone() を public のアクセッサにする
  • CloneNotSupportedException のスロー宣言を外す
@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // 発生するはずがない
    } 
}

上記のメソッドはリリース 1.5 からは PhoneNumber を返すことができる。 これは共変戻り値型が導入されたためで、いいかえれば、オーバーライドしているメソッドの戻り値型が、オーバーライドされたメソッドの戻り値型のサブタイプであることができる。

Object.clone() は Object を返すので、PhoneNumber.clone は super.clone() の結果を返す前にキャストしなければならない。

可変オブジェクトの参照がある場合

可変オブジェクトへの参照を保持しているクラスの場合、参照先を「深いコピー」しなければならない。 例えば項目6の Stack クラスを考える。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    // 省略
}

このクラスを clone によって複製可能にする場合、単に super.clone() の結果を返すだけでは、elements の参照先がコピーしたオブジェクトと共有される。 そのため、状態の不整合が発生するが、これはコンストラクタ Stack を呼び出した場合には発生しない。

このように、clone メソッドはもう一つのコンストラクタとして機能している。 そのため、clone では、元のオブジェクトに何も害を及ぼしてないこと、複製先に対して不変式を適切に確立していることを保証しなければならない。

Stack に関する正しい clone の実装は以下の通り。

@Override
public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            // 配列の clone は同じ型を返すのでキャストは必要なし
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

もし、elements が final である場合にはこの方法は上手くいかない。 clone では final に対する代入は許可されていないためである。 すなわち、clone のアーキテクチャは可変オブジェクトを参照する final フィールドとは両立しないことを意味している。

clone の代替手段

このように正しく機能する clone を実装するのは手間がかかる。

そのため、オブジェクトのコピーを行う何からの代替手段を提供するか、オブジェクトの複製を単に提供しない方が賢明である。 たとえば、コピーコンストラクタやコピーファクトリーを提供する方法が考えられる。

public Stack(Stack orig) {
        this.size = orig.size;
        this.elements = orig.elements.clone();
    }

    public static Stack newInstance(Stack orig) {
        Stack s = new Stack();
        s.size = orig.size;
        s.elements = orig.elements.clone();
        return s;
    }

コピーコンストラクタやコピー static ファクトリーメソッドは Cloneable/clone アーキテクチャよりも多くの長所を持っている。 言語外のオブジェクト生成の仕組みに依存していませんし、final フィールドの使われ方と相反することがない。 また、不要な例外もスローしないし、キャストも必要としない。

コピーコンストラクタや static ファクトリーメソッドは、インタフェース内にいれることはできないという欠点を持っている。 ただし、そもそも Cloneable は clone メソッドを保持しておらず、通常のインタフェースとして機能していないので、この欠点は Cloneable との対比においては意味はない。

感想

長かった。最初にいろいろ書いて、長いと思ってだいぶ削ってもこの長さ。

Object.clone の実装を見てみようと思って、Object.java を見てみたら Native で実装されてるようだった。

GC: Object - java.lang.Object (.java) - GrepCode Class Source

 protected native Object clone() throws CloneNotSupportedException;

この先が気になったので、native の方のソースを確認してみた。

jdk7/jdk7/hotspot: 9b0ca45cd756 src/share/vm/prims/jvm.cpp

// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}

cloneable じゃなければ、CloneNotSupportedException がスローされていることが分かる。

ちなみに以下の投稿が情報の元ネタ。Stack Overflow 様様である。 stackoverflow.com

(c) The King's Museum