Javaで不変クラスを作成する方法

イントロダクション

この記事では、Javaプログラミングでイミュータブルなクラスを作成する方法について概説します。

オブジェクトが初期化された後に状態が変化しない場合、そのオブジェクトはイミュータブルです。例えば、Stringはイミュータブルなクラスであり、インスタンス化されるとStringオブジェクトの値は変わりません。JavaにおけるStringクラスがなぜイミュータブルなのかについて詳しく学びましょう。

イミュータブルなオブジェクトは更新できないため、状態の変更ごとに新しいオブジェクトを作成する必要があります。しかし、イミュータブルなオブジェクトには以下の利点もあります:

  • 値の変更について心配する必要がないため、キャッシュ目的に適しています。
  • イミュータブルなクラスは元々スレッドセーフなので、マルチスレッド環境でのスレッドセーフ性について心配する必要がありません。

Javaのマルチスレッドについて詳しく学び、Javaマルチスレッドのインタビュー質問を参照してください。

Java でイミュータブルクラスを作成する

Java でイミュータブルクラスを作成するには、次の一般的な原則に従う必要があります:

  1. クラスをfinalとして宣言して、拡張できないようにします。
  2. すべてのフィールドをprivateにすることで、直接アクセスが許可されないようにします。
  3. 変数のためのセッターメソッドを提供しないでください。
  4. すべての可変フィールドをfinalにすることで、フィールドの値が一度だけ代入されるようにします。
  5. すべてのフィールドをディープコピーを行うコンストラクタメソッドを使用して初期化します。
  6. ゲッターメソッドでオブジェクトのクローンを作成し、実際のオブジェクト参照ではなくコピーを返します。

次のクラスは、イミュータビリティの基本を示す例です。 FinalClassExample クラスは、フィールドを定義し、オブジェクトを初期化するためにディープコピーを使用するコンストラクタメソッドを提供します。 FinalClassExample.java ファイルの main メソッドのコードは、オブジェクトのイミュータビリティをテストします。

FinalClassExample.java という新しいファイルを作成し、次のコードをコピーしてください:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// FinalClassExample クラスのフィールド
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// 可変オブジェクトのための Getter 関数

	public HashMap<String, String> getTestMap() {
		return (HashMap<String, String>) testMap.clone();
	}

	// ディープコピーを実行するコンストラクタメソッド
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// "this" キーワードは現在のオブジェクトを指します
		this.id=i;
		this.name=n;

		HashMap<String,String> tempMap=new HashMap<String,String>();
		String key;
		Iterator<String> it = hm.keySet().iterator();
		while(it.hasNext()){
			key=it.next();
			tempMap.put(key, hm.get(key));
		}
		this.testMap=tempMap;
	}

	// Immutable クラスをテストする

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// ce の値を表示
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// ローカル変数の値を変更
		i=20;
		s="modified";
		h1.put("3", "third");
		// 再び値を表示
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

プログラムをコンパイルして実行:

  1. javac FinalClassExample.java
  2. java FinalClassExample

注意:ファイルをコンパイルすると、次のメッセージが表示される可能性があります:Note: FinalClassExample.java uses unchecked or unsafe operations、なぜなら Getter メソッドが HashMap<String,String> から Object への未検査キャストを使用しているためです。この例の目的では、コンパイラの警告を無視できます。

次の出力が得られます:

Output
Performing Deep Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second} ce testMap after changing variable from getter methods: {1=first, 2=second}

出力は、コンストラクタがディープコピーを使用し、Getter 関数が元のオブジェクトのクローンを返すため、HashMap の値が変更されていないことを示しています。

ディープコピーとクローンを使用しない場合の動作

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// FinalClassExample クラスのフィールド
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// mutable オブジェクトのためのゲッター関数

	public HashMap<String, String> getTestMap() {
		return testMap;
	}

	// シャローコピーを実行するコンストラクタメソッド

	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Shallow Copy for Object initialization");
		this.id=i;
		this.name=n;
		this.testMap=hm;
	}

	// 不変のクラスをテスト

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// ce の値を出力
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// ローカル変数の値を変更
		i=20;
		s="modified";
		h1.put("3", "third");
		// 再び値を出力
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

プログラムをコンパイルして実行してください:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Output
Performing Shallow Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second, 3=third} ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}

出力には、コンストラクタメソッドが浅いコピーを使用しているため、HashMapの値が変更されました。getter関数には元のオブジェクトへの直接参照があります。

結論

Javaで不変のクラスを作成する際に従う一般的な原則のいくつかを学びました。これには、ディープコピーの重要性も含まれます。さらに詳しくは、Javaチュートリアルで学習を続けてください。

Source:
https://www.digitalocean.com/community/tutorials/how-to-create-immutable-class-in-java