2006年04月17日

シリアライズを利用したディープコピー

【概要】
オブジェクトをディープコピーに対応させる際に、コピー対象のオブジェクトごとに clone() メソッドを定義していく手間を省くことができるシリアライズを利用したディープコピーの方法の紹介。

【キーワード】
ディープコピー、Deep Copy、シリアライズ、直列化、Serialization、Serialize

【詳細】
Java でディープコピー(DeepCopy)を行うには clone() メソッドをオーバーライドしてディープコピーの動作を自分で記述しけばよいが、他にも直列化(シリアライズ)の機構を利用してディープコピーを行う方法もある。 例えば次のような File クラスを考えてみる。

import java.util.Date;

public class File {
 private String name;
 private String description;
 private Date createdDate;
 private Date updatedDate;
 private Date lastAccessedDate;

 ...
}
このクラスを通常の方法でディープコピーに対応させる場合、次のようになる。
import java.util.Date;

public class File implements Cloneable {
 private String name;
 private String description;
 private Date createdDate;
 private Date updatedDate;
 private Date lastAccessedDate;

 ...

 public Object clone() throws CloneNotSupportedException {
  File clonedFile = (File) super.clone();
  clonedFile.setName(name);
  clonedFile.setDescription(description);
  if (createdDate != null)
   clonedFile.setCreatedDate((Date)createdDate.clone());
  if (updatedDate != null)
   clonedFile.setUpdatedDate((Date)updatedDate.clone());
  if (lastAccessedDate != null)
   clonedFile.setLastAccessedDate((Date)lastAccessedDate.clone());
  return clonedFile;
 }

 public static void main(String[] args) throws Exception {
  File file1 = new File();
  file1.setName("test");
  file1.setCreatedDate(new Date());

  // ディープコピーを行う
  File file2 = (File)file1.clone();
 }
}
直列化を使用したディープコピーを行うためには、まず次のようなディープコピー用のユーティリティーを用意する。
import java.io.*;

public class CopyUtil {
 public static Object deepCopy(Serializable obj)
   throws IOException, ClassNotFoundException {
  if (obj == null)
   return null;
  ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
  ObjectOutputStream out = new ObjectOutputStream(byteOut);
  out.writeObject(obj);
  ByteArrayInputStream byteIn =
   new ByteArrayInputStream(byteOut.toByteArray());
  ObjectInputStream in = new ObjectInputStream(byteIn);
  return in.readObject();
 }
}
そしてコピー対象となる File クラスは、次のように implements Serializable に変更する。
import java.io.Serializable;

public class File implements Serializable {
 private String name;
 private String description;
 private Date createdDate;
 private Date updatedDate;
 private Date lastAccessedDate;

 ...

 public static void main(String[] args) throws Exception {
  File file1 = new File();
  file1.setName("test");
  file1.setCreatedDate(new Date());

  // ディープコピーを行う
  File file2 = (File)CopyUtil.deepCopy(file1);
 }
}
直列化を使用した方法の良いところは、コピー対象のクラスごとにディープコピーの処理内容を定義していく必要がないため、開発の手間が省け、フィールド追加時に clone() メソッド内にディープコピー対応コードを入れ忘れてしまったり、コピー対象がネストした構造になっている時に、実は階層の下の方で has-a されていたのだけれど clone() メソッドを実装し忘れてしまったというようなミスも起こりにくくなる。

この方法で気をつけなければならないこととして、通常の方法と比べてパフォーマンスはそれなりに劣化するため注意が必要なことと、次のような場合に図らずもディープコピーが行われてしまうことがあるという点が挙げられる。

この例では A4 という Type と B4 という Type のインスタンスは一つしかつくられないように見えるが、ディープコピーされた結果 A4 というタイプが複製数分生成されてしまっている。

import java.io.Serializable;

public class Type implements Serializable {
 public static final Type A4 = new Type();
 public static final Type B4 = new Type();
 
 private Type() {
 }
}
import java.io.Serializable;

public class Paper implements Serializable {
 private final Type type;
 
 public Paper(Type type) {
  if (type == null)
   throw new IllegalArgumentException("Type should not be null.");
  this.type = type;
 }
 
 public Type getType() {
  return type;
 }

 public static void main(String[] args) throws Exception {
  Paper paper1 = new Paper(Type.A4);
  Paper paper2 = new Paper(Type.A4);
  Paper paper3 = (Paper)CopyUtil.deepCopy(paper1);
  
  System.out.println(paper1.getType() == paper2.getType()); // true
  System.out.println(paper1.getType() == paper3.getType()); // false(!)
 }
}
また、何らかの理由でクラスローダーを自作している場合には上述の CopyUtil は次のような要領で変更する必要がある。
import java.io.*;

