- 初めに
- Javaにおける参照型
- それぞれの「渡し」の定義について
- Java の評価戦略
- プリミティブ型の「値渡し」の例
- 参照型(配列)における「参照渡し」に見える「値渡し」
- Java が参照渡しをサポートしていない事を示す例
- Java には参照渡しがないことのまとめ
- 終わりに
初めに
参照渡しができるという記事や解説がよく見られますが、Java には「参照渡し」はありません。確かに配列やオブジェクトなどの参照型の変数を扱う場合、参照渡しの様に二つの変数間で同じインスタンスを共有している動きが見られます。いわゆる、別名参照問題です。
プリミティブ型変数には、プリミティブ型の値そのものが格納され、代入などの操作を行うと、値がコピーされ、結果的に別々の値になります。これに対して、参照型変数に格納されるのは参照値なので、代入操作を行うとオブジェクト自身がコピーされるわけではなく、参照値(ポインタ)がコピーされます。参照値はメモリ上に展開されているデータの住所の様なものでしかありません。
ポインタとは、プログラムに含まれる変数や配列、構造体、関数、オブジェクトのインスタンスなどメモリ上の特定の位置に配置されるその位置を指し示すメモリ空間上のアドレス値などを格納した変数のことです。メモリアドレスを示します。
Oracle / Java Language Specification:4.3.1. Objects
An object is a class instance or an array.
The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.
(日本語訳)
もし2つの変数が同じオブジェクトへの参照を含んでいれば、一方の変数のオブジェクトへの参照を使ってオブジェクトの状態を変更し、変更された状態をもう一方の変数の参照を通して観察することができる。
Javaにおける参照型
Javaにおける型は、プリミティブ型と参照型の2種類に分類されます
- プリミティブ型:boolean型、char型、数値型(byte、short、int、long、float、double)
- 参照型:配列型、クラス型、インタフェース型
Java における参照とはどの様な定義でしょうか?
Oracle / Java Language Specification:4.3.1. Objects
An object is a class instance or an array.
The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.
(日本語訳)
オブジェクトとは、クラスのインスタンスや配列のことです。参照値(多くの場合、単に参照)は、これらのオブジェクトへのポインタと、特別なnull参照で、これはどのオブジェクトも参照しません。
参照ではないと公式で言っています。参照値という値、ポインタを渡しているのです。
それぞれの「渡し」の定義について
重要なこと
1. 実引数の何を、仮引数にどの様に渡すか?
2. 渡す方(実引数)と渡されたもの(仮引数)の紐付き方
- 値渡し(pass by value)
1. 値をコピーして渡す
2. 実引数と仮引数の紐付きはない
値が仮引数に複製されて処理に用いられる方法で、仮引数の変数の内容を変更しても、呼び出し元の実引数の変数には何の影響も及ぼさない。
- 参照渡し(pass by reference)
1. オブジェクトの参照値(ポインタ)を渡す
2. 実引数と仮引数の紐付きが保たれる
実引数と仮引数が完全に同じ実体を表すように引数を受け渡すこと。仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映されます。ちなみにポインタはメモリアドレスを指し示します。
- 参照(値)の値渡し(pass by referenceValue)
1. オブジェクトの参照値(ポインタ)を渡す
2. 実引数と仮引数の紐付きはない
呼び出し先で仮引数を操作すると呼び出し元の実引数にも反映される点は参照渡しと同じです。ですが、新しいオブジェクトを生成して代入するなど、仮引数が指し示す参照値自体を変更するような操作を行うと、新しい参照値は呼び出し元に反映されず、参照渡しと異なる動作となります。これは参照値(ポインタ)の値を変更した際に、それぞれの引数で変更した参照値が連動しない動きになることで発生します。
Java の評価戦略
Javaの評価戦略は値渡しです。 Javaでいう参照値(reference values)・参照(references)を引数で渡しても、値渡しで評価されます。
下記引用は翻訳しています。原文は引用元をどうぞ
Java is Pass-by-Value, Dammit!
Javaは厳密にはC言語と同じく値渡しです。Java言語仕様(JLS)を読んでください。ちゃんと書いてあるし、正しい。https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#jls-8.4.1
メソッドまたはコンストラクタが呼び出されると (15.12 節)、メソッドまたはコンストラクタの本体の実行前に、実際の引数式の値が、宣言された型の新しく作成されたパラメータ変数をそれぞれ初期化する。FormalParameterに現れるIdentifierは、メソッドまたはコンストラクタの本体で形式パラメータを参照するための単純な名前として使用されることがあります。
〜〜〜中略〜〜〜 要するに、Javaにはポインタがあり、厳密に値渡しである。特別なルールはありません。シンプルで、クリーンで、クリアです。(まあ、C++のような邪悪な構文が許す限りは、明確なのですが)
以下のコードを見てみましょう。まずは、Java でのプリミティブ型の値渡しをみていきましょう。Java のプリミティブ型は全て値渡しになります。
プリミティブ型の「値渡し」の例
public class PrimitiveType { public static void main(String[] args) { int i = 5; System.out.println("Before ::" + i); change(i); // 実引数 i System.out.println("After ::" + i); } private static void change(int a) { // 仮引数 int a a++; System.out.println("in change ::" + a); } } Before ::5 in change ::6 After ::5
メソッドへの引数は値渡しであり、単純に値がコピーされているため、i
とa
は全く別々の値です。i
を書き換えることはできません。
プリミティブ型の場合は単純に値のみがコピーされて渡されます。
値渡し(call by value)は、値が仮引数に複製されて処理に用いられる方法で、仮引数の変数の内容を変更しても、呼び出し元の実引数の変数には何の影響も及ぼしません。
一方、実引数と仮引数が完全に同じ実体を表すように引数を受け渡すことを「参照渡し」(call by reference)といいます。参照渡しされた仮引数に対して加えられた変更はすべて呼び出し元の実引数にも反映されます。 次のコードは、参照渡しではありませんが、参照渡しに似た動きをします。
参照型(配列)における「参照渡し」に見える「値渡し」
import java.util.Arrays; public class ReferenceType { public static void main(String[] args) { int[] arr = new int[] {1, 2, 3}; System.out.println("Before ::" + Arrays.toString(arr)); change(arr); // 実引数 arr System.out.println("After ::" + Arrays.toString(arr)); } private static void change(int[] a) { // 仮引数 int[] a a[0]++; a[1]++; a[2]++; } } Before ::[1, 2, 3] After ::[2, 3, 4]
引数と仮引数が完全に同じ実体を表すため、仮引数に対してインクリメントした処理が、呼び出し元の実引数であるarr
に反映されています。
参照渡しとは上記で説明した通り、変数などを引き渡す際に呼び出し先でも同じ実体を表すように渡す方式のことです。Java における参照とはどの様な定義かもう一度確認しましょう。
Oracle / Java Language Specification:4.3.1. Objects
An object is a class instance or an array.
The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.
(日本語訳)
オブジェクトとは、クラスのインスタンスや配列のことです。参照値(多くの場合、単に参照)は、これらのオブジェクトへのポインタと、特別なnull参照で、これはどのオブジェクトも参照しません。
参照ではないと公式で言っています。参照値という値(ポインタ)を渡しているのです。 参照渡しではないことを表すコードが以下になります。
Java が参照渡しをサポートしていない事を示す例
import java.util.Arrays; public class ReferenceType { public static void main(String[] args) { int[] arr = new int[] {1, 2, 3}; System.out.println("Before ::" + Arrays.toString(arr)); change(arr); System.out.println("After ::" + Arrays.toString(arr)); } private static void change(int[] a) { // 仮引数の参照値を変更 a = new int[] {2, 1, 4}; a[0]++; a[1]++; a[2]++; } } Before ::[1, 2, 3] After ::[1, 2, 3]
呼び出し先で仮引数を操作すると呼び出し元の実引数にも反映される点は参照渡しと同じでしたね。
ですが、changeメソッドの
a = new int[] {2, 1, 4};
の様に、新しいオブジェクトを生成して、そのインスタンスの参照値を再代入するなどの、仮引数が指し示す参照値自体を変更するような操作を行うと、新しい参照値は呼び出し元の実引数には反映されず、参照渡しと異なる動作となります。
本来の参照渡しは呼び出し先で参照先を変更するような操作(新しいオブジェクトを生成して、そのインスタンスの参照値を再代入)を行うと呼び出し元にもその変更が反映されますが、参照値の値渡しは参照を表す値を渡しているだけになり、仮引数の参照値を変更しても呼び出し元の実引数の参照値は呼び出し前と変わりません。
参照渡しであれば、
After ::[3, 2, 5]
という出力になるはずです。
実際の出力は上記の通りです。これは仮引数が change メソッド内で違う参照値を代入されることで発生する現象です。実引数となる arr
変数の参照値とはこのタイミングで異なるのです。
参照渡しであれば、実引数と仮引数の紐付きは保たれるので、仮引数の異なる参照値代入が実引数にも反映されます。Java ではこの紐付きがないのです。
この様に、参照値を別の参照値に変更するため「参照(値)の値渡し」と表現されることがありますが、参照値とはポインタの事を意味するためポインタ渡しとも呼ぶ様です。
Java には参照渡しがないことのまとめ
参照渡しとはなんだったのかもう一度確認します。
参照渡し
『実引数と仮引数が完全に同じ実体を表すように引数を受け渡すこと。参照渡しされた仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映される』
- 「Java が参照渡しをサポートしていない事を示す例」
で紹介したコードの通り、「仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映される」という参照渡しの挙動は Java では起こり得ません。なぜか?変数に参照値、いわゆるポインタ(メモリアドレス)を渡すだけだからです。仮引数と実引数の紐付きがありません。仮引数の参照値が変わると、呼び出し元の実引数と呼び出された側の仮引数の関係性はなくなります。
これに対して、参照渡しは関係性が保たれます。仮引数の参照値を変更した際の、実引数の参照値変更は認められません。なので仮引数に対する変更は実引数にも影響します。
参照渡しとポインタ渡しの違いは「実引数の参照値を変更できるか、できないか?」となります。他にもありますがここでは割愛します。
- 「Java が参照渡しをサポートしていない事を示す例」
で紹介したコードの様に実引数の参照値の変更ができるのが参照値の値(ポインタ)渡しです。参照の値渡しと表現できますが、厳密には値渡しです。プリミティブ型ではそのまま「値」を、オブジェクトや配列の場合は参照値(ポインタ、つまりメモリアドレス)の「値」を評価します。
終わりに
JVMには「スタック領域」と「ヒープ領域」という仮想メモリ領域があり、この中で変数を処理しています。参照型変数とプリミティブ型変数では、この仮想メモリ領域での管理方法が異なります。
以下にそちらを続きとして書きました。