🧑‍🤝‍🧑

JavaのcloneメソッドとClonableインターフェースについて

2020/12/11に公開

はじめに

大学のJava言語で学ぶデザインパターン入門の授業でPrototypeパターンについて学び、その中でcloneメソッド,Clonableインターフェースについて調べる機会があったのでまとめておこうと思います。
まずcloneメソッドとCloneableインターフェースについて説明し、その後にそれらを使った浅いコピーと深いコピーの例を示します。
初めてちゃんとした(?)記事を書くので何か不備があればご指摘いただければと思います。

cloneメソッドとCloneableインターフェースの概要

インスタンスの複製をするにはcloneメソッドを使うことが必要になります。そしてcloneメソッドを使うには、その複製したいクラスがCloneableインターフェースを継承していることが必要です。この「Cloneableインターフェースを継承している」というのは、

  • 複製したいクラスそのものがCloneableインターフェースを継承している
  • 複製したいクラスのスーパークラスがCloneableインターフェースを継承している

の2パターンあって、どちらでもOKです。
また、Cloneableインターフェースを継承していないのにcloneメソッドを呼び出そうとするとCloneNotSupportedExceptionが投げられるので、try-catchで囲む必要があります。

cloneメソッドとCloneableインターフェースの関係

先ほど「cloneメソッドを使うにはCloneableインターフェースを継承している必要がある」と書きましたが、そう聞くと「Cloneableインターフェースの中にcloneメソッドが定義されているのかな?」と思ってしまうかもしれません。
しかし!実際はそうではないのです!!
実は、cloneメソッド自体はjava.lang.Objectクラスのなかで定義されているもので、そのjava.lang.Objectクラスはすべてのクラスが勝手に継承しているクラスです。なので、すべてのクラスがcloneメソッドを持っていることになります。つまりCloneableインターフェースは1つもメソッドを持っておらず、「このクラスはcloneメソッドで複製できますよ」という目印でしかないのです。こういった目印の役割を持つインターフェースをマーカーインターフェースと言います。
また、JavaのAPIリファレンスを見るとcloneメソッドは

protected Object clone() throws CloneNotSupportedException

というふうになっています。注目して欲しいのがprotectedです。アクセス修飾子がprotectedになっているので、「Aというインスタンスを複製したい!」となった時にmainメソッドのなかで

Main.java
A.clone();

というふうにcloneメソッドを呼び出そうとすると、コンパイル時に

clone()はObjectでprotectedアクセスされます

というエラーが発生します。
じゃあどうするかというと、Cloneableを継承したAクラスの中にpublicのcreateCloneとか適当なメソッドを作り、その中でcloneメソッドを呼び出すという方法をとります。

ここまでで必要最低限のことは書いたと思うので、cloneを使った実際のサンプルを見てみようと思います。
cloneメソッドは浅いコピーと深いコピーの2種類の複製の仕方があるので順番に紹介します。

浅いコピーのサンプル

Hero.java, Sword.java, Main.java を使った例を示そうと思います。
ここで複製したいのはHeroクラスです。

Hero.java
import java.lang.Cloneable;

public class Hero implements Cloneable{
  String name;
  Sword sword;
  public Hero(String name, Sword sword){
    this.name = name;
    this.sword = sword;
  }

  public Hero createClone(){
    Hero h = null;
    try {
      h = (Hero)clone();
    } catch(Exception e) {
      e.printStackTrace();
    }
    return h;
  }

}
Sword.java
public class Sword {
  String name;

  public Sword(String name){
    this.name = name;
  }

}
Main.java
public class Main{
  public static void main(String[] args){

    Sword s = new Sword("炭治郎の日輪刀");
    Hero h1 = new Hero("炭治郎", s);
    Hero h2 = h1.createClone();
    Hero h3 = h1;

    System.out.println(h1 == h2); 
    System.out.println(h1 == h3); 

    h1.sword.name = "胡蝶しのぶの日輪刀";
    System.out.println(h1.sword.name);
    System.out.println(h2.sword.name);
    System.out.println(h1.sword == h2.sword);

  }
}

どのように出力されるかを見る前にコードを確認します。
複製したいHeroクラスはCloneableを実装しています。そしてprotectedのcloneメソッドをpublicのcreateメソッドの中で呼び出し、それをMainの中で呼び出す、という流れになっています。また、cloneの返り値はObject型なのでHero型にキャストしています。
では出力結果を見てみます。

false
true
胡蝶しのぶの日輪刀
胡蝶しのぶの日輪刀
true

h1 == h2 で falseと出力されていることから、cloneメソッドで新しくインスタンスが複製されたことが確認できますね。一方、 h1 == h3 がtrueなのは h3にはHeroインスタンスそのものではなく参照が代入されているためです。cloneとの比較で書いておきました。
問題なのは次の3行の出力です。h1のswordの名前しか変えてないのにh2のswordの名前も変わってしまっています。これは、cloneは浅いコピーをしているからです。浅いコピーはフィールドの先にあるインスタンスをコピーするのではなく参照をコピーしています。つまりHeroインスタンスは無事複製できても、Swordインスタンスは複製できていないのです。参照しかコピーできていないのでh1.swordもh2.swordも結局同じSwordを指していることになります。そのことはh1.sword == h2.sword が true と出力されていることからも確認できます。
Swordインスタンスも複製したい!となったら深いコピーをします。次の項でサンプルを紹介します。

深いコピーのサンプル

Swordインスタンスも複製されるようにHero.javaとSword.javaを書き直したものがこちらです。(Main.javaはそのまま)

Hero.java
import java.lang.Cloneable;

public class Hero implements Cloneable{
  String name;
  Sword sword;
  public Hero(String name, Sword sword){
    this.name = name;
    this.sword = sword;
    System.out.println("Heroインスタンスを作りました");
  }

  public Hero clone(){
    Hero copy = new Hero(this.name, (Sword)this.sword.createClone());
    return copy;
  }

  public Hero createClone(){
    Hero h = null;
    try {
      h = (Hero)clone();
    } catch(Exception e) {
      e.printStackTrace();
    }
    return h;
  }

}
Sword.java
public class Sword implements Cloneable{
  String name;

  public Sword(String name){
    this.name = name;
  }

  public Sword createClone(){
    Sword copy = null;
    try {
      copy = (Sword)this.clone();
    } catch(CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return copy;
  }

}

変わったところは

  • SwordクラスがCloneableインターフェースを実装し、createCloneメソッドを持っている
  • Heroクラスのなかでcloneメソッドをオーバーライドしている
  • そのなかでSwordクラスのcreateCloneメソッドを呼んでいる
    の3つです。このように書き換えることでSwordインスタンスも複製できます。先ほどのMain.javaを実行してみると、
false
true
胡蝶しのぶの日輪刀  //h1のswordだけ名前が変わって
炭治郎の日輪刀  //h4のswordは変わらない!
false // !!

h1.sword == h2.sword がfalseになっていることからswordもしっかり複製されていることがわかります!やったー!

まとめ

  • cloneメソッドを使うためにはcloneableインターフェースを継承する必要がある
  • cloneメソッドは自分のクラスの中からしか呼び出せないので他のクラスで複製を要請する場合はpubulicメソッドを定義してその中でcloneを呼び出す必要がある

参考文献

Discussion