koturnの日記

転職したい社会人1年生の技術系日記

Javaで簡潔にディープコピーを行う

はじめに

発端は以下の一連のツイート.

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();
}