javaのifとかforとかのことまとめとく。

以前書いてたこの記事で

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);    //なんかやる
}

ループの抜け方

ラベルなしbreakcontinueは、一番内側のループに対して行われる。

これに対して、ラベルつきbreak,continueを使用することで、指定したループに対しても行える。

知らなかったけど、javaでもラベルはある。 使ったことがないけど、外側のループに対してbreakcontinueをやりたい場面はある。 今までは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.Interableiterator()メソッド以外にもメソッドが増えてる。streamAPI関係??

java8ではfor-eachよりstreamAPI??

こちらも興味深いはなし。 こんな話もあるみたいなので、for文を書くことは今後なくなっていくのかもしれない。

【TODO】streamAPIの実装

streamAPIを試そうと思ったけど、ラムダ式というものがまずわかってないし、レベルがおいついてないので、後ほどやる。


【参考書籍】

プログラミング言語Java 第4版:第10章 制御の流れ

・Effective Java 第2版:項目45 ローカル変数のスコープを最小限にする

・Effective Java 第2版:項目46 従来のforループよりfor-eachループを選ぶ