JavaのObject clone()メソッド – Javaでのクローニング

クローニングは、オブジェクトのコピーを作成するプロセスです。Java Objectクラスはネイティブのclone()メソッドを備えており、既存のインスタンスのコピーを返します。ObjectがJavaのベースクラスであるため、デフォルトですべてのオブジェクトはクローニングをサポートします。

Javaオブジェクトのクローニング

Java Objectのclone()メソッドを使用したい場合は、java.lang.Cloneableマーカーインターフェースを実装する必要があります。そうでない場合は、実行時にCloneNotSupportedExceptionがスローされます。また、Object cloneは保護されたメソッドなので、オーバーライドする必要があります。Javaにおけるオブジェクトクローニングを例のプログラムで見てみましょう。

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Employee implements Cloneable {

	private int id;

	private String name;

	private Map<String, String> props;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Map<String, String> getProps() {
		return props;
	}

	public void setProps(Map<String, String> p) {
		this.props = p;
	}

	 @Override
	 public Object clone() throws CloneNotSupportedException {
	 return super.clone();
	 }

}

Object clone()メソッドを使用しているので、Cloneableインターフェースを実装しました。スーパークラスのclone()メソッド、つまりObject clone()メソッドを呼び出しています。

Object clone()メソッドの使用

テストプログラムを作成して、オブジェクトのclone()メソッドを使用してインスタンスのコピーを作成しましょう。

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Map;

public class CloningTest {

	public static void main(String[] args) throws CloneNotSupportedException {

		Employee emp = new Employee();

		emp.setId(1);
		emp.setName("Pankaj");
		Map props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

		Employee clonedEmp = (Employee) emp.clone();

		// empとclonedEmpの属性が同じかどうかを確認します
		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		// デフォルトのクローン使用の効果を確認しましょう
		
		// empのプロパティを変更します
		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		// empの名前を変更します
		emp.setName("new");
		System.out.println("clonedEmp name:" + clonedEmp.getName());

	}

}

出力:

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:Pankaj

ランタイムでのCloneNotSupportedException

EmployeeクラスがCloneableインターフェースを実装していない場合、上記のプログラムはCloneNotSupportedExceptionのランタイム例外をスローします。

Exception in thread "main" java.lang.CloneNotSupportedException: com.journaldev.cloning.Employee
	at java.lang.Object.clone(Native Method)
	at com.journaldev.cloning.Employee.clone(Employee.java:41)
	at com.journaldev.cloning.CloningTest.main(CloningTest.java:19)

オブジェクトクローンの理解

上記の出力を見て、Object clone()メソッドの動作を理解しましょう。

  1. empとclonedEmp == test: false:empとclonedEmpは異なるオブジェクトであり、同じオブジェクトを参照していません。これは、Javaのオブジェクトクローニングの要件と一致しています。
  2. emp と clonedEmp HashMap == テスト: true:emp と clonedEmp オブジェクト変数の両方が同じオブジェクトを参照しています。これは、基礎となるオブジェクトの値を変更すると、深刻なデータ整合性の問題になり得ます。値の変更はクローンされたインスタンスにも反映される可能性があります。
  3. clonedEmp props:{city=New York, salary=10000, title=CEO}:clonedEmp プロパティに変更を加えていないのに、emp と clonedEmp 変数の両方が同じオブジェクトを参照しているために変更されました。これは、デフォルトの Object clone() メソッドが浅いコピーを作成するためです。クローンプロセスを通じて完全に切り離されたオブジェクトを作成したい場合に問題が発生する可能性があります。これにより、望ましくない結果が生じることがあるため、Object clone() メソッドを適切にオーバーライドする必要があります。
  4. clonedEmp name:Pankaj:ここで何が起こったのでしょうか?emp 名を変更しましたが、clonedEmp 名は変わりませんでした。これは、String が不変であるためです。したがって、emp 名を設定すると新しい文字列が作成され、emp 名の参照がthis.name = name;で変更されます。したがって、clonedEmp 名は変更されません。同様の動作は、任意のプリミティブ変数型に対しても見られます。したがって、オブジェクトにプリミティブおよび不変の変数のみが含まれている限り、Java オブジェクトのデフォルトのクローニングは問題ありません。

オブジェクトのクローニングの種類

2種類のオブジェクトのクローニングがあります – シャロークローニングとディープクローニング。それぞれを理解し、Javaプログラムでのクローニングの最良の方法を見つけましょう。

1. シャロークローニング

Javaオブジェクトのclone()メソッドのデフォルト実装は、シャローコピーを使用しています。インスタンスのコピーを作成するために反射APIを使用しています。以下のコードスニペットは、シャロークローニングの実装を示しています。

@Override
 public Object clone() throws CloneNotSupportedException {
 
	 Employee e = new Employee();
	 e.setId(this.id);
	 e.setName(this.name);
	 e.setProps(this.props);
	 return e;
}

2. ディープクローニング

ディープクローニングでは、フィールドを1つずつコピーする必要があります。リスト、マップなどのネストされたオブジェクトを持つフィールドがある場合、それらを1つずつコピーするコードを記述する必要があります。これがディープクローニングまたはディープコピーと呼ばれる理由です。ディープクローニングのために、以下のようにEmployeeのcloneメソッドをオーバーライドすることができます。

public Object clone() throws CloneNotSupportedException {

	Object obj = super.clone(); //utilize clone Object method

	Employee emp = (Employee) obj;

	// イミュータブルフィールドのディープクローニング
	emp.setProps(null);
	Map hm = new HashMap<>();
	String key;
	Iterator it = this.props.keySet().iterator();
	// フィールドごとのディープコピー
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, this.props.get(key));
	}
	emp.setProps(hm);
	
	return emp;
}

