2019/02/06
sponsored link
参照型の反対は?
初心者にとってちょっと難しい参照型変数というものの解説をします。
参照型変数って言うぐらいだから、変数の種類を分類しているというのが分かると思います。分類してるってことは、参照型じゃない変数があるわけです。
参照型じゃないのは何かと言うと、値型です。
変数の中身に、参照値が入っているのか?それとも値そのものが入っているのか?の違いで、そのように分類します。←この意味をこれから解説しますので分からなくて大丈夫です。
ただ、値型変数っていうのは言い方を変えると基本データ型変数のことなので、参照型の反対は基本データ型であるともと言えます。
言葉の意味としては、参照型の反対は値型だと言うことが、このページを読み終わった頃には理解できるはずです。
基本データ型と参照型は何がどう違うのか?それが分かれば参照型変数の仕組みは簡単に理解できるので、まずは基本データ型変数の仕組みを説明したいと思います。
基本データ型変数のメモリ使用領域
基本データ型変数というのは、
boolean | 1bit |
---|---|
byte | 8bit |
char | 16bit |
short | 16bit |
int | 32bit |
float | 32bit |
long | 64bit |
double | 64bit |
の8つです。それぞれの詳細は基本データ型変数についてを見てもらうとして、注目は上記のbit数です。これは何を表しているのかというと、それぞれの変数がどれだけのメモリ領域を消費するかです。
1bitというのは2進数1桁ということです。例えばboolean型の変数というのはtrueかfalseのどちらかなので、情報量としては1bitで事足りるわけです(0か1のどちらか)。変数をよく箱に例えますが、そのノリで行くとbit数は箱の大きさを表していると言えます。
変数を使う時にはまず宣言をします。
int a;
この時点で、Java仮想マシンは、メモリ上に32bitの「a」という領域を確保します。
int a; a = 5;
続いて「a」に5を代入しました。これは、先ほど確保したメモリ上の「a」という領域に5という値を保持させたんです。
コンピュータは全て0か1の2進数で処理していますので、5なんていう数字を知りません。なのでメモリ内では5も2進数として処理されます。5を2進数で表すと何になりますか?2の2乗+1だから101ですね。101ということは2進数3桁です。2進数3桁ということは3bitで事足りますね。「a」はint型の変数なので32bitのメモリ領域を確保したのに、3bitしか使わなかったから、29bit余りましたね・・、ってそうじゃないんです!
int型の変数32bit分の領域を確保した以上は、たった2進数3桁で表わせる5であろうとも32bitをフルに使って保持します。5なら、101の前に0を29個つければ情報量としては32bitになりますよね。誰も0の数を数えてはくれないと思いますが、一応書きますと、
00000000000000000000000000000101
↑これで、5を表す32bit(2進数32桁)の情報です。
このようにint型の箱の中に入るのはどんな値であろうとも32bitなんです。それ以上でもそれ以下でもなく2進数32桁きっちりです。その代わり、-2の31乗から2の31乗-1までの値しか入りません。それ以上絶対値が大きい数値を入れるには32桁では足らないからです。
つまり、何が言いたいのかというと、基本データ型の変数というのは、型によってその大きさ(bit数)がきっちり過不足無く決まっているということです。ということは、変数を宣言した時点でメモリ内のその変数の使用領域を過不足なく確保できるわけです。その決まった領域内に(その型の範囲内の値ならば)どんな値でも保持することができるんです。
もちろんint型に限らず基本データ型の変数はbit数こそ違えど、仕組みは全く同じです。
参照型変数とは?
では、参照型というのはどういうことなのか?参照型変数である配列変数を使って説明しますので、以下の配列変数の宣言と初期化の例を見て下さい。
int[] a; //① a = new int[]{1,2,3}; //②
①int型の配列変数aを宣言。
②int型配列{1,2,3}作って、aに代入。
配列変数も、まず宣言する必要があります。
基本データ型変数を宣言した場合は上述したようにその型が必要とするスペースをメモリ内に(名前をつけて)確保しました。必要なスペースというのはintなら32bit、doubleなら64bitです。その桁数に収まる範囲で後から実際に値を入れることができます(範囲外の値を入れるとコンパイルエラー)。
それを踏まえて、配列変数を宣言したらメモリ上でどういう処理が行われるか考えてみて下さい。
基本データ型と違って配列の場合、代入されるまでそこにどんな配列が入るのか分かりません。{1,2,3}(32bit×3)が代入されるかも知れないし、{1,2,3,4,5,6,7,8,9,10}(32bit×10)が代入されるかも知れません。とりあえず配列を入れるスペースとして512bitほど確保しておいて余ったら返そうとか、もし入りきらないようなら後から付けたそうとか、そういう処理はできません。一度、メモリ内でその変数が使うスペースを確保したら、それを後から大きさを変えたりすることはできないようになっています。
・・ということは、配列変数を宣言した際、メモリ内ではどんな大きさのスペース(bit数)をその配列変数の為に確保すればいいのか分からないですよね。
この問題を解決するのが「参照」というやつです。
配列変数aには、{1,2,3}が代入されるわけではないのです。{1,2,3}はメモリ内のどこか違う場所に作られます。その作られた{1,2,3}のある場所を示すコードが1234567だったとすると、配列変数aにはその1234567というコードが代入されるわけです。
この場所を示すコードのことを参照値と言います。
これなら、どんなサイズの配列を作ったとしても、配列変数aに代入されるのはただの場所を示すコード(参照値)なので、配列の要素数(bit数)は関係ありません。配列が長すぎて(情報量が大きすぎて)配列変数aに入りきらないということはありません。
そして配列変数を宣言した際に確保するべきメモリスペースは、参照値が納まるスペースでいいので、そのスペースをきっちり過不足なく確保することができます。ちなみに参照値はおそらく16進数7桁で表わされる(配列変数aをprintメソッドの引数に渡して出力すると見れます)ようなので、2の4乗の7乗bitが、配列変数の為に確保されるスペースなんだと思います。推測です。。(サイズが大きすぎるのでおそらく誤りです。コメントいただいた方、ありがとうございます。)
追記 16進数7桁っていうのは、4bit×7で28bitですね。(アホな間違いをコメント欄にて丁寧に指摘してくださり感謝です)
このように、値(配列)そのものを保持するのではなく、その値(配列)が置いてある場所を示すコードを保持している変数を、参照型変数と言います。
値の代入と参照値の代入
以上を踏まえて、問題です。
以下のプログラムで一体何が出力されるでしょうか?これは参照型変数と基本データ型変数の大きな違いが分かるプログラムです。ゆっくり一行ずつ理解していけば全然難しくはないので、何が出力されるか自分で考えてみてください。
Test.java
public class Test{ public static void main(String[] args){ /*問1*/ int[] a = new int[]{1,2,3};//① int[] b = a; //② b[0] = 5; //③ System.out.println(a[0]); /*問2*/ int c = 1; //④ int d = c; //⑤ d = 5; //⑥ System.out.println(c); } }
問1
①int配列aを宣言、要素は1,2,3。
②int配列bを宣言、そこに配列変数aを代入。(ここがポイント!配列変数aには何が入っていたんでしたか?)
③b[0]に5を代入。
さて、a[0]の値は何でしょうか??
問2
④int変数cを宣言し、そこに1を代入。
⑤int変数dを宣言し、そこにcを代入。
⑥dに5を代入。
さて、cの値は何でしょうか??
分かりますでしょうか?本気で考えてみて下さい。参照型変数の仕組みを理解している人は、この問題が簡単に解けると思います。答えはこちら↓
aの参照値をbに代入するということは、aとbが同じ参照値を持つことになり、aもbも同じ実体(配列)を参照する状態になります。実体(配列)はあくまで一つしかないんです。つまり、a[0]とb[0]は全く同じものを指しているわけです。
基本データ型の場合は、それぞれの箱の中にそれぞれの値が入っているだけです。箱の中身が実体なので箱の数だけ実体があるということになります。代入するというのはその値をコピーして渡しているだけです。
オブジェクトと参照型
参照型に分類されるのは、上述の配列変数ともう一つ、クラス型変数があります。クラス型変数というのはすなわちオブジェクトを保持する変数です。
配列変数に配列そのものではなくその参照値(その配列の場所を示すコード)が入っているように、クラス型変数には、オブジェクトそのものが入っているのではなくその参照値(そのオブジェクトの場所を示すコード)が入っています。
おそらく一番よく使われるクラス型変数Stringを例に見てみましょう。
String s = "あいうえお";
Stringオブジェクトの作り方はちょっと特殊なので、細かい説明はそもそもStringって何?に譲るとして、上記はString変数sに”あいうえお”を代入しています。これで、変数sは文字列”あいうえお”を保持するStringオブジェクトを参照するようになります。
Stringクラスは、ご存じのように文字列を保持することができるのですが、基本的にその文字列を書き換えることはできないようになっています。
はぁ?書き換えることができない?
いやいやいやいや、簡単に書き換えられるよ。
String s = "あいうえお";
s = "かきくけこ";
ほら。sの中身は”かきくけこ”に変わったやん。
と思っているのは残念ながら素人です。。
s = "かきくけこ";
これは実は、”かきくけこ”という文字列を保持したオブジェクトを新たに作ってその参照値をsに代入しているだけです(つまりこの時点でStringオブジェクトは2つ存在)。その前に作った”あいうえお”オブジェクトはなんら変化していません。sの参照先が”あいうえお”オブジェクトから”かきくけこ”オブジェクトに変わっただけです。
つまり、変数s(オブジェクトへの参照値)が書き換えられただけで、それぞれのStringオブジェクトが持つ文字列自体を書き換えたわけではないのです。
equals( )と、比較演算子「==」
Stringクラスで定義されているインスタンスメソッドの一つに、equals( )というのがあります。equalsメソッドは、その名の通り、文字列が等しいかどうかを判定するメソッドです。
等しいかどうかを判定すると言えば、比較演算子の「==」がありますよね。equalsメソッドと、比較演算子「==」の働きの違いが分かると、参照型というものの理解がより深まると思うので、ちょっとご紹介。
public boolean equals(Object anObject)
※このequalsメソッドは元々はStringクラスのメソッドではなくObjectクラスで定義されているもので、それをStringクラスでオーバーライド(上書きみたいなこと)することによって再定義しています。
String s = "あいうえお";
String t = "あいうえお";
boolean b = s.equals(t);
そのStringインスタンスが保持する文字列と、引数に渡した文字列が全く同じ文字列ならば、戻り値としてtrueを返し、同じでなかったらfalseを返します。ちなみに上記では、sが保持している文字列も、引数に渡されたtが保持する文字列も、全く同じ”あいうえお”なのでtrueを返し、bにはtrueが入ります。
では、こちらをご覧下さい。
String s = "あいうえお";
String t = "あいうえお";
boolean b = (s == t);
比較演算子の「==」です。sとtが同じならばtrueを返します。さて、bにはtrueが入るでしょうか?falseが入るでしょうか?
答えは、falseです。両方とも”あいうえお”じゃないか!と怒った人は残念ながら素人です。[追記]すみません。間違っておりました。。理解を深める為にも以下の間違ったままの文章を載せておきます。ぜひ何がどう間違っているか理解して下さい。
この「==」は何を比べているかと言うと、それぞれの変数の中に入っている値です。String変数には何が入っているんでしたか?クラス型変数には何が入っているんでしたか?そのオブジェクトの場所を示す参照値です。何度も言うようですが、変数sの中に”あいうえお”が入っているのではありません。変数sの中に入っているのは、”あいうえお”の置き場所を示すコードです。そして、sが参照する”あいうえお”とtが参照する”あいうえお”は同じ文字列ではありますが、決して同じオブジェクトではありません。
String s = "あいうえお"; String t = "あいうえお";
これは、それぞれ別のオブジェクトを作って、その参照をそれぞれのString変数(sとt)に代入しています。保持する文字列は同じですが、同じオブジェクトを参照しているわけではありません。ということは、参照値もそれぞれ違って当然ですね。だから、
String s = "あいうえお"; String t = "あいうえお"; boolean b = (s == t);
変数sに入っている参照値と、変数tに入っている参照値を比べた結果、falseが返されてbにはfalseが入ります。つまりこの「==」の場合、そのインスタンスが保持する文字列は一切関係ないということです。
言い訳じゃありませんが、考え方自体は間違ってはいません。ただ、同じ文字列を再度作る場合、先に作ってあるオブジェクトを自動的に再利用するということみたいです。
この場合、2回目の”あいうえお”の時点で、新たに”あいうえお”オブジェクトを作るのではなく、”あいうえお”オブジェクトならここにあるよってことで、先に作った”あいうえお”オブジェクトの参照値をtに代入しているわけです。なのでsもtも同じオブジェクトを参照していることになります。つまりsとtは同じ参照値を持つので、bにはtrueが入ります。
間違った情報を載せてしまい申し訳ないです。コメント欄にて指摘していただいたジェイさんありがとうございました。
では、もう一つ。
String s = "あいうえお";
String t = s;
boolean b = (s == t);
今度は、String tにsを代入しています。これは一体、何を代入しているのか?それはやはり参照値です。これでtとsは同じ参照値を持つことになります。言い方を変えると、sとtは同じオブジェクトを参照しているということになります。
この場合、bには何が入るでしょうか?
答えはtrueです。sとtの中身は全く同じ参照値が入っているので、「==」で比べた結果、trueになります。もちろん同じオブジェクトを参照しているので保持している文字列も等しいことになります。
equalsメソッドと、「==」の違いが分かりましたでしょうか?
もし、String変数が保持する文字列が等しいかどうかを比べたかったら、「==」ではなく、equalsメソッドを使わないとダメだということです。ここは初心者が間違いやすいところだと思いますので気をつけてください。
まとめ
基本データ型変数の中に入っているのは、値そのものです。値というのは整数や小数や文字(char)やtrue(boolean)などのことです。
それに対して、参照型変数の中に入っているのは、その変数が保持しているオブジェクト(あるいは配列)の参照値です。
今だけ→転職できなければ全額返金の「エンジニア転職保証コース」
絶対エンジニアになる!→テックエキスパート
フリーランスエンジニアの収入例を見てみる→レバテックフリーランス
コメント
失礼します。上のコメントと関連するのですが、
16進数7文字なら28bitのはずです。
(16進文字が4bit、7文字なので4bit*7=28bit)
また、16^7=2^28は、
16進7桁の数字がとりうる値のパターン数です。
by D4Y2a 2017/07/06 15:03
>D4Y2aさん
本当ですね。bit数と、取りうるパターン数がごっちゃになってました。
1bitで2パターン
4bitで16パターン
ですね。
ご指摘ありがとうございます。
by Nobuo@管理人 2017/07/06 15:56
[…] Javaのプリミティブ型と参照型について 【Java】 基本データ型 と 参照型 の違い […]
by 参照を知らずに開発を続けていいのか | IT技術情報局 2018/03/04 16:56
むちゃくちゃわかりやすい
本当にありがとうございます。
by 匿名 2018/06/13 13:11
[…] https://nobuo-create.net/sanshougata/ […]
by 基本データ型と参照型 | IT技術情報局 2018/11/20 12:50
とてもわかりやすいし、面白いです。
良い勉強になりました!
by 匿名 2020/09/10 11:36
[…] 扱いの違いについてはこちらのサイトが詳しい。こちらでは比較演算子を使って実際に扱いが異なることを確認してみる。 […]
by Java 変数型 変数の扱い | Programming自習室 2021/05/10 20:26