どうも、フリーランスエンジニアのこじらです。
今回は参照渡し、値渡しについてです。
実用的な知識としてインプットできるように無駄な部分を全部カットして書いていきます。
※便宜上、分かりやすくするためにグレーな表現も使います。「流石にそれはあかんだろ…」と思う内容があればご指摘ください。
まず3つのことを覚える
まず、
- 型
- 変数
- メモリ領域
の3つの存在を認識してもらいます。
今回はJavaでの話ですが、Javascriptとかの「参照の値渡し系」の言語ではほぼ同様の考え方でイケます。
プリミティブ型とオブジェクト型
Javaには2種類の型があります。
プリミティブ型…int, long, doubleみたいな小文字で始まるやつ。値を直接定義する基本の型。
オブジェクト型…プリミティブ型以外。配列やString形も含まれていて、型の名前が大文字で始まる。参照型とも呼ばれる。
最初は「なんか2種類あるんだな。」と思ってもらえればOKです。
はい。型の説明は以上!
変数の種類
変数は3種類あります。
- クラス変数…staticがついてるやつ
- メンバ変数…staticがついてないやつ
- ローカル変数…メソッド、コンストラクタの中にあるやつ
です。
public class Clazz1 { static int num; // クラス変数 int num2; // メンバ変数 void method() { int num3; // ローカル変数 } }
こんな感じです。
スタック領域とヒープ領域
Javaにはスタック領域とヒープ領域というのがあります。
メモリの使い方の話です。
ここでは「ふーん」で十分です。
はい、これで前提知識はOK。
これらの情報を紐づける形で説明していきます。
情報の紐づけ
型とメモリ領域
型にはプリミティブ型とオブジェクト型があって、メモリ領域にはスタック領域とヒープ領域があるという話でした。
今度は、「プリミティブ型とオブジェクト型の値が、それぞれどのメモリ領域に置かれるか」という話です。
プリミティブ型
まずはプリミティブ型。
ソース上で
int num = 1;
という処理を記述します。
メモリではこういうことが起きます。
スタック領域に1という値が作られ、スタック領域にnumという変数が定義されます。
オブジェクト型
じゃあオブジェクト型の場合、メモリでは何が起きるのか。
ソースでのイメージはこうです。
Object obj = new Object();
メモリではこうです。
ヒープ領域にインスタンスが置かれ、スタック領域にobjという変数が作られ、スタック領域にヒープのアドレスを保持します。
この「ヒープ領域との紐づき」が参照です。
オブジェクト型のミソは、オブジェクトのインスタンスをnewするとヒープ領域に生成されるということです。
はい次々ィ
変数とメモリ領域
変数によってメモリが確保される場所が違います。
public class Clazz1 { static int num; // クラス変数→ヒープ領域 int num2; // メンバ変数→ヒープ領域 void method() { int num3; // ローカル変数→スタック領域 } }
「ローカル変数はスタック領域に記憶されるんだぁ~ふーんふーーん。」
はい次々ィ
参照渡しと値渡し
はい本題。
参照渡し、値渡しというのは変数が保持している値を渡すときに参照値を渡すのか、実際の値を渡すのかという話です。
参照渡し
こういう状況があったとします。
ソース上はこうです。
Object obj = new Object(); Object obj2 = new Object();
これをこうします。
obj = obj2;
obj2のアドレスをコピーし、objのアドレスをobj2のアドレスで上書きします。
これがJavaにおける参照渡しです。
値渡し
値渡し。今度はこういう状況。
ソース上はこうです。
int num = 1; int num2 = 4;
これをこうします。
num = num2;
これが値渡しです。
ここまで大丈夫でしょうか?
ここから少し厄介になります。
参照渡し、値渡しを理解していないといけないのはここからです。
この辺の話で問題になってくるのは「別のメソッドで値を代入したとき、呼び出し元のメソッドの値が変化するかどうか」です。
でも安心してください。スタック領域とヒープ領域を知っていれば余裕です。
メソッドを介した参照渡し
まずは見分けるコツからお教えします。
見分けるコツ
見分けるコツは呼び出したメソッドの方での「書きっぷり」です。
以下例です。
public class Clazz1 { void method() { Clazz2 c = new Clazz2(); c.id = 1; // method2を呼び出す(引数にcを渡す) method2(c); } Object method2(Clazz2 c2) { // こう書いたとき(パターン①) c2.id = 2; // こう書いたとき(パターン②) c2.setId(3); // こう書いたとき(パターン③) c2 = new Clazz2(); // こう書いたとき(パターン④) c2 = null; } } public class Clazz2 { int id; void setId(int id) { this.id = id; } }
例が長くなって申し訳ないです。
やっていることは、
- Clazz2をnewしてインスタンスを生成
- method2を呼び出し、method2のほうで値を書き換える
です。
この4パターンそれぞれで、呼び出し元のmethod()のローカル変数(c)が書き変わるかという話。
結論はこうです。
// こう書いたとき(パターン①) c2.id = 2; // 更新される // こう書いたとき(パターン②) c2.setId(3); // 更新される // こう書いたとき(パターン③) c2 = new Clazz2(); // 更新されない // こう書いたとき(パターン④) c2 = null; // 更新されない
…おわかりいただけたでしょうか?
- 引数で渡された値の中身(今回はid)を変えた場合→書き換わる
- 引数で渡された値(今回はc2)を直接書き換えた場合→書き換わらない
Javaにおける参照渡しはこんなもんの認識で大丈夫です。(流石に怒られそう)
理屈を説明します
メソッド呼び出し時
まずはメソッドを呼び出したタイミングのスタック領域とヒープ領域の状態を整理しておきましょう。
参照値がコピーされているだけなので、見ているインスタンスは同じです。
この状態からどうなるかという話ですね。
パターン① c2.id = 2;
idが元々見ていた1の参照値を2の参照値に変えます。
インスタンスが抱えている情報が書き換わっただけなので、呼び出し元の変数(c)も書き換わります。
パターン② c2.setId(3);
こちらもやっていることは変わりません。
setId(int)は、
this.id = id;
と、idの参照値を書き換えています。thisの「この」はヒープ領域にあるインスタンスを指しています。
このパターンも同様、呼び出し元の変数(c)が一緒に更新されます。
パターン③ c2 = new Clazz2();
新しいインスタンスがヒープ領域に生成され、c2に参照させています。
newしてから=で繋げた訳ですからね。
見ての通り呼び出し元の変数(c)は更新されません。
パターン④ c2 = null;
nullは参照値がない状態です。
見ての通り、こちらも呼び出し元の変数(c)は更新されないです。
プログラミングにおける難関ポイントではありますが、理屈が分かっちゃうと全然難しい話ではないですねー。
はい、本題は以上!
余談:オブジェクトは参照のチェーンを成す
オブジェクト型っていうのはプリミティブ型のアドレスまでの道筋であり、チェーンのように捉えることができます。
上記の説明で使用したClazz2もint型のidまでの道筋を示す役割をしている訳ですからね。
そのため、「末端のプリミティブ型を直接扱う場合は参照を介さないため値渡しになる。」と解釈すれば良いと思っています。
まとめ
なんとなく理解できたでしょうか?
参照渡し、値渡しはコーディングにおける基礎知識ですが、理解していない人は多いです。
ただ、バグの原因になる可能性があるので、理解して考慮する必要があります。
今回は参照渡し、値渡しの面で見てメモリの使い方を見てみましたが、「マルチスレッド実行時の考慮点」について考える場合も今回と同じ要領で考えることができます。
マルチスレッドの場合はこんな感じになります。
用語や色々な概念が登場してきて大変ですが、基本原理は同じです。
あ、今回の内容をちゃんと理解したい人はJVMにおけるメモリの使い方をググってちゃんとした知識をつけておいてくださいw
あと、この記事の「情報の紐づけ」の「変数とメモリ領域」の項に着眼してソースを読むとコーディング力がグンッッと上がります。
ソースの違和感にパッと見だけで気づきやすくもなるのでおすすめです。
こじらでした
じゃ