Javaでイミュータブルクラスを作成する方法
はじめに
この記事では、Javaプログラミングにおいて不変クラスを作成する方法について概説しています。
オブジェクトが初期化された後にその状態が変化しない場合、そのオブジェクトは不変(immutable)です。例えば、Stringは不変クラスであり、一度インスタンスが生成されると、Stringオブジェクトの値は変わりません。JavaにおけるStringクラスがなぜ不変なのか、詳しく学んでください。
不変なオブジェクトは更新できないため、プログラムは状態の変更ごとに新しいオブジェクトを作成する必要があります。ただし、不変なオブジェクトには以下の利点もあります。
- An immutable class is good for caching purposes because you don’t have to worry about the value changes.
- An immutable class is inherently thread-safe, so you don’t have to worry about thread safety in multi-threaded environments.
Javaのマルチスレッディングについてもっと学び、Javaマルチスレッディングのインタビューに関する質問を閲覧してください。
Javaにおいてイミュータブルクラスを作成する
Javaでイミュータブルなクラスを作成するには、以下の一般的な原則に従う必要があります。
-
- クラスをファイナルとして宣言して、拡張できなくする。
-
- すべてのフィールドをプライベートにし、直接アクセスを許さないようにする。
-
- 変数に対するセッターメソッドを提供しない。
-
- 変更可能なフィールドをすべてファイナルとして、値を一度だけ割り当てられるようにする。
-
- フィールドの初期化は、ディープコピーを実行するコンストラクタメソッドを使用して行う。
- ゲッターメソッドではオブジェクトのクローンを作成し、実際のオブジェクト参照を返さないようにする。
以下のクラスは、イミュータビリティの基礎を示す例です。FinalClassExampleクラスはフィールドを定義し、オブジェクトを初期化するためにディープコピーを使用するコンストラクタメソッドを提供します。FinalClassExample.javaファイルのmainメソッドのコードは、オブジェクトのイミュータビリティをテストします。
「FinalClassExample.java」という新しいファイルを作成し、以下のコードをコピーしてください。
import java.util.HashMap;
import java.util.Iterator;
public final class FinalClassExample {
// fields of the FinalClassExample class
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 function for mutable objects
public HashMap<String, String> getTestMap() {
return (HashMap<String, String>) testMap.clone();
}
// Constructor method performing deep copy
public FinalClassExample(int i, String n, HashMap<String,String> hm){
System.out.println("Performing Deep Copy for Object initialization");
// "this" keyword refers to the current object
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;
}
// Test the immutable class
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);
// print the ce values
System.out.println("ce id: "+ce.getId());
System.out.println("ce name: "+ce.getName());
System.out.println("ce testMap: "+ce.getTestMap());
// change the local variable values
i=20;
s="modified";
h1.put("3", "third");
// print the values again
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());
}
}
プログラムをコンパイルして実行してください。
- javac FinalClassExample.java
- java FinalClassExample
Note
以下の出力が得られます。
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}
出力を示すと、HashMapの値は変わっていません。なぜなら、コンストラクタがディープコピーを使用しており、ゲッター関数は元のオブジェクトのクローンを返すからです。
ディープコピーとクローニングを使わない場合、何が起こるのですか?
FinalClassExample.javaファイルに変更を加えて、ディープコピーの代わりにシャローコピーを使用し、オブジェクトではなくコピーを返すとどうなるかを示すことができます。オブジェクトはもはや不変ではなく、変更することができます。例ファイルに以下の変更を加えてください(またはコード例からコピー&ペーストしてください)。
- Delete the constructor method providing deep copy and add the constructor method providing shallow copy that is highlighted in the following example.
- In the getter function, delete return (HashMap<String, String>) testMap.clone(); and add return testMap;.
例のファイルは今、このようになっています。
import java.util.HashMap;
import java.util.Iterator;
public final class FinalClassExample {
// fields of the FinalClassExample class
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 function for mutable objects
public HashMap<String, String> getTestMap() {
return testMap;
}
//Constructor method performing shallow copy
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;
}
// Test the immutable class
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);
// print the ce values
System.out.println("ce id: "+ce.getId());
System.out.println("ce name: "+ce.getName());
System.out.println("ce testMap: "+ce.getTestMap());
// change the local variable values
i=20;
s="modified";
h1.put("3", "third");
// print the values again
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());
}
}
プログラムをコンパイルして実行してください。
- javac FinalClassExample.java
- java FinalClassExample
あなたは次の出力を受け取ります。
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の値が変更されたことを示しています。また、ゲッター関数には元のオブジェクトへの直接参照があります。
結論
Javaで不変クラスを作成する際には、ディープコピーの重要性を含む一般的な原則のいくつかを学びました。さらにJavaのチュートリアルで学びを深めましょう。