Javaプログラミングをする上で"参照"について学んだことがあるかと思います。すんなり理解できた人もいれば、なんとなくでしか理解していない人、もしくは理解できず困っている人もいると思います。この記事はJavaの"参照"について「何となくわかっているけど他人に説明はできないな」と思っている人や「言葉で説明されてもイメージが湧かないよ」と言う人に図を使って説明する記事になります。図に関して、実際に目で見えないものを表現しているのと、最低限の情報で構成しているので正確ではありません。あらかじめご了承ください。
プリミティブ型と参照型
Javaの型にはプリミティブ型(基本データ型)と参照型と二種類の型が存在します。プリミティブ型は全て小文字で始まる型で、メモリに直接書き込まれる具体的なデータ値(数値や文字)を代入することができます。それと参照型と違いメソッドを持たないのも特徴の一つと言えます。
型名 | 分類 | サイズ(bit) |
---|---|---|
long | 整数 | 64 |
int | 整数 | 32 |
short | 整数 | 16 |
byte | 整数 | 8 |
double | 小数 | 64 |
float | 小数 | 32 |
boolean | 真偽 | ? |
char | 文字 | 16 |
上記の8つがプリミティブ型で、それ以外は全て参照型になります。プリミティブ型と違い参照型には、具体的な値ではなくnewして生成したオブジェクトへのポインタ(メモリ上のアドレス)を参照値として代入します。ここで言うオブジェクトはインスタンスと同意です。
配列とString
個人的に勘違いしやすいと思うのは、配列とStringです。配列もStringも参照型なので、オブジェクトを生成します。
// プリミティブ型
int i = 1;
// 参照型
int[] array = {1, 2, 3};
String string = "りんご";
int[]型はintに[](ブランケット)を付けて表現するので勘違いしやすいと思います。int型とint[]型は違うので気をつけましょう。String型は文字列を代入しますが、Stringオブジェクト内部で文字列をchar配列として扱っていることに気をつけましょう。下の三つはオブジェクト生成の記法は違いますが、全て等価になります。同値と等価は意味が違うので気をつけてください。同値と等価についてはこちらの記事を読んでもらえると嬉しいです。
String a = "りんご";
String b = new String("りんご");
String c = new String(new char[]{'り', 'ん', 'ご'});
System.out.println(a); // りんご
System.out.println(b); // りんご
System.out.println(c); // りんご
String型はJavaを学習し始めるとプリミティブ型と一緒に覚えることがほとんどだと思います。なのでプログラミング経験が浅い人は、その違いを深く理解できていないままなことが多いと思います。では、図を使ってプリミティブ型と参照型の違いを表現してみます。
int型の変数はメモリのデータ値を直接代入します。int[]型は、オブジェクトを生成しメモリに展開、そのオブジェクトのポインタを参照値として変数に代入します。String型は、内部のchar[]変数がchar[]オブジェクトの参照値を持っているので、このような図になりました。結局コンピュータなのでほとんどがメモリ上の話ですが、見やすさを考えて変数と値を分けて書いています。
nullとは
nullとは参照型変数に代入できるもので、その変数の参照先がないことを表現することができます。java.lang.NullPointerException(いわゆるヌルポ)が発生するのは、参照先がない変数のメソッドを実行しようとしたときです。
変数に代入する
変数とは名前をつけて確保したメモリ領域のことで、その領域に値を格納することを代入と言います。変数間の代入とは、右辺の値をコピーして左辺に格納しデータを共有することを言います。
参照を理解していないと正しいプログラミングをすることができないので、しっかりとイメージすることができるようになりましょう。
では実際にコードを書いて値の受け渡しを見ていきます。
//【A】プリミティブ型
int intA = 1;
int intB = intA;
intB = 2;
System.out.println(intA); // 1
System.out.println(intB); // 2
//【B】参照型
char[] arrayA = {'A', 'B', 'C'};
char[] arrayB = arrayA;
arrayB[0] = 'D';
System.out.println(arrayA[0]); // D
System.out.println(arrayB[0]); // D
//【C】参照型(イミュータブル)
String stringA = "文字列";
String stringB = stringA;
stringB = "もじれつ";
System.out.println(stringA); // 文字列
System.out.println(stringB); // もじれつ
【A】については説明不要ですね。注目すべきは、【B】と【C】です。【B】はarrayBに対して代入しarrayAにまで影響が及んでいますが、【C】ではstringAに影響が及んでいません。何が起きているのか、これも図を使って説明します。
【B】の③でarrayBの配列にデータ値を代入しますが、参照しているオブジェクトがarrayAと同じ(これをarrayAとarrayBは同値という)なためarrayAとarrayBの出力が同じになるのです。
【C】では③で"もじれつ"を持つ新しいStringオブジェクトを生成し参照値をstringBに代入するので、"文字列"オブジェクトへの参照が切れます。そのためstringAとstringBの出力が異なる結果となるのです。つまりStringオブジェクトの値を変更しようと思うと、内部のchar配列に対してデータ値を代入する必要があるのです。しかしStringクラスの内部のchar配列変数はprivate finalで定義してあるので、書き換えが不可能となっています。このようにオブジェクトの価値が変更不可能な設計をイミュータブル(不変)と言います。
メソッドの呼び出し
メソッド呼び出しも勘違いしやすいので、コードと図からプログラムの動きをイメージしましょう。
void main() {
String string = "文字列";
sub(string);
System.out.println(string); // 文字列
}
void sub(String string) {
string = "もじれつ";
}
これは一見すると、mainから渡した変数にsubメソッド内で別の値を代入しているように見えますね。しかし、渡しているのは変数ではなく、変数が参照しているオブジェクトの参照値だと言うことを理解してください。
メソッド内で宣言される変数はそのメソッドのローカル変数と呼び、そのメソッド内でしか使用できません。なのでmainメソッドからsubメソッドを呼び出し引数を渡しますが、これはmainメソッドのstring変数からsubメソッドのstring変数に代入が行われていることになります。これも、変数名が同じだったりするので勘違いが起きやすい箇所だと思います。
最後に
Javaの参照についての記事はたくさんWeb上にありますが、言葉とコードでの説明が大半だと思います。プログラミングセンスがある人はそれだけでも理解ができてしまうと思いますが、いまいち理解ができていない人や学習を始めたばかりの人は、この記事を見てスッキリ理解してもらえればと思います。