クラスの詳細としてインスタンス(オブジェクト)の取扱いについて詳しくみていきます。
インスタンス変数の型は参照型(オブジェクト型)となります。プリミティブ型とはJVMでの管理方法が違いますので注意が必要です。
JVMには「スタック領域」と「ヒープ領域」という仮想メモリ領域があり、この中で変数を処理しています。参照型変数とプリミティブ型変数では、この仮想メモリ領域での管理方法が異なります。
「スタック領域」は「ローカル変数領域」とも呼ばれ、主にメソッド処理などで一時的な値保持に使用されますが、プリミティブ型変数(変数で保持している値を含む)はこの「スタック領域」で処理されます。これに対し、参照型変数の場合、保持する値(インスタンス)はヒープ領域に作成して「スタック領域」には、その保持したインスタンスへのアドレスが格納されます。
例えば、次のような変数宣言があるとします。
int a = 1; Integer i = new Integer();
この場合、JVMの仮想メモリは下図のような状態になっています。
参照型変数の宣言自体はスタック領域で処理されますが、初期化は「new演算子」によりヒープ領域にオブジェクト(インスタンス)を作成して、そのオブジェクトを参照するように設定しているというところが重要です。※1
変数同士の代入などを行った場合、「new」してオブジェクトを新たに作成していませんので同じオブジェクトを参照する変数が増えているだけです。つまり、代入先の変数からそのオブジェクトに変更を加えた場合、代入元の変数のオブジェクトにも影響している(同じものを参照しているので当然ですが)事に注意してください。サンプルソース(Sample0701.java)で動作確認します。
※1参照型変数を初期化せずに変数宣言だけのときは、その変数には「null」が格納されます。
・サンプルソース(Sample0701.java)
public class Sample0701 { public static void main(String[] args) { int a = 1; // プリミティブ型変数の初期化 int b = a; // プリミティブ型変数同士の代入 int[] A = {1}; // 参照型変数の初期化(new int[];とA[0]=1;の簡略した書式) int[] B = A; // 参照型変数同士の代入 System.out.println("更新前(プリミティブ型)"); System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("「b = 2;」の更新後(プリミティブ型)"); b = 2; // 代入先変数の値を変更 System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("更新前(参照型)"); System.out.println("A[0] = " + A[0]); System.out.println("B[0] = " + B[0]); System.out.println("「B[0] = 2;」の更新後(参照型)"); B[0] = 2; // 代入先変数の値を変更 System.out.println("A[0] = " + A[0]); System.out.println("B[0] = " + B[0]); } }
・実行結果
C:\dev\java>javac Sample0701.java [Enter] C:\dev\java>java Sample0701 [Enter] 更新前(プリミティブ型) a = 1 b = 1 「b = 2;」の更新後(プリミティブ型) a = 1 b = 2 更新前(参照型) A[0] = 1 B[0] = 1 「B[0] = 2;」の更新後(参照型) A[0] = 2 ← 代入もとの変数内の値も変更されている。 B[0] = 2
上記処理結果は次のような状態を示しています。
・備考
同じ参照型の「String型」や「Integer型」などは上記のような変数同士の代入を行っても参照先の値変化による参照元への影響はありません。
これらのオブジェクトはイミュータブル(Immutable)なオブジェクトと呼ばれ、インスタンスの値を後から変更できないよう設計されているからです。つまりこれらの変数の値を変えるには新たにオブジェクトを作成する( new )しかない訳です。
オブジェクトの変数は参照型になりますので、オブジェクトをコピーをする場合、参照型変数とオブジェクト自身を区別して考える必要があります。
参照型変数の参照情報(オブジェクトを示す情報)のみのコピーでよいのか、参照しているオブジェクト自身もコピーが必要かを検討しなくてはいけません。これらの違いにより参照型のコピーにはつぎの2種類が存在します。
参照型変数を「=」(代入演算子)を使ってコピーを行う、つまり参照情報のみコピーすることをさします。
この場合、コピー元とコピー先は同じオブジェクトを参照しているので、注意が必要です。
・サンプルソース(Sample0702.java)
// コピー対象クラス class Sample0702Copy { String s = "nnn"; } public class Sample0702 { public static void main(String[] args) { Sample0702Copy cp1 = new Sample0702Copy(); cp1.s = "abc"; // 「=」によるコピー。 Sample0702Copy cp2 = cp1; System.out.println("コピー直後の状態 "); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); cp2.s = "xyz"; // コピー先のみ更新します。 System.out.println("コピー先(cp2)のみ値を更新(abc→xyz)"); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); } }
・実行結果
C:\dev\java>javac Sample0702.java [Enter] C:\dev\java>java Sample0702 [Enter] コピー直後の状態 cp1.s = abc cp2.s = abc コピー先(cp2)のみ値を更新(abc→xyz) cp1.s = xyz ← コピー元も変化してしまっている。 cp2.s = xyz
参照型変数の参照情報だけではなく、オブジェクト自身のコピーも行う完全なコピーをさします。
実現方法として、コピー元に自身のコピーを行うメソッド(慣習としてObjectクラスのcloneメソッドをオーバーライド)を用意する方法と、コピー対象がシリアライズ可能(Serializableインターフェスを実装)な場合、シリアライズを利用する方法があります。※1
※1 シリアライズとは「直列化」とも呼ばれ、ファイルに保存したりネットワークで送受信できるように、バイト列やXML形式に変換することです。シリアライズを利用したコピーは実装が間単になりますが、ストリームを使用した読み書きが行われる為、レスポンスは劣化します。また、「static」や「transient 」修飾子が付いたメンバーはシリアライズされませんので注意してください。
・方法1. コピー対象クラスにコピーメソッドを用意する場合。サンプルソース(Sample0703.java)
// Cloneableインターフェースを実装します(これがないとcloneメソッド呼びだし時に「CloneNotSupportedException」が発生)。 class Sample0703Copy implements Cloneable { String s = "nnn"; // cloneメソッドをオーバーライドします。 protected Object clone() throws CloneNotSupportedException { // このオブジェクトのコピーを記述します。 // この例だと、実はあまり意味がありません(return super.clone();でによる浅いコピーを返だけでも同じ)。 Sample0703Copy cp = new Sample0703Copy(); cp.s = this.s; return cp; } } public class Sample0703 { public static void main(String[] args) throws CloneNotSupportedException { Sample0703Copy cp1 = new Sample0703Copy(); cp1.s = "abc"; // コピー対象オブジェクトのclone()を使用してコピー。 Sample0703Copy cp2 = (Sample0703Copy)cp1.clone(); System.out.println("コピー直後の状態 "); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); cp2.s = "xyz"; // コピー先のみ更新します。 System.out.println("コピー先(cp2)のみ値を更新(abc→xyz)"); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); } }
・実行結果
C:\dev\java>javac Sample0703.java [Enter] C:\dev\java>java Sample0703 [Enter] コピー直後の状態 cp1.s = abc cp2.s = abc コピー先(cp2)のみ値を更新(abc→xyz) cp1.s = abc ← コピー元は変化なし。 cp2.s = xyz
・方法2. シリアライズを利用する場合。サンプルソース(Sample0704.java)
import java.io.*; // Serializableインターフェースが実装(シリアライズ可能)されている必要があります。 class Sample0704Copy implements Serializable { String s = "nnn"; } public class Sample0704 { public static void main(String[] args) throws IOException, ClassNotFoundException { Sample0704Copy cp1 = new Sample0704Copy(); cp1.s = "abc"; // シリアライズを利用したコピー。 -------------------------------------------------------------------------- ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(byteOut); out.writeObject(cp1); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray())); Sample0704Copy cp2 = (Sample0704Copy)in.readObject(); // --------------------------------------------------------------------------------------------------------- System.out.println("コピー直後の状態 "); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); cp2.s = "xyz"; // コピー先のみ更新します。 System.out.println("コピー先(cp2)のみ値を更新(abc→xyz)"); System.out.println("cp1.s = " + cp1.s); System.out.println("cp2.s = " + cp2.s); } }
・実行結果
C:\dev\java>javac Sample0704.java [Enter] C:\dev\java>java Sample0704 [Enter] コピー直後の状態 cp1.s = abc cp2.s = abc コピー先(cp2)のみ値を更新(abc→xyz) cp1.s = abc ← コピー元は変化なし。 cp2.s = xyz
参照型変数に格納しているのはオブジェクトへの参照情報ですから、参照型変数の比較を行う場合も注意が必要です。
例えば、プリミティブ型の変数同士で「==」(比較演算子)を用いて比較すれば変数内の値が等しいか否かが解りますが、参照型変数同士の場合では、参照先が同じか否かを判定する事になりオブジェクトの値を比較していません。つまり、参照先が異なればオブジェクトの値が同じであっても違うものと判断されます。
参照型変数で保持している値が同じであるかを比較するには、「epualsメソッド」を使用します。「epualsメソッド」は「java.lang.Objectクラス」に実装している為、全てのクラスで使用できます。※1
※1 但し、独自に作成したクラスなどは等価であることを正しく評価するようにオーバーライドする必要があります。(そのままでは参照情報のみの比較になります。)
・サンプルソース(Sample0705.java)
public class Sample0705 { public static void main(String[] args) { String s1 = new String("ABC"); // オブジェクト生成を明示した書式です。(「String s1 = "ABC"」と等価) String s2 = new String("ABC"); // オブジェクト生成を明示した書式です。(「String s2 = "ABC"」と等価) System.out.println("変数s1 = " + s1 + " 変数s2 = " + s2 + "の時"); System.out.print("「==」演算子で比較した結果 : "); if (s1 == s2) { System.out.println("変数s1と変数s2は同じです。"); } else { System.out.println("変数s1と変数s2は異なります。"); } System.out.print("「equalsメソッド」を使用して比較した結果 : "); if (s1.equals(s2)) { System.out.println("変数s1と変数s2は同じです。"); } else { System.out.println("変数s1と変数s2は異なります。"); } } }
・実行結果
C:\dev\java>javac Sample0705.java [Enter] C:\dev\java>java Sample0705 [Enter] 変数s1 = ABC 変数s2 = ABCの時 「==」演算子で比較した結果 : 変数s1と変数s2は異なります。 「equalsメソッド」を使用して比較した結果 : 変数s1と変数s2は同じです。
オブジェクトは生成(「new」)されると、メモリ上のヒープ領域に格納されていきますが、メモリも当然有限なリソースです。従って使われなくなったオブジェクトをメモリ上から開放する仕組みが必要となります。
C++などはメモリの開放もプログラム上で明示的に記述しなければいけませんが、JavaはJVMが自動的に使われなくなったオブジェクトの開放を行います。このJVMによるオブジェクトの開放処理を「ガベージコレクション」と言います。
JVMが自動的にオブジェクトの開放を行いますが、コーディングをする上で少なくとも次の事柄について認識しておく必要があります。
ヒープ領域に生成されたオブジェクトは、スタック上の変数から参照され利用されています。よってJVMはどこからも参照されていないオブジェクトを対象にガベージコレクションを行います。
逆にいうと、実際には使用されていないオブジェクトであっても変数などに格納したままで参照していると、そのオブジェクトは開放されずに残ってしまいます。従って使用しなくなったオブジェクトを参照している変数に対しては次のように明示的に初期化を行います。
参照型変数 = null;
通常、ガベージコレクションの動作はシステムに依存(システム毎に最適化)するものであり、基本的にプログラムによる制御は行わないようにします。
しかし、Javaの標準クラスである「Systemクラス」には「gcメソッド」が用意されており、このメソッドを呼ぶ事でガベージコレクションを実行する事が出来ます。
java.lang.System.gc();
※「java.lang」パッケージは省略可。
ガベージコレクションの目的はヒープ領域の開放ですから処理が行われる頻度や処理する時間はヒープ領域の容量で決定されます。ヒープ領域が大きいと処理する頻度が少なくなりますが、処理する時間は増えます。逆にヒープ領域が少ないと処理時間は少なくてすみますが、処理が頻繁に行われることとなります。これを踏まえてヒープ領域の設定をすることが大切です。
ヒープ領域の容量はJVMの起動時に次のオプションを指定する事で設定できます。ヒープ領域が不足してしまうと「java.lang.OutOfMemoryError」が発生し強制終了となりますので設定には注意してください。
オプション | 意味 |
-Xms | ヒープ領域の初期値を指定します。指定する値は1Mbyte以上で、1024byteの倍数にします。初期値は2Mbyteです。 |
-Xmx | ヒープ領域の最大値を指定します。指定する値は2Mbyte以上で、1024byteの倍数にします。初期値は64Mbyteです。 |
例として、ヒープ領域を「初期値128Mbyte、最大512Mbyte」に設定して「Sample」クラスを起動する場合の書式は次のようになります。
C:\dev\java>java -Xms128M -Xmx512M Sample