第6章 Prototype - 増補改訂版Java言語で学ぶデザインパターン入門

第6章は、Prototypeパターン。扉絵がかわいい。

幸か不幸か、自分はcloneメソッドを一切使わない環境にいるので、まずそこから勉強する必要がありました。というわけで、復習メモ。

Prototypeパターンとは

インスタンスをコピーして、別のインスタンスを作成します。コピーには、java.lang.Object.clone()を使います。

Prototypeパターンを使うメリットは、クラス名をクライアントコードに書かずに済むことです。これは、クラス名を指定してインスタンスを生成するのではないためです。

どういうときに使うのか

本文で言及されるのは、次の3つの状況です。

  • 作りたい種類が多すぎる場合

クラスの数が多すぎて管理しにくくなる場合に、Prototypeパターンを使うという話が出てきます。

個人的には、Prototypeパターンでインスタンスの管理をするくらいなら、クラスが増えた方がマシなんですが…(まだ本当に困る状況に出会っていないだけかも?)

  • クラスからコピーを作るのが大変なとき

インスタンスからコピーを作った方が手っ取り早い場合に、Prototypeパターンを使います。例として、ユーザ操作で作られた図形のインスタンスが挙げられています。

たしかに、図形のインスタンスを構成する値は、数も種類も不確定だから、ツラそうです。これは、理由としてまだわかります。

本文のサンプルコードでは、インスタンスを作り分けるために文字列を使用しています。これによって、コピー作成の枠組がインスタンス(の元になるクラス)に依存しないコードになっています。

コピー対象にCloneableを実装する

clone()でコピーするクラスは、マーカーインタフェースであるjava.lang.Cloneableを実装する必要があります。クラスやスーパークラスが、次のいずれかを行えばOKです。

  • 直接Cloneableを実装する
  • Cloneableを拡張したインタフェースを実装する
  • Cloneableのサブインタフェースを実装する

Cloneableを実装しないクラスにclone()を使おうとすると、CloneNotSupportExceptionが投げられます。

他のクラスからコピーできるようにする

Java言語のcloneメソッドは、自分のクラス(およびサブクラス)からしか呼び出すことができない

p.70にある上の記述どおり、clone()はprotectedメソッドです。他の(サブクラスでない)クラスからコピーを行う場合は、サンプルコードのようにpublicな別のメソッドでくるむか、clone()をpublicでオーバーライドします。

浅いコピーと深いコピーの話

clone()は、フィールドからフィールドへの浅いコピーです。コピー元に存在する参照をコピーします。インスタンスを複製する深いコピーが必要なときは、clone()をオーバライドしてあげる必要があります。もしくは、シリアライズを使う方法もあるそうです。

浅いコピーと深いコピーの違いは、次のページの図がわかりやすいです。

しかし、次のページの「深いコピー」のサンプルでclone()が使われているのをみて、いろいろとわからなくなりました。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracleを読むと、次のようにあります。

配列はすべて、インタフェース Cloneable を実装しているものと見なされることに注意してください。実装していない場合、このメソッドはこのオブジェクトのクラスの新しいインスタンスを生成し、そのフィールドをすべて、このオブジェクトの対応する各フィールドの内容で初期化します。これは代入と同様で、フィールドの内容自身が複製されるのではありません。つまりこのメソッドは、オブジェクトの「シャローコピー」を生成しますが、「ディープコピー」は生成しません。

リンク先のコードは、プリミティブ型の配列だから、clone()でディープコピーのような動きをする、という理解でいいんでしょうか。じゃあ、参照型の配列だとどうなるのか? というわけで、ちょっと改変させてもらって、試してみます。

参照変数の代入(浅いコピーですらない場合)

Substitute.java

class Clazz{
    String name = null;
    
    Clazz(String name) {
        this.name = name;
    }
    
    void setName(String name){
        this.name = name;        
    }
    
    String getName() {
        return this.name;
    }
}

