Javaのcloneメソッド考察

# 背景
「Javaってクラス型の変数をコピーすると参照をコピーしちゃうからコピー先を変更するとコピー元も書き換わっちゃうんだよ」
「じゃあインスタンス複製するときは気をつけなきゃいけないねー」

みたいな話があるので、インスタンス複製するときの挙動をいろいろ試してみたのでメモ
特にArrayListクローン時の挙動とか
Java8使ってます。

# cloneを使わないとどうなるか
「=」を使って新しいインスタンスを作成した場合の挙動をとりあえず見てみる。
Cloneメソッドを実装しない。ComputerAクラス、NicAクラスを用意した。

``` ComputerA.java
public class ComputerA {

	private int id;
	private String name;
	private NicA nic = new NicA();
	// getter,setterは省略

}
```

```NicA.java
public class NicA {

	private String macAddress;
	private String ipAddress;
	// getter,setterは省略

}
```

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

		ComputerA com = new ComputerA();

		// とりあえず値をセット
		com.setId(1);
		com.setName("myComputer");
		com.getNic().setMacAddress("00-00-00-00-00-00-00-E0");
		com.getNic().setIpAddress("192.168.1.1");

		// 新しいインスタンスを生成して=を使ってコピー
		ComputerA com2 = com;
		// com2の名前を書き換える
		com2.setName("yourComputer");
		// com2のmacAddressを書き換える
		com2.getNic().setMacAddress("11-11-11-11-11-11-11-E1");

		// com, com2のName,macAddressを出力
		System.out.println("comのNameは(" + com.getName() + ")");
		System.out.println("com2のNameは(" + com2.getName() + ")");
		System.out.println("comのmacAddressは(" + com.getNic().getMacAddress() + ")");
		System.out.println("com2のmacAddressは(" + com2.getNic().getMacAddress() + ")");
	}
}
```

```shell-session:実行結果
comのNameは(yourComputer)
com2のNameは(yourComputer)
comのmacAddressは(11-11-11-11-11-11-11-E1)
com2のmacAddressは(11-11-11-11-11-11-11-E1)
```

見て分かる通り、com2の値を書き換えるとcomの値も書き換わってしまう。

# cloneメソッドを使う

javaには、Object型にCloneメソッドが用意されていて、これを各クラスに実装することで自分自身を複製することができる。
詳細は[Javaのcloneメソッドの正しい実装方法](http://qiita.com/SUZUKI_Masaya/items/8da8c0038797f143f5d3)を参考に
Cloneメソッドを実装したComputerBクラス、NicBクラスを用意した。

```ComputerB.java
public class ComputerB implements Cloneable {
	
	private int id;
	private String name;
	private NicB nic = new NicB();
	
	@Override
	public ComputerB clone(){
		
		ComputerB comB = new ComputerB();
        try {
           comB = (ComputerB)super.clone();
           comB.nic = this.nic.clone();
        }catch (Exception e){
            e.printStackTrace();
        }
		return comB;
	}
	// getter,setterは省略
}
```

```NicB.java
public class NicB implements Cloneable{
	private String macAddress;
	private String ipAddress;
	
	@Override
	public NicB clone(){
		
		NicB comB = new NicB();
        try {
           comB = (NicB)super.clone();
        }catch (Exception e){
            e.printStackTrace();
        }
		return comB;
	}
	// getter,setterは省略
}
```

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

		ComputerB com = new ComputerB();

		// とりあえず値をセット
		com.setId(1);
		com.setName("myComputer");
		com.getNic().setMacAddress("00-00-00-00-00-00-00-E0");
		com.getNic().setIpAddress("192.168.1.1");

		// 新しいインスタンスを生成してCloneメソッドを使ってコピー
		ComputerB com2 = com.clone();
		// com2の名前を書き換える
		com2.setName("yourComputer");
		// com2のmacAddressを書き換える
		com2.getNic().setMacAddress("11-11-11-11-11-11-11-E1");

		// com, com2のName,macAddressを出力
		System.out.println("comのNameは(" + com.getName() + ")");
		System.out.println("com2のNameは(" + com2.getName() + ")");
		System.out.println("comのmacAddressは(" + com.getNic().getMacAddress() + ")");
		System.out.println("com2のmacAddressは(" + com2.getNic().getMacAddress() + ")");
	}
}
```

```shell-session:実行結果
comのNameは(myComputer)
com2のNameは(yourComputer)
comのmacAddressは(00-00-00-00-00-00-00-E0)
com2のmacAddressは(11-11-11-11-11-11-11-E1)
```

きちんと、複製されていることがわかる。
ちなみに、ここでNicB.javaにCloneメソッドを実装しないと、NicB.javaのフィールドであるmacAddressがクローンされず以下の様な実行結果になるので注意

```shell-session:実行結果
comのNameは(myComputer)
com2のNameは(yourComputer)
comのmacAddressは(11-11-11-11-11-11-11-E1)
com2のmacAddressは(11-11-11-11-11-11-11-E1)
```

# ArrayListのCloneは？