このclone()メソッドの実装を使用すると、テストプログラムは次の出力を生成します。

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:Pankaj

ほとんどの場合、これが私たちの望む動作です。clone()メソッドは、元のインスタンスと完全に切り離された新しいオブジェクトを返す必要があります。したがって、プログラムでObjectのクローンとクローニングを使用する予定がある場合は、慎重に行い、可変フィールドを適切にオーバーライドしてください。クラスが他のクラスを拡張し、さらにそのクラスが他のクラスを拡張する場合、これは困難なタスクになる可能性があります。すべての可変フィールドのディープコピーを処理するために、Objectの継承階層全体を移動する必要があります。

シリアル化を使用したクローニング?

ディープクローニングを簡単に実行する方法の1つは、シリアル化を介して行うことです。ただし、シリアル化は高コストな手続きであり、クラスはSerializableインターフェースを実装する必要があります。すべてのフィールドとスーパークラスもSerializableを実装する必要があります。

Apache Commons Utilの使用

プロジェクトで既にApache Commons Utilクラスを使用していて、クラスがシリアル化可能である場合は、以下の方法を使用してください。

Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);

クローン用のコピーコンストラクター

オブジェクトのコピーを作成するためにコピーコンストラクターを定義することができます。なぜ完全にオブジェクトのclone()メソッドに依存する必要があるのでしょうか?例えば、以下のような従業員のコピーコンストラクターを持つことができます。

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map hm = new HashMap<>();
	String key;
	Iterator it = emp.getProps().keySet().iterator();
	// フィールドごとのディープコピー
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, emp.getProps().get(key));
	}
	this.setProps(hm);

}

従業員オブジェクトのコピーが必要な場合は、Employee clonedEmp = new Employee(emp);を使用して取得できます。ただし、クラスに多くの変数がある場合、特にプリミティブおよび不変の場合、コピーコンストラクターを記述することは煩雑な作業になる可能性があります。

Javaオブジェクトのクローニングのベストプラクティス

  1. クラスにプリミティブおよび不変の変数がある場合、またはシャローコピーを作成したい場合には、デフォルトのObject clone()メソッドを使用してください。継承の場合は、Objectレベルまで拡張しているすべてのクラスをチェックする必要があります。

  2. あなたのクラスが主に変更可能なプロパティを持っている場合、コピーのコンストラクタも定義できます。

  3. オーバーライドされたクローンメソッドでsuper.clone()を呼び出し、変更可能なフィールドのディープコピーを行います。

  4. クラスがシリアライズ可能な場合、クローニングにはシリアライゼーションを使用できます。ただし、パフォーマンスに影響があるため、シリアライゼーションを使用する前にベンチマークを行ってください。

  5. クラスを拡張して、ディープコピーを使用して適切にクローンメソッドを定義している場合、デフォルトのクローンメソッドを利用できます。例えば、次のように従業員クラスで適切に定義されたclone()メソッドがあります。

    @Override
    public Object clone() throws CloneNotSupportedException {
    
    	Object obj = super.clone();
    
    	Employee emp = (Employee) obj;
    
    	// 不変フィールドのディープクローニング
    	emp.setProps(null);
    	Map<String, String> hm = new HashMap<>();
    	String key;
    	Iterator<String> it = this.props.keySet().iterator();
    	// フィールドごとのディープコピー
    	while (it.hasNext()) {
    		key = it.next();
    		hm.put(key, this.props.get(key));
    	}
    	emp.setProps(hm);
    
    	return emp;
    }
    

    子クラスを作成し、スーパークラスのディープクローニングを利用できます。

    package com.journaldev.cloning;
    
    public class EmployeeWrap extends Employee implements Cloneable {
    
    	private String title;
    
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String t) {
    		this.title = t;
    	}
    
    	@Override
    	public Object clone() throws CloneNotSupportedException {
    
    		return super.clone();
    	}
    }
    

    EmployeeWrap クラスには可変プロパティがなく、スーパークラスの clone() メソッドの実装を利用しています。可変フィールドがある場合は、それらのフィールドのディープコピーに注意する必要があります。こちらは、このクローニング方法がうまく機能するかどうかをテストするための単純なプログラムです。

    package com.journaldev.cloning;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class CloningTest {
    
    	public static void main(String[] args) throws CloneNotSupportedException {
    
    		EmployeeWrap empWrap = new EmployeeWrap();
    
    		empWrap.setId(1);
    		empWrap.setName("Pankaj");
    		empWrap.setTitle("CEO");
    		
    		Map<String, String> props = new HashMap<>();
    		props.put("salary", "10000");
    		props.put("city", "Bangalore");
    		empWrap.setProps(props);
    
    		EmployeeWrap clonedEmpWrap = (EmployeeWrap) empWrap.clone();
    		
    		empWrap.getProps().put("1", "1");
    		
    		System.out.println("empWrap mutable property value = "+empWrap.getProps());
    
    		System.out.println("clonedEmpWrap mutable property value = "+clonedEmpWrap.getProps());
    		
    	}
    
    }
    

    出力:

    empWrap mutable property value = {1=1, city=Bangalore, salary=10000}
    clonedEmpWrap mutable property value = {city=Bangalore, salary=10000}
    

    期待どおりにうまく機能しました。

これで、Javaでのオブジェクトのクローニングについてすべてです。Javaオブジェクトのclone()メソッドとそれを適切にオーバーライドする方法についてのアイデアを得られたと思います。

プロジェクトは私のGitHubリポジトリからダウンロードできます。

参考:Object cloneのAPIドキュメント

Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java