public class Substitute {
    public static void main(String[] args) {
        Clazz[] clazz1 = {new Clazz("hoge"), new Clazz("piyo")};
        Clazz[] clazz2 = clazz1;
        
        // clazz1の全要素にpugyaを代入。
        for(int i = 0; i < clazz1.length; i++){
            clazz1[i].setName("pugya");
        }

        // clazz1の全要素表示。
        System.out.println("コピー元の要素");
        for(int i = 0; i < clazz1.length; i++){
            System.out.println(clazz1[i].getName());
        }
        
        // clazz2の全要素表示。
        System.out.println("コピー先の要素");
        for(int i = 0; i < clazz2.length; i++){
            System.out.println(clazz2[i].getName());
        }

        //参照先の比較。
        if(clazz1 == clazz2){
            System.out.println("同じ参照先");
        }else{
            System.out.println("別の参照先");
        }
    }
}

実行結果。

コピー元の要素
pugya
pugya
コピー先の要素
pugya
pugya
同じ参照先

Clazz配列参照変数clazz2にClazz配列参照変数clazz1を代入した結果、clazz1とclazz2は、同じClazz[]配列オブジェクトを参照します。図にすると、こんなかんじです。

ちなみに、学生の頃に読んだJavaの参考書にでてくる図が、こんなかんじでした。この本では、cloneメソッドについては触れられていなかったような気がします。

「浅いコピー」配列版

ShallowCopy2.java

class Clazz implements Cloneable{
    String name = null;
    
    Clazz(String name) {
        this.name = name;
    }
    
    void setName(String name){
        this.name = name;        
    }
    
    String getName() {
        return this.name;
    }
}

public class ShallowCopy2 {
    public static void main(String[] args) {
        Clazz[] clazz1 = {new Clazz("hoge"), new Clazz("piyo")};
        Clazz[] clazz2 = clazz1.clone();
        
        // clazz1の全要素にpugyaを代入。
        for(int i = 0; i < clazz1.length; i++){
            clazz1[i].setName("pugya");
        }

        // clazz1の全要素表示。
        System.out.println("コピー元の要素");
        for(int i = 0; i < clazz1.length; i++){
            System.out.println(clazz1[i].getName());
        }
        
        // clazz2の全要素表示。
        System.out.println("コピー先の要素");
        for(int i = 0; i < clazz2.length; i++){
            System.out.println(clazz2[i].getName());
        }

        //参照先の比較。
        if(clazz1 == clazz2){
            System.out.println("同じ参照先");
        }else{
            System.out.println("別の参照先");
        }
    }
}

実行結果。

コピー元の要素
pugya
pugya
コピー先の要素
pugya
pugya
別の参照先

clazz1とclazz2は、別のClazz[]配列オブジェクトを参照します。しかし、それぞれの配列オブジェクトは、ヒープ領域に存在する同じClazzオブジェクトを参照しています。自分の理解では、図のようなかんじですが…(自信ないです)


「深いコピー」配列版

配列要素を1個1個コピーします。

TrueDeepCopy.java

class Clazz implements Cloneable {
    String name = null;

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

    void setName(String name) {
        this.name = name;
    }

    String getName() {
        return this.name;
    }

    public Object clone() {
        Clazz c = new Clazz(this.name);
        return c;
    }
}

public class TrueDeepCopy {
    public static void main(String[] args) {
        Clazz[] clazz1 = { new Clazz("hoge"), new Clazz("piyo") };
        Clazz[] clazz2 = { 
                (Clazz) clazz1[0].clone(),
                (Clazz) clazz1[1].clone() };

        // clazz1の全要素にpugyaを代入。
        for (int i = 0; i < clazz1.length; i++) {
            clazz1[i].setName("pugya");
        }

        // clazz1の全要素表示。
        System.out.println("コピー元の要素");
        for (int i = 0; i < clazz1.length; i++) {
            System.out.println(clazz1[i].getName());
        }

        // clazz2の全要素表示。
        System.out.println("コピー先の要素");
        for (int i = 0; i < clazz2.length; i++) {
            System.out.println(clazz2[i].getName());
        }

        // 参照先の比較。
        if (clazz1 == clazz2) {
            System.out.println("同じ参照先");
        } else {
            System.out.println("別の参照先");
        }
    }
}

