はじめに
発端は以下の一連のツイート.
@ryunix にゃるほどっ...clone的なの実装しますかね...
— ちゃっく@貴方の承認欲求満たします (@chakku_000) 2017年1月22日
Javaのオブジェクトのコピーといえば clone()
メソッドを用いるものである.
しかし,標準ライブラリのArrayListでは以下のどちらの手段を用いても,シャローコピーとなるようだ.
// list01 の型は ArrayList<Hoge> ArrayList<Hoge> list02 = (ArrayList<Hoge>) list01.clone(); ArrayList<Hoge> list03 = new ArrayList<>(list01);
これを解消するためには,一々要素のコピーを行う必要があるだろう.
ArrayList<Hoge> list03 = new ArrayList<>(Arrays.asList(list01.stream() .map(elm -> { Hoge obj = null; try { obj = (Hoge) elm.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } return obj; }).toArray(Hoge[]::new)));
しかし,こういうのはちょっと面倒なように思える上, ArrayList
にしか適用できない.
そこで,任意のオブジェクトに対して,ディープコピーを行うことを考える
ディープコピー
C# だと,オブジェクトのシリアライズ,デシリアライズを利用してコピーを行うのが一般的となっている. それをJavaで真似てやってみる.
以下のメソッドを定義する.
@SuppressWarnings("unchecked") public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(obj); return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject(); }
このメソッドは以下のように利用できる. 返り値の型はジェネリクスの型推論により,引数と同一の型になるため,呼び出し側でキャストを行う必要はない.
// list01 の型は ArrayList<Hoge>
ArrayList<Hoge> list02 = deepcopy(list01);
このように簡潔にディープコピーが可能である.
ただし,シリアライズ,デシリアライズを行うためには,対象のオブジェクトのクラスが Serializable
インタフェースを実装しておく必要(メンバも含め)がある点には気をつけておくこと.
また,シリアライズ,デシリアライズのコストはやや高めなので,自作クラスのディープコピーは clone()
メソッドを適切に実装するのがよいだろう.
サンプルコード
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; public class Main { public static void main(String[] args) throws ClassNotFoundException, IOException{ ArrayList<Hoge> list01 = new ArrayList<>(); list01.add(new Hoge(10)); list01.add(new Hoge(20)); @SuppressWarnings("unchecked") ArrayList<Hoge> list02 = (ArrayList<Hoge>) list01.clone(); ArrayList<Hoge> list03 = new ArrayList<>(list01); ArrayList<Hoge> list04 = new ArrayList<Hoge>(Arrays.asList(list01.stream() .map(elm -> { Hoge obj = null; try { obj = (Hoge) elm.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } return obj; }).toArray(Hoge[]::new))); ArrayList<Hoge> list05 = deepcopy(list01); list01.get(0).setValue(100); System.out.println("list01: Original"); list01.stream().mapToInt(t -> t.getValue()).forEach(System.out::println); System.out.println("list02: Shallow copy"); list02.stream().mapToInt(t -> t.getValue()).forEach(System.out::println); System.out.println("list03: Shallow copy"); list03.stream().mapToInt(t -> t.getValue()).forEach(System.out::println); System.out.println("list04: Deep copy"); list04.stream().mapToInt(t -> t.getValue()).forEach(System.out::println); System.out.println("list05: Deep copy"); list05.stream().mapToInt(t -> t.getValue()).forEach(System.out::println); } @SuppressWarnings("unchecked") public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(obj); return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject(); } } /** * Cloneable は 要素を1つずつ追加するdeep copyの例のため * Serializable は シリアライズ / デシリアライズによる deep copyのため */ class Hoge implements Cloneable, Serializable { private static final long serialVersionUID = 1145141919810L; private int value; Hoge(int value) { this.value = value; } int getValue() { return value; } void setValue(int value) { this.value = value; } @Override public Hoge clone() throws CloneNotSupportedException { Hoge obj = (Hoge) super.clone(); obj.value = value; return obj; } }
まとめ
Javaの標準ライブラリの中にはディープコピーが楽ではないものもある.
シリアリアズ,デシリアライズ用いてdeep copyを行うためには以下のようなメソッドを用意するとよい.
@SuppressWarnings("unchecked") public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(obj); return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject(); }