public class CopyUtil {

 public static Object deepCopy(Serializable obj)
  throws IOException, ClassNotFoundException {
  return deepCopy(obj, null);
 }

 public static Object deepCopy(Serializable obj, ClassLoader loader)
   throws IOException, ClassNotFoundException {
  if (obj == null)
   return null;
  ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
  ObjectOutputStream out = new ObjectOutputStream(byteOut);
  out.writeObject(obj);
  ByteArrayInputStream byteIn =
   new ByteArrayInputStream(byteOut.toByteArray());
  ClassLoaderSpecifiedObjectInputStream in =
    new ClassLoaderSpecifiedObjectInputStream(byteIn, loader);
  return in.readObject();
 }
}
import java.io.*;

public class ClassLoaderSpecifiedObjectInputStream extends ObjectInputStream {

 private final ClassLoader loader;

 public ClassLoaderSpecifiedObjectInputStream(
   InputStream in, 
   ClassLoader loader) throws IOException {
  super(in);
  if (loader == null)
   this.loader = obj.getClass().getClassLoader();
  else
   this.loader = loader;
 }

 public Class resolveClass(ObjectStreamClass v)
   throws IOException, ClassNotFoundException {
  try {
   return super.resolveClass(v);
  } catch (ClassNotFoundException e) {
  }
  return Class.forName(v.getName(), false, loader);
 }
}

※ 2006/5/1 追記
nagaton さんのブログで、Paper と Type のケースでは結局どのようにすれば良いのかということについて指摘があったので追記。

この問題は過去に実際アプレッソで発生したことがある問題なのだが、その時には次のように対処した。

import java.util.*;

public class Type {
 public static final Type A4 = new Type("A4");
 public static final Type B4 = new Type("B4");

 public static final Type[] TYPES = new Type[] {
  A4,
  B4
 };

 private static final Map INSTANCE_MAP = new HashMap();

 static {
  for (Type type : TYPES) {
   INSTANCE_MAP.put(type.getName(), type);
  }
 }

 private final String name;

 private Type(String name) {
  if (name == null || name.equals(""))
   throw new IllegalArgumentException("Name should not be null nor blank.");
  this.name = name;
 }

 public String getName() {
  return name;
 }

 public static Type getInstance(String name) {
  Type type = INSTANCE_MAP.get(name);
  if (type == null) {
   throw new IllegalArgumentException(
    "Type instance for name is not found. name=" + name);
  }
  return type;
 }
}
import java.io.Serializable;

public class Paper implements Serializable {
 ...
 private final String typeName;
 ...
 public Paper(Type type) {
  if (type == null)
   throw new IllegalArgumentException("Type should not be null.");
  this.typeName = type.getName();
 }
 ...
 public Type getType() {
  return Type.getInstance(typeName);
 }
 ...
}

他にも getInstance() と同様の内容を readResolve() 内に定義する次のような方法もある。

上の方法の長所としてはディープコピーのためだけに Type を Serializable にする必要がないことが挙げられるが、Type を Serializable にする必要がある場合にはいずれにしても下記のような対応が必要になるため、こちらの方法が望ましい。

import java.util.*;
import java.io.*;

public class Type implements Serializable {
 public static final Type A4 = new Type("A4");
 public static final Type B4 = new Type("B4");

 public static final Type[] TYPES = new Type[] {
  A4,
  B4
 };

 private static final Map INSTANCE_MAP = new HashMap();

 static {
  for (Type type : TYPES) {
   INSTANCE_MAP.put(type.getName(), type);
  }
 }

 private final String name;

 private Type(String name) {
  if (name == null || name.equals(""))
   throw new IllegalArgumentException("Name should not be null nor blank.");
  this.name = name;
 }

 public String getName() {
  return name;
 }

 private Object readResolve() throws ObjectStreamException {
  Type type = INSTANCE_MAP.get(name);
  if (type == null) {
   throw new IllegalArgumentException(
    "Type instance for name is not found. name=" + name);
  }
  return type;
 }
}

このエントリーをはてなブックマークに追加 このエントリーを含むはてなブックマーク livedoorクリップ del.icio.us テクノラティ検索
lalha_java at 21:08 │Comments(0)TrackBack(1)

トラックバックURL

この記事へのトラックバック

まずは参考サイト。小野和俊氏のブログより。 Java Programming Tips:シリアライズを利用したディープコピー import java.io.*; public class CopyUtil {     public static Object deepCopy(Serializable obj)       &a...

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