目次
本項の目的
参照値についてJavaの挙動確認
アンチパターンの備忘録
この実装結果、どうなる?
まず、これを見てほしい。
実行結果はどうなるでしょう?
public void method(){ List<String> list = new ArrayList<>(); list.add("10"); add(list); System.out.println("実行結果:" + list); } public void add(List<String> list){ list.add("11"); }
11が追加され、以下のように出力されます。
実行結果:[10,11]
では、次はどうでしょう?
public void method(){ List<String> list = new ArrayList<>(); list.add("10"); add(list); System.out.println("実行結果:" + list); } public void add(List<String> list){ list = new ArrayList<>(); list.add("11"); }
実行結果は、10が出力されます。
実行結果:[10]
この出力についてきちんと説明できるでしょうか?
本記事では順を追って説明したいと思います。
言葉の定義について
この記事内では、メソッドに渡す型がオブジェクト型の場合「参照渡し」と呼んでいます。
メソッドに渡す型がプリミティブ型の場合は、「値渡し」と呼びます。
本質的にこれらをなんと言うべきかについては、あまり興味はありません。
相手に伝われば良いと思います。
個人的には「参照渡し」「参照値渡し」「値渡し」これらのどの言葉を選ぶかは、
話者によって変えるのが適切だと思われます。
Javaに詳しい人であれば、どの用語でも文脈的に意図は通じるはずです。
仮に誤解があっても、話せば何が言いたいかは通じるので大きな問題にはならないと思います。
Javaにあまり詳しい人でないのであれば、プリミティブと参照型を分ける意味で使い分けてもいいかもしれません。
その場合、どこまで踏み込んで話をするかは相手次第です。
文書的に書くのであれば、誤解のないように前提を置いた上で話した方が適切だと思われます。
つまり。バランスが大事。
参照渡しおさらい
変数がもつ参照値を渡して値を書き換える事で、呼び出し元の値をあたかも変えた風にする。
厳密に言えばJavaはそもそも参照渡しは存在しません。
Javaのメソッドやクラスにオブジェクトを入力値とすると、参照渡しっぽい挙動になるのが混乱を招いてる元だと思う。
参照渡しについてはC#など、言語レベルでサポートしている物もあります。
値渡しおさらい
変数の値のみを呼び出し先に渡します。
呼び出し先でどこまで値を変更しても、呼び出し元に影響はありません。
Javaでは基本こっち。
ただ、オブジェクトの参照値を渡した場合は呼び出し元に影響することもあります
挙動確認
実際の挙動をカテゴリー毎に見ていきます。
プリミティブの場合
プリミティブは値渡しに近い挙動になります。
以下の出力結果は10でしょうか?それとも11?
ちなみに、int/long/charなど全てのプリミティブ型では同じ挙動となります。
public void method(){ int value = 10; inclement(value); System.out.println("実行結果:" + value); } private void inclement(int value){ value++; }
正解は10です。
変数の値が変わらないのは、これが値渡しであるからです。
実行結果:10
Object型の場合
Javaで複数変数や意味を持たせた値を保持する場合、それをクラス化する場合がある。
例えば、RPG等ではキャラクターやアイテム等をそれぞれ型として宣言することができる。
このアイテムクラスで先ほどのプリミティブ型でやった時と同じような事を行っていきます。
// Getter・Setter・toStringは省略するが定義している物とする public class Item{ private int id; }
オブジェクトの保持変数を取得して、その変数書き換えのみ行った場合
先ほどのアイテムクラスを引数に渡した場合を検証します。
生成したアイテムインスタンスをまるごとinclementに渡しています。
この場合、実行結果は10でしょうか?11でしょうか?
public void method(){ Item item = new Item(); item.setId(10); inclement(item); System.out.println("実行結果:" + item.getId()); } private void inclement(Item item){ int id = item.getId(); id++; }
10が出力されます。
挙動としては上記のプリミティブの場合と大差ありません。
実行結果:10
オブジェクトが保持する変数を書き換えた場合
では、下記の場合どうでしょう?
inclementの中でsetId()とする事でIDを書き換えています。
実行結果は10でしょうか?11でしょうか?
public void method(){ Item item = new Item(); item.setId(10); inclement(item); System.out.println("実行結果:" + item.getId()); } private void inclement(Item item){ item.setId(11); }
11が出力されます。
お察しの通り呼び出し先で設定した値がインスタンスに反映されます。呼び出し元が渡したオブジェクトが保持している、参照値の中身が変更された為です。
実行結果:11
オブジェクトが保持する変数を、newしてから書き換えた場合
では、さらに以下の場合どうでしょう?
この辺りから、出力は分かっても事象を説明できる人がぐんと減るのではないかと思います。
public void method(){ Item item = new Item(); item.setId(10); inclement(item); System.out.println("実行結果:" + item.getId()); } private void inclement(Item item){ item = new Item(); //ここでnewしてインスタンスを上書きする item.setId(11); }
結果は、以下のようになります。11が出力されない理由はメソッドには参照値コピーを渡しているからです。
メソッド内で新たにインスタンスを生成していますが、呼び出し元ではコピーする元を保持しています。
そのため、コピー元の10が出力されます。
実行結果:10
先ほどのnewしないパターンはこんなイメージです。
比較すると、コピー先の保持している参照先を操作しているのかどうかの違いが見えます。
コレクションの場合
コレクションの幅が広すぎるので、ここではArrayListのみを扱います。
この場合の実行結果は[10]?[11]?[10,11]?
public void method(){ Item item = new Item(); item.setId(10); List<Item> list = new ArrayList<>(); list.add(item); add(list); System.out.println("実行結果:" + list); } public void add(List<Item> list){ Item item = new Item(); item.setId(11); list.add(item); }
結果は、以下のようになります。
挙動としては、[オブジェクトが保持する変数を書き換えた場合]と同じような挙動になります。
実行結果:[10,11]
では、addメソッドを下記のように変更したらどうでしょうか?
冒頭の[この実装結果、どうなる?]のケースと同様です。
public void method(){ Item item = new Item(); item.setId(10); List<Item> list = new ArrayList<>(); list.add(item); add(list); System.out.println("実行結果:" + list); } public void add(List<Item> list){ list = new ArrayList<>(); //ここが異なる Item item = new Item(); item.setId(11); list.add(item); }
結果は、以下のようになります。
挙動としては、[オブジェクトが保持する変数を、newしてから書き換えた場合]と同じような挙動になります。
実行結果:10
ちなみに、以下のような場合ではリストのオブジェクトの中身が変わります。
参照(リスト)が保持する、参照(アイテムインスタンス)の値が変更され、コピー元での値も変更されます。
public void method(){ Item item = new Item(); item.setId(10); //値を10に設定 List<Item> list = new ArrayList<>(); list.add(item); set(list); // 3行目の設定した値の出力は10? 11? System.out.println("実行結果:" + item.getId()); } public void set(List<Item> list){ Item item = list.get(0); item.setId(11); }
実行結果:11
String型,Integer型の場合
Javaの基本的な型であるこれらの型ではどうなるでしょう?
呼び出し先で値を変更した場合
public void method(){ String value = "val"; concat(value); System.out.println("実行結果:" + value); } private void concat(String value){ value.concat("concat"); }
String型の場合クラス自体がImmutableクラスなので、値変更は行われません。
実行結果:val
リスト要素の中身を変更した場合
では、リストから取得した値を変更した場合どうなるでしょうか。
public void method(){ String value = "val"; List<String> list = new ArrayList<>(); list.add(value); set(list); System.out.println("実行結果:" + value); } private void set(List<String> list){ String val = list.get(0); val = "set"; }
リストから取得した値が変更された場合でもコピー元のvalue変数に変更はありません。
これも、Immutableクラスの特性です。
実行結果:val
リスト要素そのものを変更した場合
リストの値そのものを変更した場合、リストの値は変更されます。
しかし、格納前の値(value)は変更されません。
public void method(){ String value = "val"; List<String> list = new ArrayList<>(); list.add(value); set(list); System.out.println("実行結果:" + value); System.out.println("実行結果:" + list); } private void set(List<String> list){ String val = list.get(0); val = "set"; //val変数の中身を書き換え list.set(0, val); }
コレクション自体はImmutableではないので、値の変更が行われます。
実行結果:val 実行結果:[set]
まとめ
リストやオブジェクトなど、Immutableではない値だとコピー先で変更された場合コピー元が変わる。
ただし、コピー先のオブジェクト参照先を、newで変更した場合はコピー元は変わらない。
Immutableの場合、値の変更はコピー元では起こらない。
Javaの設計上、変数の書き換えを行わないのがベター。
Immutableのクラスを使う事で値の変更性を意識する必要が減る。
参照値を渡す場合、できるかぎりコピー元を変更しない。
変更する場合、メソッド単位でのドキュメンテーションを行う。
ドキュメンテーションについては、下記参照。