配列が勝手に書き換わる?Javaで参照型変数を安易にコピーしちゃだめな理由

配列を使って処理をしていたら何もしてないのに配列の中身が変わってしまったという経験はありませんか?例えば下の例のような処理です。

public class Test {

	public static void main(String[] args) {
		int[] a = {1, 2, 3};
		int[] b = a;
		b[0] = 100;
		System.out.println(b[0]); //100
		System.out.println(a[0]); //?
	}
}

整数が格納された配列aを作成し、同様に配列bは配列aをコピーして作成しました。
次に配列bの先頭の要素の値に100を代入した後に配列a,bそれぞれの先頭値を出力するというものです。

1つめの出力である配列bの先頭値b[0]は100であるのは問題ないでしょう。
では2つめの出力a[0]はどのように表示されるでしょうか?

配列aには特に何の処理も行っていないのでa[0]は1となりそうです。しかしこのプログラムを実行すると100と出力されます。

これは配列が参照型のデータであることが原因です。今回は参照型とはなにか、そして使用にあたってどのような注意が必要かについて紹介します。

変数の型は2種類ある

プログラミング言語にはたくさんのデータの型が存在します。名称は言語によって異なりますが、例えばJavaでは整数を格納するint型や文字列を格納するString型などが挙げられます。

そしてこれらのデータ型はさらに大きな分類として2種類に分けることができます。それが基本型参照型です。両者の違いはデータを変数に格納する方法にあります。

データ型

Javaでは参照型の変数に配列や文字列、クラスなどが該当します。JavascriptやPythonなど他の言語でも多少の違いはあれ同様の概念は存在します。

基本型変数は値、参照型変数はアドレスを格納

では変数の格納の仕方がどのように異なるかというと、基本型は変数に直接値が格納されるのに対して、参照型では値が存在するメモリのアドレスが格納されます。

例えば

int i = 1;

のように整数型の変数iを宣言すると変数iの情報がコンピュータ上のメモリに作られて、その領域に1という値が直接格納されます。

一方で配列の場合は異なります。

int[] a = {1,2,3};

のように配列変数aを宣言すると、変数aの領域がメモリ上に作成され、それとは別の領域に配列の値{1, 2, 3}が格納されます。

では変数aには何が格納されるかというと、配列の値が存在するメモリのアドレスが格納されています。配列変数はそのアドレスを「参照」して値を表示していることから参照型と呼ばれています。

参照型のメモリの使い方

例題を振り返る

以上をふまえて冒頭の例を考えます。

public class Test {

	public static void main(String[] args) {
		int[] a = {1, 2, 3};
		int[] b = a;
		b[0] = 100;
		System.out.println(b[0]); //100
		System.out.println(a[0]); //?
	}
}

まず配列変数aを宣言すると変数aには配列が存在するメモリのアドレスが格納されます。

配列aを作成

続いて変数bは変数aを代入すると宣言されているため、変数bも変数aと同様に配列が存在メモリのアドレス(7032番地)が格納されることになります。

つまり変数aと変数bは同じメモリのアドレスを参照して値を返しているということになります。

配列bを作成してaを代入

となればあとは問題の原因はわかりますね。変数bの先頭値を100に書き換える処理は参照先の配列の値を書き換えるということです。変数bと変数aの参照先は等しいので変数aの値も書き換わってしまいました。

したがって変数a,b両方の先頭値が100と出力されたというわけです。

参照先の先頭値に100を代入

配列のコピーはclone()をつかおう

このような問題を回避するためにはclone()というメソッドを使う必要があります。

package chapter5;

public class Test {

	public static void main(String[] args) {
		int[] a = {1, 2, 3};
		int[] b = a.clone();
		b[0] = 100;
		System.out.println(b[0]); //100
		System.out.println(a[0]); //1
	}

}

clone()を使うことで配列aをメモリの別の場所にコピーしてそのアドレスを変数bに格納するという処理が行われるため上の例の出力は直感に矛盾しない出力となるはずです。

参照型の変数がないと不便

そもそもなぜ参照型という一見扱いづらそうなデータ型が存在するかというと、基本型の変数だけでは使い勝手が悪いからです。

Javaでは変数が宣言されると、宣言されたデータ型に応じて決まった大きさの領域がコンピュータのメモリ上で確保されます。

基本型と参照型のメモリの使い方

しかし宣言と同時に変数のメモリサイズを確定させてしまうと場合によっては使い勝手が悪い場合があります。

例えばデータベース内のレコードの名前全てを配列に入れたいと思ったときに、レコードの総数がいくつになるかわかりませんよね。なので配列変数を宣言した瞬間にメモリサイズが決まってしまうと最悪入り切らないという事態が起きてしまいます。

そのため配列など参照型の変数ではメモリの別の領域(ヒープ:上図の青い領域)に必要な分だけ領域を確保して値を格納し、変数には格納先のアドレスを指定するという方式をとっています。

コメント

タイトルとURLをコピーしました