7 章: クラス II

参照型とプリミティブ (primitive) 型

インスタンス変数の型は参照型 (オブジェクト型) となる.

プリミティブ型とは JVM での管理方法が違うので, 注意が必要.

参照型とプリミティブ型の違い

JVM には [スタック領域] と [ヒープ領域] という仮想メモリ領域があり, この中で変数を処理している.

参照型変数とプリミティブ型変数では, この仮想メモリ領域での管理方法が異なる.

[スタック領域] は [ローカル変数領域] とも呼ばれ, 主にメソッド処理などで一時的な値保持に使用されるが, プリミティブ型変数 (変数で保持している値を含む) はこの [スタック領域] で処理される. これに対し, 参照型変数の場合, 保持する値 (インスタンス) はヒープ領域に作成して [スタック領域] には, その保持したインスタンスへのアドレスが格納される.

例えば, 次のような変数宣言があるとする:

int a = 1;
Integer i = new Integer();

この場合, JVM の仮想メモリは次のような状態になっている.

_images/java-class2012.gif

参照型変数の宣言自体はスタック領域で処理されるが, 初期化は [new 演算子] によりヒープ領域にオブジェクト (インスタンス) を作成して, そのオブジェクトを参照するように設定しているというところが重要.

変数同士の代入などを行った場合, [new] してオブジェクトを新たに作成していないので, 同じオブジェクトを参照する変数が増えているだけ. つまり, 代入先の変数からそのオブジェクトに変更を加えた場合, 代入元の変数のオブジェクトにも影響している (同じものを参照しているのが当然)

注意:

参照型変数を初期化せずに変数宣言だけのときは, その変数には [null] が格納される.

Sample0701.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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]);
    }
}

上記のプログラムの実行結果:

[wtopia 7]$ java Sample0701
更新前 (プリミティブ型)
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

上記処理結果は次のような状態を示している.

_images/java-class2022.gif

注意:

同じ参照型の [String 型] や [Integer 型] などは上記のような変数同士の代入を行っても参照先の値変化による参照元への影響はい.
これらのオブジェクトはイミュータブル (Immutable) なオブジェクトと呼ばれ, インスタンスの値を後から変更できないよう設計されているから. つまりこれらの変数の値を変えるには新たにオブジェクトを作成する (new) しかないわけ.

オブジェクトのコピー

オブジェクトの変数は参照型になるので, オブジェクトをコピーをする場合, 参照型変数とオブジェクト自身を区別して考える必要がある.

参照型変数の参照情報 (オブジェクトを示す情報) のみのコピーでよいのか, 参照しているオブジェクト自身もコピーが必要かを検討しなくはいけない. これらの違いにより参照型のコピーには次の2種類が存在する:

1. 浅いコピー (shallow copy, シャローコピー)
2. 深いコピー (deep copy: ディープコピー)

浅いコピー (shallow copy: シャローコピー)

参照型変数を [=] (代入演算子) を使ってコピーを行う. つまり, 参照情報のみコピーすることを指す.

_images/java-class2032.gif

注意:

この場合, コピー元とコピー先は同じオブジェクトを参照している.

Sample0702.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// コピー対象クラス

class Sample0702Copy{
    String s = "nnnn";
}

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 = "wtopia"; // コピー先のも更新する
	System.out.println("コピー先 (cp2) のみ値を更新 (abc->wtopia)");
	System.out.println("cp1.s = " + cp1.s);
	System.out.println("cp2.s = " + cp2.s);
    }
}

上記のプログラムの実行結果:

[wtopia 7]$ java Sample0702
コピー直後の状態
cp1.s = abc
cp2.s = abc
コピー先 (cp2) のみ値を更新 (abc->wtopia)
cp1.s = wtopia
cp2.s = wtopia

深いコピー (deep copy: ディープコピー)

参照型変数の参照情報だけではなく, オブジェクト自身のコピーも行う完全なコピーを指す.

_images/java-class2042.gif

実現方法として:

1. コピー元に自身のコピーを行うメソッド (習慣として Object クラスの clone メソッドをオーバーライド) を用意する方法
2. コピー対象がシリアライズ可能 (Serializable インタフェースを実装) な場合, シリアライズを利用する方法がある.

注意:

シリアライズとは [直列化] とも呼ばれ, ファイルに保存したりネットワークで送受信できるように, バイト列や XML 形式に変換することである.
シリアライズを利用したコピーは実装が簡単になるが, ストリームを使用した読み書きが行われるため, レスポンスは劣化する.
また, [static] や [transient] 修飾子が付いたメンバはシリアライズされないので, 注意してください.

方法1. コピー対象クラスにコピーメソッドを用意する場合.

Sample0703.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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 = "wtopia"; // コピー先のみ更新する
	System.out.println("コピー先 (cp2) のみ値を更新 (abc->wtopia)");
	System.out.println("cp1.s = " + cp1.s);
	System.out.println("cp2.s = " + cp2.s);
    }
}

上記のプログラムの実行結果:

[wtopia 7]$ java Sample0703
コピー直後の状態
cp1.s = abc
cp2.s = abc
コピー先 (cp2) のみ値を更新 (abc->wtopia)
cp1.s = abc
cp2.s = wtopia

方法2. シリアライズを利用する場合.

Sample0704.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 = "wtopia"; // コピー先のみ更新する
	System.out.println("コピー先 (cp2) のみ値を更新 (abc->wtopia)");
	System.out.println("cp1.s = " + cp1.s);
	System.out.println("cp2.s = " + cp2.s);
    }
}

