こんにちは。ぬるりとQiitaデビューいたします。
当記事は以前別名義・別の媒体でネットの海に残したものを改稿したものとなります。
どうぞコーヒーでもご用意の上お付き合いください。
当記事の対象読者
- Java初心者
- Javaを復習したい人
- 値渡しと参照渡しを復習したい人
基本Javaの知識が最低限ある方に向けて書きますが、Java特有の記述を適当に流し読みしていただいても問題ございません。
#はじめに
そもそもJavaの参照渡しって難しくないですか?
全体的にはそこまで難しくないと思うのですが、初学者が避けては通れないにも関わらず、明らかに変な挙動をするヤツがいます。
そう、Stiring型。
君ですよ君。
ぶっちゃけ、
「String型は(intみたいな)基本型と同じ扱いでOK」
「けど比較するときは==を使わず.equals()を使おうね」
くらいの認識でもなんとかなります。
ただ、冷静に考えると気持ち悪いですよね?
public static void main(String[] args) {
String str1 = "文字列1";
translate(str1);
System.out.println(str1);//文字列1
}
private static void translate(String str2) {
str2 = "文字列2";
}
Javaにおいて基本型以外はオブジェクト(Objectクラスを継承)です。
で、String型は基本型ではありません。オブジェクトです。
オブジェクトは参照で渡されます。
……ということは、出力されるのは"文字列2"じゃないと変ですよね?
ここで"文字列1"が出力される理由を理解されている場合、多分この記事は不要です。
「String型はぶっちゃけ基本型みたいなもんだから」は……正解ですが不正解です。ごめんね。
当記事では初学者(インスタンスの意味くらいは分かる方)向けに**「何が起こっているのか」**を噛み砕いて解説したいと思います。
実のところ、Javaをある程度理解されている方には「String型はimmutableだからこうなるだけだよ」とか「Javaはそもそも全て参照の値渡しだ!」とか言いたくなる方も多いでしょう。が、初学者には負担が大きいと思います。
Javaについてかなり幅広く「値渡し」「参照渡し」の考え方に準拠した解説がある以上、「とりあえずJavaを学ぶ上で『値渡し』『参照渡し』がそれぞれ何を言いたいのか」理解することは無益ではないでしょう。
ということで行ってみましょう。
#基本中の基本:「値渡し」の場合
基本中の基本ですが、確認しましょう。
public static void main(String[] args) {
int num_sender = 1;
add(num_sender);
System.out.println(num_sender);//1だよ
}
private static void add(int num_receiver) {
num_receiver += 1;
}
オススメの覚え方は**「num_sender君の頭の中にある"1"という数字を『白いカードに書いて』add()メソッドに引き渡している」**というイメージです。
頭の中にある数字をカードに書いて、渡す。
add()メソッドで何がされようとも、num_sender君の頭の中にある数字は変わりません。
ただし、こちらの場合……
public static void main(String[] args) {
int num_sender = 1;
num_sender = add(num_sender);
System.out.println(num_sender);//2だよ
}
private static int add(int num_receiver) {
num_receiver += 1;
return num_receiver;
}
は、明示的にnum_sender君が**「変更後の数字を覚えろやァ!!!!」**と命じられているわけなので、出力は"2"になります。
まあ当然ですね。
「簡単すぎる」と思われた方もいるかもしれませんが、最後までお付き合いください。
後々効いてくるので。
#基本中の基本2:参照渡し
public static void main(String[] args) {
List<String> onlyOneList = new ArrayList<String>();
onlyOneList.add("2018年です");
replaceFirstItemTo2019(onlyOneList);
System.out.println(onlyOneList.get(0));//"2019年です"
}
//リストの最初の要素を2019年にしちゃいます
private static void replaceFirstItemTo2019(List<String> another_hensu) {
another_hensu.add(0, "2019年です");
}
ちょっと露悪的な変数名にしてみましたが、とりあえずよく言われている参照渡しは「たった一つの物体(オブジェクト)を操作している」ことをイメージしてください。
とはいえ違和感が残る場合、こう考えてください。
変数”onlyOneList君”の中身は、豪華ホテル"memory"の20号室で昼寝してます。
20号室っていう番号はnew ArrayList();したときになんとなく決まりました。
……さらっと書きましたけどめっちゃ大事なところなので、覚えておいてください。
…………めっちゃ大事やで?言ったからな?
それはそうと、replaceFirstItemTo2019()メソッドに引き渡されているのは
"20号室"
っていう情報なんです。
で、replaceFirstItemTo2019()メソッドでは20号室に寝てるオンリーワンなList君の最初の要素を"2019年です"に書き換えちゃってます。最終的に出力される情報は"20号室の情報"、つまり"2019年です"に書き換えられた情報なんですね。
これがJavaチョットデキル人たちが言う「参照の値渡し」ってやつです。
ここで「参照」と「参照の値」ってどう違うの?と思われた方は……余白が足りないので忘れてください。
さて、復習が終わりました。
ここからちょっとだけ応用です。
#ここが難しいよねJava
こちらのコードをお読みください。
public static void main(String[] args) {
List<String> onlyOneList = new ArrayList<String>();
onlyOneList.add("2018年です");
replaceFirstItemTo2019(onlyOneList);
System.out.println(onlyOneList.get(0));//"2018年です"
}
//リストの最初の要素を2019年にしちゃいます
private static void replaceFirstItemTo2019(List<String> another_hensu) {
another_hensu = new ArrayList<String>();
another_hensu.add(0, "2019年です");
}
ナンデ2018年のままなの!???
もうだめだぁ・・・おしまいだぁ
冗談抜きに実務でこれやっちゃう場合あるのよね。気をつけよう!
ここでさっきの例から書き換えたのは、この一行だけ。
another_hensu = new ArrayList<String>();
でも、これって滅茶苦茶重大なことやっちゃってる。
さっき書いたことを思い出して欲しい。
変数”onlyOneList君”の中身は、豪華ホテル"memory"の20号室で昼寝してます。
20号室っていう番号はnew ArrayList<String>();したときになんとなく決まりました。
つまり、newって何をしてるかっていうと**「ホテル(メモリ)の寝室を割り振った上で、新しいモノ(オブジェクト)を誕生させる」**ってことなのね。
ここでは"anothe_hensu君"の中身に"21号室"とか、とにかく20号室とは違うどこかの部屋番号が割り振られた上で、新しいListが誕生しちゃってる。
そして変数”onlyOneList君”が記録する情報は「20号室」のまま。
さらに困ったことに、この時点でListはオンリーワンではなくなってしまった!
で、"21号室"のListには"2019年です"が格納されたけれど、”onlyOneList君”が保持する情報である"20号室"のListには何の変化もなく、そのまま"2018年です"が出力された。……ということ。
#String型の場合
ここまでお読みいただいた方ならなんとなく理解できると思います。
public static void main(String[] args) {
String str1 = "文字列1";
translate(str1);
System.out.println(str1);//文字列1
}
private static void translate(String str2) {
str2 = "文字列2";
}
このコードだけど、String型は確かに「オブジェクト」で、「参照渡し」で渡されます。
でも、String型は"immutable"です。
……どういうことやねん?
オブジェクトを直接いじれないってことです。
……
……どういうことやねん!?キレるで!!?
まず、前提としてStringの場合「例外的に」
String str1 = "文字列1";
この記述だけでString型のオブジェクトが「豪華ホテル"memory"の適当な部屋に生まれて、格納される」のです。
今回は雑に"22号室"に生まれたとしましょう。
もう一度、書き方を変えて書きます。
(String) 変数名 = "hoge";
この記述だけで、String型のオブジェクトが「豪華ホテル"memory"の適当な部屋に生まれて、格納される」のです。
ということは?
str2 = "文字列2";
こう書いた時点で"23号室"(なのか24号室なのか5000兆号室なのか知りませんが、とにかく22号室ではない別の部屋に)生まれちゃってるわけです。
じゃあ"22号室"の中で寝ている"文字列1"を変更する方法は……!?
そんなものはない。
残念でした。諦めましょう。
つまるところ、これが「String型はimmutable」とか「オブジェクトを直接変更できない」とか、そういう意味です。
プログラムを記述している人から見た変数の中身(値)は書き換えることができます。「22号室」から「23号室」と書き換えることはできる、ってことです。
で、結果的にこの動きは基本型 = 値渡し的な挙動になります。
おしまい。
##応用:結局String型は何をしてるの?
(ここは読み飛ばしていただいて構いません)
要するに、
String str1 = "文字列1";
は
String str1 = new String("文字列1");
のシンタックスシュガーです。
以上!
すまん嘘だ。ごめんな。
ぶっちゃけこの記事書くまで僕自身そう認識してましたが、反証があるんですよね……
public static void main(String[] args) {
String a = "あ";
String b = "あ";
System.out.println(a == b); //true
}
あ、君そういえばtrueだったね……
この挙動、初学者がやってみて「あるぇー???.equals()わざわざ使わなくても==で比較できるじゃーん???」とか考えて色んな人に聞いてみるも納得できる回答が得られないやつだ。僕がそうでした。
(参考文献)
Javaにおいて “Hoge” は単なる new String(“Hoge”) のシンタックスシュガーではない
http://blog.supermomonga.com/articles/java/difference-bewtween-two-way-to-create-string-instanc.html
おそらくだけど、メモリ節約のため全く同じ文字列の場合オブジェクトを使い回すような機構が存在してるんでしょう。
→正解でした。
""(ダブルクォート)による初期化とnew String("")による初期化の違い
http://education.yachinco.net/tips/java/06/2.html
めでたしめでたし。
##参考
もう参照渡しとは言わせない
https://qiita.com/mdstoy/items/2ef4ada6f88341466783
終わり。
またどこかでお会いしましょう。