以前書いてたこの記事で
http://snoopopo.hatenablog.com/entry/2014/12/15/235222
for文のループ判定式がループする度に呼ばれていることを僕はわかっていなかったのだけど(@@;)
よくよく見るとプログラミング言語Java第4版にちゃんと書いてあるし、 ループ文というものは判定式がfalseになったら抜けるものなのだから、 ループする都度判定してるのは当然な話なんだけど、我ながらなんとも考えが浅いなあ。
要点を改めてまとめてみよう。
for文のループ式はループの度に呼ばれる
最初に書いときます!
for (int i = 0 ; i < array.size() ; i++) { doSomething(i); //なんかやる }
よくやっていたのが上のような例。 リストのサイズを見るのにわざわざ変数にいれる必要なくない?と思っていてこう書いてたけど, array.size() はループの都度評価されてる。
for (int i = 0 ; i < array.size() ; i++) { array.add("hogehoge"); //addするのでsize()での戻り値が変わる }
とかやってたら、当然無限ループだ。
java.lang.OutOfMemoryError: Java heap space
とか出た。
以下のようにいったん変数にいれてあげる必要がある。
int n = array.size(); for (int i = 0; i < n ; i++) { doSomething(i); //なんかやる }
for文のローカル変数
上術したこれに対して、
int n = array.size(); for (int i = 0; i < n ; i++) { doSomething(i); //なんかやる }
EffectiveJavaで紹介されているのは、 変数のスコープをできるだけ小さくした方がいい、という観点からこういう書き方をしてる。
for (int i = 0, n = array.size(); i < n ; i++) { doSomething(i); //なんかやる }
forのローカル変数宣言で宣言された変数は、forブロック内でしか使えないので、スコープが狭くなる。
forの中では上で書いた通り複数ローカル変数を宣言することができる。 ただしこれは同じ型の場合だけで、型の違う複数の変数を宣言することはできない。コンパイルエラーとなる。
for (int i = 0, n = array.size(), String str = ""; i < n ; i++) { doSomething(i); //なんかやる }
ループの抜け方
ラベルなしbreak
やcontinue
は、一番内側のループに対して行われる。
これに対して、ラベルつきbreak
,continue
を使用することで、指定したループに対しても行える。
知らなかったけど、javaでもラベルはある。
使ったことがないけど、外側のループに対してbreak
やcontinue
をやりたい場面はある。
今まではreturnして、後続の処理を無駄に2回書いたりしてしまっていた。←冷静になるとひどすぎる;
ラベルをちゃんと{}で囲めば、可読性も悪くないし使うべき場面では使うべきだ。
LabelTest: { for (int cntA = 0, sizeA = arrayA.size() ; cntA < sizeA ; cntA++) { System.out.println(arrayA.get(cntA)); for (int cntB = 0, sizeB = arrayB.size() ; cntB < sizeB ; cntB++) { System.out.println(arrayB.get(cntB)); break LabelTest } } }
ループの中でreturn
をした場合は、ループはもちろん中断されるし、メソッドも当然抜けるので、
以下の例だと、"poyopoyo" は1回だけ出力されて、 "hoge" はされない。
for (int i = 0, n = 3, i < n ; i++) { System.out.println("poyopoyo"); return; } System.out.println("hoge");
ループの中でExceptionが発生した場合も、ループは中断されて例外処理を実行する。 この例だと、"poyopoyo" が出力されて "booboo" が出力されて処理が終わる。
try{ for (int i = 0, n = 3, i < n ; i++) { System.out.println("poyopoyo"); throw new Exception(); } } catch(Exception e) { System.out.println("booboo"); }
switchではdefaultを書こう!でもdefaultでnullは拾えないワナ。
switch文のdefault
は省略できるけど、書くべき。
default
の処理を書いておけばcaseの追加忘れにすぐ気づける。
int ii = 1; switch(ii) { case 1: System.out.println("1"); break; case 2: System.out.println("2"); break; case 3: System.out.println("3"); break; default: System.out.println("err!!"); //↑case 4 書き忘れてた場合、err!!が出るから気づける! }
java7からはswitch文に文字列も使えるようになった。
注意したいのはStringがnullの場合はどうなるか?ということ。 defaultケースを処理せずに、NullPointerExceptionが発生する。
java7からのStringに限らず、enum型やintをボクシングしたInteger型がnullの場合も同様で、 defaultケースを処理せずに、NullPointerExceptionが発生する。
上のページでnullの場合を考慮したイディオムの説明が書かれている。
Integer ii = null; if (ii == null) { System.out.println("null."); } else switch(ii) { case 1: System.out.println("1"); break; case 2: System.out.println("2"); break; case 3: System.out.println("3"); break; default: System.out.println("err!!");
else-switch
なんて書き方ができるとは!
if/else-if/switch の使い分け
まずelse-if
が使える場面では使うべき。
if (temp1()) { } if (temp2()) { }
と
if (temp1()) { } else if (temp2()) { }
は当然違う。
後者のelse-if
を使った場合は、temp1()がtrueの場合にtemp2()は評価されない。
確実にtemp1()がtrueである場合にtemp2()がfalseになる場合はelse-if
を使う。
if
を使わないでswitch
を使う理由は、
こんな風に、case1でもcase2でも同じことさせたい場合かなあ。
switch(i) { case 1: case 2: System.out.println("2"); break; case 3: System.out.println("3"); break; default: System.out.println("err!!"); }
あとは、break
をかかずにcase1,case2だと、"2"と"3"を出力させてほしいけど、
case3だと"3"だけ出力させてほしい、みたいな場合かなあ。って思ってる。
switch(i) { case 1: case 2: System.out.println("2"); case 3: System.out.println("3"); break; default: System.out.println("err!!"); }
whileよりforを使う
ループ変数の内容がループが終了した後に必要ない場合には、whileループよりforループを選んでください。 (EffectiveJava page.204)
EffectiveJavaでは、これもローカル変数のスコープを小さくするという観点から、whileよりもforを推奨している。
forよりfor-eachを使う
for (String a : arrayA) { System.out.println(a); //なんらかの }
for-each
を使うと見やすい。変数が散らからない。
for-each
が使えない場面としてはインデックスが必要なとき。
EffectiveJavaではコレクションからオブジェクトを削除するときや、変換したりして詰め直すときを上げている。 また、複数ネストしたループだけど、並列に複数のループをイテレートしたいときは、 同じインデックスでイテレートさせたいわけなので、インデックスが必要となり、for-eachではできない。
それから、for-eachはコレクションの昇順で処理されるから、 リストの後ろから要素をとりたい場合など、従来のfor文ではインデックスをデクリメントして処理してたようなことは出来ない。
【TODO】for-eachの実装
for-eachが使えるオブジェクトを作るには、java.lang.Iterable
を実装する必要がある。
ジェネリクス学習時にやる。
#java8でjava.lang.Interable
にiterator()メソッド以外にもメソッドが増えてる。streamAPI関係??
java8ではfor-eachよりstreamAPI??
こちらも興味深いはなし。 こんな話もあるみたいなので、for文を書くことは今後なくなっていくのかもしれない。
【TODO】streamAPIの実装
streamAPIを試そうと思ったけど、ラムダ式というものがまずわかってないし、レベルがおいついてないので、後ほどやる。
【参考書籍】
・Effective Java 第2版:項目45 ローカル変数のスコープを最小限にする
・Effective Java 第2版:項目46 従来のforループよりfor-eachループを選ぶ