上記のプログラムの実行結果:

[wtopia 7]$ java Sample0704
コピー直後の状態
cp1.s = abc
cp2.s = abc
コピー先 (cp2) のみ値を更新 (abc->wtopia)
cp1.s = abc
cp2.s = wtopia

オブジェクトの比較

参照型変数に格納しているのはオブジェクトへの参照情報であるから, 参照型変数の比較を行う場合も注意が必要.

例えば, プリミティブ型の変数同士で [==] (比較演算子) を用いて比較すれば変数内の値が等しいかどうかが分かるが, 参照型変数同士の場合では, 参照先が同じかどうかを判定することになりオブジェクトの値を比較していない. つまり, 参照先が異なればオブジェクトの値が同じであっても違うものと判断される.

_images/java-class2051.gif

参照型変数で保持している値が同じであるかを比較するには, [equals メソッド] を使用する. [equals メソッド] は [java.lang.Object クラス] に実装しているため, すべてのクラスで使用できる.

注意:

独自に作成したクラスなどは等価であることを正しく評価するようにオーバーライドする必要がある (そのままでは参照情報のみ比較になる).

Sample0705.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Sample0705{
    public static void main(String[] args){
	String s1 = new String("ABC"); // オブジェクト生成を明示した書式
	String s2 = new String("ABC"); // オブジェクト生成を明示した書式
	
	// String s1 = "abc";
	// String s2 = "abc";
	// s1 == s2

	System.out.println("変数 s1 = " + s1 + ", " + "変数 s2 = " + s2 + " の時");

	System.out.println("[==] 演算子で比較した結果:");
	
	if(s1 == s2){
	    System.out.println("変数 s1 と変数 s2 は同じである");
	}else{
	    System.out.println("変数 s1 と変数 s2 は異なる");
	}

	System.out.println("[equals メソッド] を使用して比較した結果:");
	if( s1.equals(s2) ){
	    System.out.println("変数 s1 と変数 s2 は同じである");
	}else{
	    System.out.println("変数 s1 と変数 s2 は異なる");
	}
	
    }
}

上記のプログラムの実行結果:

[wtopia 7]$ java Sample0705
変数 s1 = ABC, 変数 s2 = ABC の時
[==] 演算子で比較した結果:
変数 s1 と変数 s2 は異なる
[equals メソッド] を使用して比較した結果:
変数 s1 と変数 s2 は同じである

ガベージコレクション

オブジェクトは生成 ([new]) されると, メモリ上のヒープ領域に格納されているが, メモリも当然有限なリソースである. 従って使われなくなったオブジェクトをメモリ上から開放する仕組みが必要になる.

C++ などはメモリの開放もプログラマ上で明示的に記述しなければいけないが, Java は JVM が自動的に使われなくなったオブジェクトの開放を行う.

この JVM によるオブジェクトの開放処理を [ガベージコレクション] と言う.

JVM が自動的にオブジェクトの開放を行うが, コーディングをする上で少なくとも次ぎの事柄について認識して必要がある:

1. 使用されていないオブジェクトとは具体的にどのように判断しているのか
2. ガベージコレクションの処理が行われるタイミング
_images/java-class2061.gif

逆にいうと, 実際に使用されていないオブジェクトであっても変数などに格納したままで参照していると, そのオブジェクトは開放されずに残ってしまう. 従って使用しなくなったオブジェクトを参照している変数に対しては次のように明示的に初期化を行う:

参照型変数 = null;

ガベージコレクションの動作時期

通常, ガベージコレクションの動作はシステムに依存 (システム毎に最適化) するものであり, 基本的にプログラムによる制御は行わないようにする.

しかし, Java の標準クラスである [System クラス] には [gc メソッド] が用意されている, このメソッドを呼ぶことでガベージコレクションを実装することができる:

java.lang.System.gc();

注意:

[java.lang] パッケージは省略可

ガベージコレクションの目的はヒープ領域の開放であるから処理が行われる頻度や処理する時間はヒープ領域の容量で決定される.

ヒープ領域が大きいと処理する頻度が少なくなるが, 処理する時間は増える. 逆にヒープ領域が少ないと処理時間は少なくて済むが, 処理が頻繁に行われることとなる.

これを踏まえてヒープ領域の設定をすることが大切.

ヒープ領域の容量は JVM の起動時に次のオブションを指定することで設定する. ヒープ領域が不足してしまうと [java.lang.OutOfMemoryError] が発生し強制終了となるので, 設定には注意してください.

_images/jvm1.gif

例として, ヒープ領域 を下記のように設定し, 実行結果は:

[wtopia 7]$ java -Xms1M -Xmx18M Sample0705
Error occurred during initialization of VM
Incompatible minimum and maximum heap sizes specified
[wtopia 7]$ java -Xms1M -Xmx19M Sample0705
変数 s1 = ABC, 変数 s2 = ABC の時
[==] 演算子で比較した結果:
変数 s1 と変数 s2 は異なる
[equals メソッド] を使用して比較した結果:
変数 s1 と変数 s2 は同じである
[wtopia 7]$ java -Xms1M -Xmx20M Sample0705
変数 s1 = ABC, 変数 s2 = ABC の時
[==] 演算子で比較した結果:
変数 s1 と変数 s2 は異なる
[equals メソッド] を使用して比較した結果:
変数 s1 と変数 s2 は同じである