実行結果。

コピー元の要素
pugya
pugya
コピー先の要素
hoge
piyo
別の参照先

clazz1とclazz2は、別のClazz[]配列オブジェクトを参照します。そして、それぞれの配列オブジェクトは、異なるClazzオブジェクトを参照しています。平たくいうと、配列の各要素がコピーされています。図にすると、こんなかんじです。

clazz1とclazz2が参照するオブジェクトが別物だから、clazz1にpugyaを代入した後も、clazz2のhogeとpiyoは無事元のままです。

本文のサンプルコードについて

p.73のList6-5にあるMainクラスのmain()では、Managerが持つHashMapに、Productの実装クラスのインスタンスを登録しています。このとき、直書きした文字列で登録とコピー作成を行っていますが、未登録の名前でコピーを作ろうとしたらNullPointerExceptionです。名前のチェックをするか、そもそも登録時の名前を列挙型にした方が安全です。

名前チェック追加
public class Main2 {
    public static void main(String[] args) {
        // 準備
        UnderlinePen upen = new UnderlinePen('~');
        MessageBox mbox = new MessageBox('*');
        MessageBox sbox = new MessageBox('/');

        Manager manager = new Manager();
        manager.register("strong message", upen);
        manager.register("warning box", mbox);
        manager.register("slash box", sbox);
        
        // 生成(名前チェック追加)
        String[] protonames = { "strong message", "warning box", "slash box" };
        
        for (int i = 0; i < protonames.length; i++) {
            if (manager.checkProtoname(manager, protonames[i])) {
                Product p = manager.create(protonames[i]);
                p.use("Hello, world.");
            }
        }
    }
}
public class Manager {
    private HashMap<String, Product> showcase = new HashMap<String, Product>();

    public void register(String name, Product proto) {
        showcase.put(name, proto);
    }

    public Product create(String protoname) {
        Product p = showcase.get(protoname);
        return p.createClone();
    }

    // 名前の存在チェック
    public boolean checkProtoname(Manager manager, String protoname) {
        boolean result = false;
        result = showcase.containsKey(protoname);
        return result;
    }
}
列挙型使用
public enum ProductType {
    STRONG_MESSAGE("strong message"), 
    WARNING_BOX("warning box"), 
    SLASH_BOX("slash box");

    private String name;

    private ProductType(String name) {
        this.name = name;
    }

    public String toString() {
        return name;
    }
}
public class Main3 {
    public static void main(String[] args) {
        // 準備
        UnderlinePen upen = new UnderlinePen('~');
        MessageBox mbox = new MessageBox('*');
        MessageBox sbox = new MessageBox('/');

        Manager manager = new Manager();
        // (列挙型使用)
        manager.register(ProductType.STRONG_MESSAGE.toString(), upen);
        manager.register(ProductType.WARNING_BOX.toString(), mbox);
        manager.register(ProductType.SLASH_BOX.toString(), sbox);
		
        // 生成(列挙型使用)
        Product p1 = manager.create(ProductType.STRONG_MESSAGE.toString());
        p1.use("Hello, World.");
        Product p2 = manager.create(ProductType.WARNING_BOX.toString());
        p2.use("Hello, World.");
        Product p3 = manager.create(ProductType.SLASH_BOX.toString());
        p3.use("Hello, World.");		
    }
}

ところで、写経ついでにサンプルコードをちまちま変更するのは、某読書会でついた癖です。本のコードは、紙面節約のために、例外処理が省かれていたり、表記が妙だったりしますよね。そのまま素直に写経して持っていくと、優しい人たちからdisられるというわけです。もちろん、よろしくない変更をしたら、それはそれでdisられるっと! :'(

なぜclone()がいけないのか

冒頭に書いたとおり、身近ではcloneメソッドを徹底的に避ける人が多いです。正直、自分はそのあたりの真意をまだ十分理解していません。物の本を読めばいろいろ書いてあるけど、実感がない…。というわけで、TODOにしておきます。

デザパタの復習というより、cloneメソッドの復習になってしまった。第6章は、ここまで。