じゃあこのCloneメソッドはArrayListの時にも使えるのだろうか？上記のComputerBクラスを格納したArrayListに対してCloneを試してみた。

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

		// com1を作成
		ComputerB com1 = new ComputerB();
		// とりあえず値をセット
		com1.setId(1);
		com1.setName("myComputer1");
		com1.getNic().setMacAddress("00-00-00-00-00-00-00-E0");
		com1.getNic().setIpAddress("192.168.1.1");
		
		//com2を作成
		ComputerB com2 = new ComputerB();
		// とりあえず値をセット
		com2.setId(2);
		com2.setName("myComputer2");
		com2.getNic().setMacAddress("00-00-00-00-00-00-00-E1");
		com2.getNic().setIpAddress("192.168.1.2");
		
		// ArrauListを作ってcom1とcom2を格納
		ArrayList <ComputerB> comList = new ArrayList<ComputerB>();
		comList.add(com1);
		comList.add(com2);

		// 新しいArrayListを生成してclone
		ArrayList <ComputerB> comList2 = (ArrayList<ComputerB>) comList.clone();
		
		// comList2の一番目のオブジェクトの名前を書き換える
		comList2.get(0).setName("yourComputer1");
		// comList2の一番目のオブジェクトのmacAddressを書き換える
		comList2.get(0).getNic().setMacAddress("11-11-11-11-11-11-11-E1");

		// com, com2のName,macAddressを出力
		System.out.println("comListの一番目のオブジェクトのNameは(" + comList.get(0).getName() + ")");
		System.out.println("comList2の一番目のオブジェクトのNameは(" + comList2.get(0).getName() + ")");
		System.out.println("comListの一番目のオブジェクトのmacAddressは(" + comList.get(0).getNic().getMacAddress() + ")");
		System.out.println("comList2の一番目のオブジェクトのmacAddressは(" +  comList2.get(0).getNic().getMacAddress() + ")");
	}
}
```

```shell-session:実行結果
comListの一番目のオブジェクトのNameは(yourComputer1)
comList2の一番目のオブジェクトのNameは(yourComputer1)
comListの一番目のオブジェクトのmacAddressは(11-11-11-11-11-11-11-E1)
comList2の一番目のオブジェクトのmacAddressは(11-11-11-11-11-11-11-E1)
```

・・・だめじゃん！。値書き換わってるじゃん！複製されてないじゃん！！
どうもArrayListのCloneメソッドは、List内の各要素のシャローコピーを取るだけで、各要素のcloneメソッドを呼んでくれているわけではないらしい。
まあ、Listに入ってるのがCloneableなクラスとは限らないしね・・・。
なのでちゃんと複製しようとすると

```Main.java
ArrayList <ComputerB> comList2 = (ArrayList<ComputerB>) comList.clone();     //複製できない
```

ではなく

```Main.java
ArrayList <ComputerB> comList2 = new ArrayList<ComputerB>();
for (ComputerB comB : comList){
	comList2.add(comB.clone());     //複製できる
}
```

のように書く必要がある。

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

		// com1を作成
		ComputerB com1 = new ComputerB();
		// とりあえず値をセット
		com1.setId(1);
		com1.setName("myComputer1");
		com1.getNic().setMacAddress("00-00-00-00-00-00-00-E0");
		com1.getNic().setIpAddress("192.168.1.1");
		
		//com2を作成
		ComputerB com2 = new ComputerB();
		// とりあえず値をセット
		com2.setId(2);
		com2.setName("myComputer2");
		com2.getNic().setMacAddress("00-00-00-00-00-00-00-E1");
		com2.getNic().setIpAddress("192.168.1.2");
		
		// ArrayList comListを作ってcom1とcom2を格納
		ArrayList <ComputerB> comList = new ArrayList<ComputerB>();
		comList.add(com1);
		comList.add(com2);

		// 新しいArrayListを生成
		ArrayList <ComputerB> comList2 = new ArrayList<ComputerB>();
		
		// ArrayListの要素全てに対してcloneメソッドを実行。comList2に格納する
		for (ComputerB comB : comList){
			comList2.add(comB.clone());
		}
		
		// comList2の一番目のオブジェクトの名前を書き換える
		comList2.get(0).setName("yourComputer1");
		// comList2の一番目のオブジェクトのmacAddressを書き換える
		comList2.get(0).getNic().setMacAddress("11-11-11-11-11-11-11-E1");

		// comList, comList2のName,macAddressを出力
		System.out.println("comListの一番目のオブジェクトのNameは(" + comList.get(0).getName() + ")");
		System.out.println("comList2の一番目のオブジェクトのNameは(" + comList2.get(0).getName() + ")");
		System.out.println("comListの一番目のオブジェクトのmacAddressは(" + comList.get(0).getNic().getMacAddress() + ")");
		System.out.println("comList2の一番目のオブジェクトのmacAddressは(" +  comList2.get(0).getNic().getMacAddress() + ")");
	}
}
```

```shell-session:実行結果
comListの一番目のオブジェクトのNameは(myComputer1)
comList2の一番目のオブジェクトのNameは(yourComputer1)
comListの一番目のオブジェクトのmacAddressは(00-00-00-00-00-00-00-E0)
comList2の一番目のオブジェクトのmacAddressは(11-11-11-11-11-11-11-E1)
```

無事、複製できた。
試してないけど、ArrayList以外のListを使った場合でもおんなじだと思う。
個人的には、CloneメソッドぐらいObjectクラスで実装しといてくれないんだろうかと思ったりもするけど、やっぱ難しいのかな・・・

# 余談
[正しいclone()の書き方](http://funini.com/kei/java/clone.shtml) になんでこんな変な仕様になったのか背景までに考察してくれていた。