Javaでプリミティブ型の引数を関数内部で更新する
毎日暑いですね
どうも。お疲れさまです。ガンズターンのRyosukeです。
まだAppStoreの審査(2回目)が始まりません……。
やはり先日のリジェクト後にバイナリを提出しなおしたせいで、審査のキューの最後尾まで回されてしまったのでしょう。
自業自得なので、首を長くして待つ所存です。
それにしても、今週に入ってから急に暑くなりましたね。
6月中は一度も活躍しなかったエアコンですが、なんと7月に入ってからは毎日数時間稼働しています。
……なので、電気代の請求がちょっと怖いです。
この数日間というもの、もっぱら「ぱずもぐ!」をAndroidへ移植するための勉強をしています。
AppStoreへの提出とAWSの勉強がひと段落したので、次なるステージに進まなければというわけです。
実は、かつて「りずもぐ!」をAndroidへ移植するためにJavaを勉強したことがあったんですが(かれこれ3年前のお話)、けっきょく「とりあえず動く」ところまで作り込んでずっと放置している状態でした。
それからずっとJavaには触れてこなかったので、また1から勉強のし直しです。
Javaはなかなか手強い言語のイメージがありますね。
なんといっても、Javaを扱う人それぞれの中で言語仕様の解釈が微妙に違っていたりするのが、やっかいだと思います。
3年前に、すでに一度勉強していたはずなんですが、何一つ覚えていることがないので、またしても手探りからの出発。
とりあえず「麦茶ライブラリ」の全クラスの骨組みを作成して、一つ一つ、手作業でObjective-CからJavaの構文へ修正しています。
一応現時点で、OpenGLを使ったスプライトの表示(画像及びテキスト)ぐらいならiPhone版と遜色ない出来で動作するようになりました。
今のところの野望は、麦茶ライブラリの各クラスのインターフェースを完全にiPhone版とAndroid版で一致させて、構文さえ手直しすればほぼ同じソースでどちらでも動くという状態にまで持っていきたいですね。
(なおかつ、構文の自動変換をするスクリプトまで作れたら完璧!)
そこさえきちんと作り込めたなら、今後は、まずiPhone版をしっかりと作り、その審査期間中にAndroid版を作成して同時期にリリース、なんて流れも見えてきます。
ちなみに、今日までの時点で、Javaの仕様でつまずいたところを、なんとなくまとめてみますね。
個人的な備忘録を兼ねているので、かなりふんわりした内容です。
むしろ、自分自身よく理解できていないことを自覚したうえで、メモとしてまとめている内容ですので、もし「それ間違ってるよ」というご意見があればメールでもコメントでもいいのでお教えいただけると助かります。
もくじ
1. やりたいこと = 引数で与えられた変数(プリミティブ型)に対する関数(メソッド)内での値更新
わたし、会社員時代はかなりレガシーなシステムに携わっていたこともあり、プログラミングの原体験は「C言語」でした。
C言語では、関数に引数として渡した変数を、関数内部で新たな値に更新することが簡単にできます。
例えば、以下のような感じ。
void mainFunc() {
int a = 0;
exampleFunc(&a);
printf("a = %d", a);
}
void exampleFunc(int *local_a) {
(*local_a)++;
}
この時、mainFunc()を動作させると、結果は以下のようになります。(実際にコンパイラ通して動作確認したわけじゃないですが、こうなるはず。笑)
> a = 1
最初にaに代入されてるのは「0」ですが、exampleFunc()の中で、与えられた引数(local_a = &a)に対して「(*local_a)++」をすることによって、値をインクリメントできてるわけですね。
ちなみに蛇足ですが、C言語では変数名の頭に「&」をつけることで、その変数が格納されているアドレス値を直接指し示すことができるのです。
なので、ポインタ変数(int *local_a)に対して、直接的に任意の変数のアドレス値(&a)を与えることができるというわけです。
さらに、C言語ではポインタ変数の頭に「*」をつけることで、そのポインタ変数が指し示す実体を操作することが可能です。
このようにして、慣れてくるとかなり柔軟に関数の内外での値の操作が可能になるので、個人的にはけっこう使ってしまう機能でした。
(ちなみに、Objective-CはC言語の上位互換という位置付けなので、これらの文法はそのまま使用することができます)
2. JavaではCのようにはいかないということ
……さて。問題はここからです。
Javaでは、C言語で許されている「&a」や「*local_a」のような記法が存在しません。
関数に渡される引数は常に、「プリミティブ型」であれば「同じ値を持った変数のコピー」になり、「参照型」であれば「同じアドレス値を指し示すポインタ変数のコピー」になります。
(このことを理解できるまで、自分はけっこう時間かかりました。こうして書いている今でも、実はなんだかモヤモヤとしてはいます)
どういうことかというと、例えば以下のような実装では意図した通りの動作はしないということです。
void mainFunc() {
int a = 0;
exampleFunc(a);
System.out.println(String.format("a = %d", a));
}
void exampleFunc(int local_a) {
local_a++;
}
上記の動作結果は以下の通り。(になるハズ!)
> a = 0
はい。exampleFunc()の中で、ローカル変数をインクリメントしているはずなのに、それがまったく反映されていませんね。
なぜこうなるのかを、簡単に図式化したのが以下です。
(かなり適当な図で申し訳ないですが、何もないよりはマシですよね)
Javaでは「int a」の実体を指し示すアドレス値を直接渡すことができないので、そもそもC言語のような書き方をすることができません。
3. 「参照型」である「Integer」クラスならどうなる?
それじゃあ、プリミティブ型ではなく参照型である「Integer」クラスを使ったら、どうなるでしょうか?
void mainFunc() {
Integer a = new Integer(0);
exampleFunc(a);
System.out.println(String.format("a = %d", a.intValue()));
}
void exampleFunc(Integer local_a) {
local_a++;
}
上記の動作結果は以下の通り。(たぶん)
> a = 0
う〜ん……。
Integerは参照型だから、関数内へはアドレス値が渡っているはずなのに、なんで意図した結果が出せないのでしょう……?
これにはなかなか頭を悩ませられました。
実はこの「Integer」というクラスは「Immutable(イミュータブル?)」、つまり「一度作成したインスタンスの属性は変えられない」という性質を持ったクラスだったのです。
そしてこの「Immutable」という性質によって、exampleFunc()内の「local_a++;」という部分の持つ意味が、通常の「int」型変数に対する「a++;」とまるっきり変わってきてしまうのでした。
なんと「local_a++;」という部分では、内部的には以下のようなことが行われているようです。
Integer tmp = new Integer(local_a.intValue() + 1);
local_a = tmp;
つまるところ、「local_aの値を+1した新たな変数」を作成して、そのアドレス値を「local_a」に代入していただけなんですね。
なので、もともとのlocal_aが指し示していた実体(呼び出し元の「a」の実体とも言えます)にはなんら影響を与えることができなかったのです。
これらのことを図にすると以下のようになります。
4. じゃあけっきょくできないの? → 配列使えばできます
さて。
ここまでのところ、JavaでC言語のように「引数で渡された変数(プリミティブ型)の値を関数内で更新する」ことはできそうにないと思えてきそうですが……。
実のところ、意外と簡単に問題は解決しちゃうのです。
結論から言ってしまえば 「値が1つしかなくても、むりやり配列にして、0番目の要素に対して操作すればOK」ということです。
以下に例を示します。
void mainFunc() {
int[] a = new int[]{0};
exampleFunc(a);
System.out.println(String.format("a = %d", a[0]);
}
void exampleFunc(int[] local_a) {
local_a[0]++;
}
上記の動作結果は以下の通り。(きっと)
> a = 1
うん……。
配列の本来の使い方と全然違うけど、一応、意図した挙動は導きだせました。
一応、この動作が意図した結果となった理由を図にすると以下の通り。
5. Javaをもっと勉強せんといかんですね
今のところ(といってもまだ本格的にJava勉強しはじめてから3日ですが)、わたしはこのやり方以外に関数内でプリミティブ型を更新する方法を知りません。
もちろんコーディングの工夫次第で、このようなある種「裏技的」な解決方法を試みる必要すらなくなるわけですが(例えば、普通に関数の返却値に更新後の値を含めておいて、それを使って呼び出し元の変数を更新する。ただしこの場合、一度に更新したい変数が複数存在するとき、それら全ての値を1つの返却値の中に含めなければならなくなって、それはそれで面倒です)、なんとかもうちょっと素直なやり方で解決したいものです。
……うん。Javaに対する勉強がまだまだぜんぜん足りない気がしてきた。
今度、きちんと本買って読もう。