初めに
参照渡しができるという記事や解説がよく見られますが、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つの変数が同じオブジェクトへの参照を含んでいれば、一方の変数のオブジェクトへの参照を使ってオブジェクトの状態を変更し、変更された状態をもう一方の変数の参照を通して観察することができる。
1. 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参照で、これはどのオブジェクトも参照しません。
参照ではないと公式で言っています。参照値という値、いわゆるポインタを渡しているのです。
2. それぞれの「渡し」の定義について
重要なこと
- 実引数の何を、仮引数にどの様に渡すか?
- 渡す方(実引数)と渡されたもの(仮引数)の紐付き方
- 値渡し(pass by value)
- 値をコピーして渡す
- 実引数と仮引数の紐付きはない
値が仮引数に複製されて処理に用いられる方法で、仮引数の変数の内容を変更しても、呼び出し元の実引数の変数には何の影響も及ぼさない。
- 参照渡し(pass by reference)
- オブジェクトの参照値(ポインタ)を渡す
- 実引数と仮引数の紐付きが保たれる
実引数と仮引数が完全に同じ実体を表すように引数を受け渡すこと。仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映されます。ちなみにポインタはメモリアドレスを指し示します。
- 参照(値)の値渡し(pass by referenceValue)
- オブジェクトの参照値(ポインタ)を渡す
- 実引数と仮引数の紐付きはない
呼び出し先で仮引数を操作すると呼び出し元の実引数にも反映される点は参照渡しと同じです。ですが、新しいオブジェクトを生成して代入するなど、仮引数が指し示す参照値自体を変更するような操作を行うと、新しい参照値は呼び出し元に反映されず、参照渡しと異なる動作となります。これは参照値(ポインタ)の値を変更した際に、それぞれの引数で変更した参照値が連動しない動きになることで発生します。
3. 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 のプリミティブ型は全て値渡しになります。
4. プリミティブ型の「値渡し」の例
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)といいます。参照渡しされた仮引数に対して加えられた変更はすべて呼び出し元の実引数にも反映されます。
次のコードは、参照渡しではありませんが、参照渡しに似た動きをします。
5. 参照型(配列)における「参照渡し」に見える「値渡し」
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参照で、これはどのオブジェクトも参照しません。
参照ではないと公式で言っています。参照値という値(ポインタ)を渡しているのです。
参照渡しではないことを表すコードが以下になります。
6. 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 ではこの紐付きがないのです。
この様に、参照値を別の参照値に変更するため「参照(値)の値渡し」と表現されることがありますが、参照値とはポインタの事を意味するためポインタ渡しとも呼ぶ様です。
7. Java には参照渡しがないことのまとめ
参照渡しとはなんだったのかもう一度確認します。
参照渡し
『実引数と仮引数が完全に同じ実体を表すように引数を受け渡すこと。参照渡しされた仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映される』
- 「Java が参照渡しをサポートしていない事を示す例」
で紹介したコードの通り、「仮引数に対して加えられた変更は、すべて呼び出し元の実引数にも反映される」という参照渡しの挙動は Java では起こり得ません。なぜか?変数に参照値、いわゆるポインタ(メモリアドレス)を渡すだけだからです。仮引数と実引数の紐付きがありません。仮引数の参照値が変わると、呼び出し元の実引数と呼び出された側の仮引数の関係性はなくなります。
これに対して、参照渡しは関係性が保たれます。仮引数の参照値を変更した際の、実引数の参照値変更は認められません。なので仮引数に対する変更は実引数にも影響します。
参照渡しとポインタ渡しの違いは「実引数の参照値を変更できるか、できないか?」となります。他にもありますがここでは割愛します。
- 「Java が参照渡しをサポートしていない事を示す例」
で紹介したコードの様に実引数の参照値の変更ができるのが参照値の値(ポインタ)渡しです。参照の値渡しと表現できますが、厳密には値渡しです。プリミティブ型ではそのまま「値」を、オブジェクトや配列の場合は参照値(ポインタ、つまりメモリアドレス)の「値」を評価します。
Java における引数などの評価戦略は「値渡しのみ」であり「参照渡し」は存在しません。以降では、プリミティブ型と参照型のメモリ管理の違いを見ていきます。具体的にどの様なことが起きているのかを紹介します。JVMには「スタック領域」と「ヒープ領域」という仮想メモリ領域があり、この中で変数を処理しています。参照型変数とプリミティブ型変数では、この仮想メモリ領域での管理方法が異なります。
8. 処理実行には実行内容をメモリ上に展開する必要がある
ほとんどのPCは「ノイマン型」コンピュータです。
よく見るやつです。入力装置はマウスやキーボード、出力装置はディスプレイなどですね。
ノイマン型のコンピュータの特徴は、
-
プログラム内蔵方式
あらかじめ機械語が主記憶装置(メモリ)に内蔵されている。 -
逐次制御方式
主記憶装置(メモリ)に格納されている情報を中央処理装置(CPU)が1つ1つ取り出して実行する。 -
アドレス指定方式でデータを扱う
固定の命令(機械語)は命令部とアドレス部から構成される。アドレス部は命令の対象となるデータがある主記憶装置(メモリ)のアドレスを示す情報を持っており、 アドレスを示す情報はCPU内部のレジスタ(高速な記憶装置)に格納される。
CPUはレジスタにある場所情報をもとにメモリから順次読み出す。 -
固定の命令(機械語)が使える
演算処理や動作などに固定の命令(機械語)を用いることができる。
「実行対象のプログラムをデータとしてメモリ上に展開し、処理演算装置(CPU)はそれを順次読み込んで処理する」
となっています。
つまり、作成したプログラムや第三者が作成したライブラリ、あるいはPC上で動く各種アプリケーションから、サーバ上で動作するWebサービスに至るまでどのようなプログラムも実行時に必ずメモリ上にその内容が展開される仕組みとなっています。メモリとは、プログラムの実行中に取り扱っているデータを一時的に保存する領域です。
実行対象プログラムは、実行前にメモリ上にその内容が展開されます。今回は取り上げませんが、この作業を行なうのはOSの役割です。
また、OSとJVMがそれぞれで占有するメモリ空間などの概念もあります。JavaのGCの仕組みを理解するのに重要ですが、こちらも今回は紹介しません。
9. メモリの管理方法による分類
メモリの管理方法によってメモリの空間が定義されています。他にもあるのですが、ここでは「スタック領域」「ヒープ領域」「スタティック領域」の3種類を紹介します。
領域 | 説明 | 変数との対応 | 生存期間 |
---|---|---|---|
スタック領域 | ローカル変数、パラメータ、戻り値、演算に使われる任意の値などを管理する領域。スタック領域は共有リソースではないため、スレッドセーフ。実際に処理されるデータを格納する領域。「スタック」=積み重ねという名前が表すように、処理対象のデータはFILO(先入れ後出し)方式でデータを管理し、処理が完了したデータはスタックから破棄。 | メソッドスコープあるいはforスコープなどの特定の処理スコープ内で定義する。 | 特定の処理スコープ内だけで有効な変数。変数定義した処理スコープの処理がすべて完了すると破棄。 |
ヒープ領域 | new演算子で生成されたオブジェクトと配列を管理。必要な時に、必要なサイズを指定して領域が確保できる自由度の高いメモリ領域。ただし、確保したメモリは必ず解放する必要がある。Java ではGCにて実行中のプログラムの動作から不要になったと判断した領域を自動的に解放。メモリの解放を明示的に行わなけばならない言語ではメモリリークに注意する必要がある。 | クラススコープ内で変数定義を行なう。 | 対象クラスのインスタンスがnew演算子にて生成されてから、破棄されるまでの間有効。 |
スタティック領域 | static変数やグローバル変数を管理する。静的領域とも呼ばれ、プログラムの開始から終了までメモリ空間は保持し続けられる特徴をもつ。 | クラススコープ内でstaticキーワード付きで変数宣言を行なう。 | クラス利用をするJavaアプリケーションの開始から終了まで有効 |
「変数を宣言・定義を行う、スコープを選択する」 = 「どのメモリ管理領域でデータ管理するかを決定する」
ということになります。メソッド内で宣言した変数やfor文内で宣言した変数が、そのスコープを脱出すると利用できなくなる理由は、このスタック領域の管理仕様によるものです。
また、Java の様なオブジェクト指向プログラミング(OOP)言語の多くでは生成されたインスタンスは全てヒープ領域に配置されます。OOPは有限なヒープ領域を大量に使用することが前提になります。メモリの占有領域の解放管理は非常に重要な概念です。現代では非常にリッチな環境なので意識する機会は少なくなっていますが。
10. 「プリミティブ型」と「参照型」はメモリ管理の仕組みが異なる
プリミティブ型では値がスタック領域に、参照型ではスタック領域にヒープにあるインスタンスそのものではなく参照(ポインタ)が格納されます。
JVMが管理するメモリ領域にも、スタック領域とヒープ領域があります。上記の様に、スタック領域にはローカル変数のデータを置き、出てきた順に、ローカル変数のデータを積み上げていきます。変数の有効範囲を抜けると、このデータはすぐに解放されます。
public class PrimitiveType {
public static void main(String[] args) {
int i = 5;
int i2 = i;
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
このプログラムの変数をスタックに積み上げると次のようになります。
ローカル変数はメソッドやメソッド内のfor、if等のスコープで定義される変数のことで、スタック変数とも呼ばれます。上記で紹介した通り、ローカル変数は"スタック領域"と呼ばれるメモリ領域で管理され、変数の有効範囲を抜けるとこのデータはすぐに破棄されます。
一方のヒープ領域は,インスタンスの実体を格納する領域です。以下に流れを記述します。
-
参照型の変数を作ると、まずスタックにその場所が用意される
-
new演算子でそこに新しいインスタンスを作ると、インスタンスの実体がデータとしてヒープ領域に作られる
-
そして、ヒープ上に存在するインスタンスの位置が、参照型の変数のデータとしてスタック領域に書き込まれる。スタック領域にはオブジェクトを参照するための情報が入っています。
これはオブジェクトを参照するための情報なので、オブジェクト型の変数は「参照型」と呼ばれます。参照とはポインタのことです。new 演算子でインスタンス生成する変数・プリミティブ型の配列は参照型変数です。new 演算子は「ヒープ領域に指定された内容を実体化する」という演算を行なう特殊な演算子です。
これに対し実際のデータ自体がスタック領域に書き込まれるのがプリミティブ型です。このようなメモリ管理をする型を「プリミティブ型」といいます。
プリミティブ型と参照型の変数の挙動は上記で紹介した通りです。
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]
下図のように、特定のメソッドを処理中に new 演算子でインスタンス化の命令が指定されると、ヒープ領域にそのインスタンス(実体)が展開されます。インスタンスはヒープに確保されますが、参照型変数はスタックにプリミティブ型変数と同じように存在します。
- プリミティブ型変数はスタックに値(実データ)が格納
- 参照型変数はヒープに確保されたインスタンスの参照を格納
参照型変数は参照(ポインタ)を元にヒープ領域に展開されたインスタンスへアクセスします。
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) {
// 仮引数の参照値を変更
int[] a = new int[] {2, 1, 4};
a[0]++;
a[1]++;
a[2]++;
}
}
Before ::[1, 2, 3]
After ::[1, 2, 3]
このプログラムでは、スタック領域の変数とヒープ上で生成されるインスタンスの関係は以下の様になります。
①では 変数 arr を出力し、②の時点でも同じ様に変数 arr を出力します。Java には参照渡しがないのでこの様な挙動になります。メモリ上でどの様にデータが管理されているかをみれば一目瞭然ですね。
具体的な流れは、上記での説明を引用します。
changeメソッドの
a = new int[] {2, 1, 4};
の様に、新しいオブジェクトを生成して、そのインスタンスの参照値を再代入するなどの、仮引数が指し示す参照値自体を変更するような操作を行うと、新しい参照値は呼び出し元の実引数には反映されず、参照渡しと異なる動作となります。
本来の参照渡しは呼び出し先で参照先を変更するような操作(新しいオブジェクトを生成して、そのインスタンスの参照値を再代入)を行うと呼び出し元にもその変更が反映されますが、参照値の値渡しは参照を表す値を渡しているだけになり、仮引数の参照値を変更しても呼び出し元の実引数の参照値は呼び出し前と変わりません。
参照渡しであれば、
After ::[3, 2, 5]
という出力になるはずです。
実際の出力は上記の通りです。これは仮引数が change メソッド内で違う参照値を代入されることで発生する現象です。実引数となるarr
変数の参照値とはこのタイミングで異なるのです。
11. 終わりに
これで別名参照問題だとか、ポインタがなんだとか、楽勝な気分です!(嘘)
この次は、シャロウコピー・ディープコピー・ ディフェンシブコピーなどを取り扱いたいと思います。
これまでの記事の紹介です。併せて他の記事も読んでいただけると嬉しいです。