値渡しと参照渡しについて
Javaを勉強していくと関数の引数の渡し方で混乱すると思います。
世間では、値渡し、参照渡し、参照値渡しという3種類の区分があり、なんじゃそれと思われている方々も多いと思います。
今回はそんな疑問点を解消する一助になるようにできるだけシンプルにまとめてみました。
値渡し
public void sample1(int a){
a = STATIC_NUM;
}
例えば、上記のような関数を定義したとします。
この関数は説明が不要なほどシンプルですね。int型の変数1つを引数に受け取り、それに定数を代入しているだけの関数です。
ではこの関数を実際に使って出力結果を見てみましょう。
public static void main(String args[]){
int a = 10;
System.out.println( a ); //10が出力される
sample1(a);
System.out.println( a ); //10が出力される
}
この結果は皆さん予想がついたかと思います。
1回目のプリントは、まぁ、言わずもがなの結果ですね。
2回目のプリントでは、sample1
を実際に使用しましたが、こちらの関数は引数にint型の変数を受け取るよという取り決めしかしていませんでした。
つまり、実際に渡されたのは変数a
自体ではなく、その値が変数という箱ごとコピーされて渡されたという認識でいいと思います。
よって、sample1
の中で処理されているint型変数aと、渡したint型変数aは全くの別物なので、STATIC_NUM
を代入するという処理の影響は受けず、初期化の値である10が出力されます。
値渡しというのは、渡す変数に直接影響がない渡し方と言うことが出来ます。
参照渡し
JavaはC言語におけるポインタの機能がないため参照渡しをすることはできません。
あばばばば、失礼しました。C言語も参照渡しはできず、C++やPHPなどの言語でできるようです。
(訂正のコメント本当にありがとうございました!)
ですので、説明は割愛させていただきます。
参照値渡し
Java言語では、例えば配列を定義するとき、その変数には配列の先頭の値が格納されているメモリのアドレスが代入されます。
つまり、配列の変数は値そのものを保持しているのではなく、どこから配列が始まるかを示すアドレスが入っているので、具体的な値にアクセスしたい場合は[0]
などの添え字を指定する必要があるのです。
では、その配列を扱う関数を定義してみましょう。
public void sample2(ArrayList<Integer> a){
a.add(STATIC_NUM);
}
こちらも簡単な関数ですね。int型の配列型変数を受け取り、その配列の末尾にSTATIC_NUM
という定数を追加するという処理を行うだけの関数です。
では、この関数を使って実際に出力結果を見てみましょう。
public static void main(String args[]){
ArrayList<Integer> a = new ArrayList<>();
System.out.println( a ); //[]が出力される。まだ何も要素を持ってない。
sample2(a);
System.out.println( a ); //[STATIC_NUM]が出力される
}
明らかに動作に違いがありますね。
1回目のプリントでは、要素の持たない配列を出力しようとしているので空の配列がそのまま[]
として出力されます。
2回目のプリントでは、sample2
にint型の配列型変数aを渡して処理をしてもらっているので、その後の出力ではsample2
でSTATIC_NUM
が要素に追加されたので、その結果が出力されます。
これも何となくイメージできますか?
あぁ、配列の先頭アドレスa
を受け取って、それにSTATIC_NUM
を追加してるんだから、当然その出力結果になるよね、と腑に落ちる方も多いのではないでしょうか。
この出力結果に納得がいくかどうかは置いておいて、ひとまず次のケースを考えてみてください。
public void sample3(ArrayList<Integer> a){
a = new ArrayList<>();
a.add(STATIC_NUM);
}
さて、このようなsample3
の関数を考えてみましょう。
こちらを用いて、先ほどのmain
関数を実行するとどうなるでしょうか?
以下のmain関数は先ほどと同じです。
public static void main(String args[]){
ArrayList<Integer> a = new ArrayList<>();
System.out.println( a ); //[]が出力される。まだ何も要素を持ってない。
sample3(a);
System.out.println( a ); // ????????????
}
さて、2回目のプリントの結果はどうなるでしょうか、、、?
出力結果は[]
となります。
ん?なんで?と思われた方もいらっしゃいますか?
では、sample2
とsample3
の結果を比較してみましょう。
この2つの関数、違いは処理の中に
a = new ArrayList<>();
の有無ですよね。
それでは、処理を順に追っていきましょう。
まず、引数であるint型の配列型変数aを受け取ります。
ここで重要なのが、main関数内で定義した変数aのアドレスが、変数という箱ごとコピーされて渡されたということです。値渡しの節でも説明したように、変数そのものが渡っているわけではなく、あくまでもsample2/sample3
の中で処理されている引数は、呼び出し元で渡された引数と同様の型と値を持つコピーだということです。
sample2
の中では、main関数内で定義された変数aが保持するアドレスと、同様のアドレスを保持している別の変数a'
(として区別しておきます)を扱っており、その変数について.add(STATIC_NUM)
という処理を行っているのです。
a
とa'
はそれぞれ別の変数ですが保持しているアドレスは同じなので、片方への操作がもう片方へと影響してしまうのです。
イメージとしては、aさんとa'さんは2人とも机の上の同じ白い紙を指さしている状態で、
a'さんがその紙に何か落書きをすれば、aさんが指さしている白い紙にもその落書きは反映されますよね。(とっても当たり前のことを言ってます笑)
次に、sample3
について。
同様にsample3
内でもmain関数で定義されている変数aのコピーa'
が処理の対象となるのですが、
a = new ArrayList<>()
という操作を行うことで、a'
は新しく生成された配列の先頭を表す別のアドレスを参照したということです。
先ほどのたとえ話で言うと、
aさんとa'さんは机の上の同じ白い紙を指さしている状態でしたが、
a = new ArrayList<>()
を実行すると、a'さんは別の白い紙を指さすようになります。
そして、a'さんがその別の白い紙に落書きをするのですが、それはaさんが指さしている白い紙とは別の紙なので、aさんが指さす白い紙は白いままなんですね!
だから、2回目のプリントの結果も[]
となるんです!