わかりずらい Java の参照渡し (3/3)

参照に対する勘違いを起こす理由として、Java のメソッドには「オブジェクトを操作するメソッド」と「オブジェクトを生成するメソッド」の 2 タイプあり、最もよく使われる String クラスに「オブジェクトを生成するメソッド」しかないことが挙げられると思います。

結論から言ってしまうと、String クラスに用意されているメソッドは、どのメソッドを使ってもオブジェクトそのものの値は変わりません。
言い換えると、オブジェクトそのものの値を変えることができません。そのようなメソッドが存在しないからです。
String クラスにはオブジェクトの値を元にした新しいオブジェクトを生成するメソッドしか存在しません。

先のサンプルAの test1 メソッドの内容を変えたサンプルCを例とします。

サンプルC
public class SampleC
{
 public static void main(String[] args)
 {
  String s1 = "main メソッド実行";

  SampleC sampleC = new SampleC();
  sampleC.test1(s1);

  System.out.println(s1);     ・・・4
 }

 public void test1(String s2)
 {
  s2.toUpperCase();        ・・・1
  s2.replaceAll("main", "test1"); ・・・2
  s2.concat("?");         ・・・3
 }
}

このサンプルCを実行した時、の部分でコンソールには
> java SampleC
main メソッド実行
と出力されます。

test1 メソッド内では変数:s2 の String オブジェクトのメソッドを実行しています。
の toUpperCase() は文字列を大文字に変換するメソッド、 の replaceAll(String, String) は文字列内にある第1引数の文字を第2引数の文字に置き換えるメソッド、 の concat(String) は引数に指定した文字列を末尾に追加するメソッドです。
しかし、このどれを実行しても、変数:s2 が参照している String オブジェクト(これは変数:s1 が参照しているオブジェクトと同じ)は変わりません。
それはこれらのメソッドが「オブジェクトを生成する」タイプのメソッドだからです。

これに対しサンプルBで使用した Vector#add(Object) メソッドは「オブジェクトを操作するメソッド」なので、元のオブジェクトの内容が変更されます。

メソッドの結果として生成されたオブジェクトは戻り値として返りますので、変数で受けてやる必要があります。

 public void test1(String s2)
 {
  String s3 = s2.toUpperCase();
  String s4 = s2.replaceAll("main", "test1"");
  String s5 = s2.concat("?");
 }
こうすると、変数:s3, s4, s5 のそれぞれに結果オブジェクト(の参照)が格納されます。
具体的には変数:s3 には "MAIN メソッド実行"、変数:s4 には "test1 メソッド実行"、変数:s5 には "main メソッド実行?" が入ります。
この時、もちろん変数:s2 が参照しているオブジェクトは変更されません。

この String クラスの「オブジェクトを生成するメソッド」に慣れてしまい、
"メソッドを実行しても元のオブジェクトは変わらない"
= "渡した元のオブジェクトは操作されない"
= "参照渡しってどういうこと?"
という混乱に繋がっているのだと思います。
そして、たまに使用する「オブジェクトを操作するメソッド」のせいで、渡したオブジェクトが変わる時と変わらない時があり、さらに混乱してしまいます。

Java には「オブジェクトを操作するメソッド」や、渡されたバッファに値を格納するようなメソッドが少ないです。
ほとんどがこの「オブジェクトを生成するメソッド」です。
ポインタ=参照渡しを意識させないための工夫でしょうが、これだけでは限界があったのか結果的に「オブジェクトを操作するメソッド」が存在することによって混乱を招いていると思います。

使用するクラスのメソッドが「オブジェクトを生成するメソッド」なのか、「オブジェクトを操作するメソッド」なのか − 意識して使用していくことで、混乱は防げると思います。

私は Sun の人間ではありませんし、VM やメモリをハックしたわけでもありませんので、記載された内容は厳密には事実と異なる部分があるかもしれません。 ですが、「納得できて理解できればいいな」という考えで公開しています。この点はご了承ください。

トラックバック

このエントリーのトラックバックURL:
http://www.tec-q.com/mt/mt-tb.cgi/873

コメント (8)

Stringクラスは「immutable」といって、
一回インスタンス化されると
その実体は変更されないというjavaの仕様と思います。

javaの教科書なんかによく出てますよ。

確認になりますが,
つまり,JavaやC#において,引数に参照型を持ってきた場合の「オブジェクトの参照渡し」とは,

引数のクラスのオブジェクト(ClassX x)と同じ場所を指す【アドレスをコピーした別のオブジェクト(ClassX x_copy)】が作られる、

と考えると良いわけですね.

つまり,
C++では引数は呼び出し元のxをそのまま名前を置き換えて使うので,xを使うことと同義になります.

一方で,JavaやC#では引数でxのアドレスをコピーした独立したオブジェクトが生成されるので,
x_copyの中身(=値)を変更すれば,呼び出し元のxの中身も変わりますが,
x_copy自体を代入(=アドレス変更),もしくは中身を変更しないメソッドを呼び出したところで,呼び出し元のxには何も影響は無い,
ということで,合っていますでしょうか?

ここまで丁寧に4つものサンプルを挙げて,Javaの「オブジェクトの参照渡し」を説明した記事はなかったと思います.
やっと,C++の「値の参照渡し」とJava/C#の「オブジェクトの参照渡し」との違いがよくわかりました.
ありがとうございました!

理解八分な初心者ですが、丁寧な解説公開に感謝します。どうもありがとう。

あるメソッドの引数へString型のOBJを渡し、その中で値を変更、関数の呼び出し元側では、その変更結果を出力するといった処理を組んでいたのですが、メソッド内での値変更が反映されず、、、悩んでいたところ、ここのホームページを発見しました。助かりました。

分かり易い説明ありがとうございます。

私自身、まったく同様の内容で混乱しわけが分からなくなっていました。

本当に分かり易い説明ありがとうございます。

とても分かりやすく、混乱していた部分がスッキリしました。

ありがとうございます!

Theres nothing like the rieelf of finding what youre looking for.

JavaからCに行ったくちで、完全に混乱していました。
まさにドンピシャの解説で本当に助かりました。

コメントを投稿