Java : for 文の構文をもっかい考える

Java における単純な for 文の構文をもっかいおさらいする。言語仕様的にできることを見直して、可能性を把握しておく。

for 文の基本構文

for( 初期化式 (initialization); 条件式 (condition); 継続式(increment/decrement) ) {
    // 処理
}

これが基本構文。

List<String> list; // 要素がいっぱい入っているテイ

for(int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

こんな for 文はよく見かけるだろう。

for 文のカッコの中のセミコロンで区切られた3ヶ所は、それぞれ以下のような役割を持つ。

この3つが、3行のコードが記述できる特殊な場所、ということをよくよく考えると、変わったことができることに気が付く。

初期化式を複数記述する

初期化式はカンマで区切って複数記述できる。たとえば先ほどの List<String> をループで回すサンプルに対し、初期化式を複数書くと、こうなる。

for(int i = 0, length = list.size(); i < length; i++) {
    System.out.println(list.get(i));
}

list.size()length という int 型の変数に格納している。条件式部分も合わせてその length 変数を見るようにした。こうすると list.size() メソッドの呼び出し回数が減らせる。for ループの高速化テクニックとしてよく見かける部類に入ると思う (高速化の効果としてはたかが知れてるっぽい)。

無論、このサンプル程度の内容なら拡張 for 文 (for-each) の使用を検討した方が良いが、今回は拡張 for 文の話はしない。

for(String str : list) {
    System.out.println(str);
}

初期化式がなくてもいい

初期化式部分は、「初期化式」という名のとおり、セミコロンで文が終わるまで、カンマで区切って複数の変数に初期値を与えることができる。要するにさっきの例はこう書いても良い。

int i = 0,
    length = list.size();

for( ; i < length; i++) {
    System.out.println(list.get(i));
}

何の式も書かない場合であっても、セミコロンの数は合ってないとダメ。

初期化式は変数宣言を外出ししておけば型が違ってもいい

初期化式部分は変数の初期値宣言の行と同じ考え方ができる。ということは、どういう時に使えるかは分からないが、一応、こんなこともできる。

int i;
double j;
String str;

for(i = 0, j = 5.5, str = "Hoge"; i < list.size(); i++) {}

条件式は複数条件組み合わせられる

初期化式が複数書けるところから想像できたかもしれないが、条件式も複数の条件を組み合わせられる。

これも良い例がないが、言語仕様上はこんなことができる。

for(int i = 0, j = 10; i < 5 && j > 4; i++) {
    System.out.println(i + ", " + j);
    j -= 2;
}

// 出力結果 :
// 0, 10
// 1, 8
// 2, 6

条件式部分が true になる間だけループを繰り返すので、AND && 条件で変数 ij がそれぞれ5未満、4超えを満たす間だけループさせてみた。

条件式を使って break っぽくループを抜けてみる

以下のような break を使ってループを抜けるような処理を、条件式部分を使ってやってみる。

// list 中の要素に "DDD" が出てきたらループを抜けるサンプル
List<String> list = new ArrayList<String>(Arrays.<String>asList("AAA", "BBB", "CCC", "DDD", "EEE"));

for(int i = 0; i < list.size(); i++) {
    if(list.get(i).equals("DDD")) {
        break;
    }
    System.out.println(list.get(i));
}

// 実行結果 :
// AAA
// BBB
// CCC

条件式部分を使うと、こうなる。

// list は上のサンプルと同じで

for(int i = 0; !list.get(i).equals("DDD"); i++) {
    System.out.println(list.get(i));
}

// 実行結果 : 上に同じ

条件式部分は、その式を boolean に変換でき、それが true である間だけループを回す継続条件になっているので、否定の論理演算子「!」で「"DDD" とイコールではない」間だけループさせるようにしている。

条件式を使わないこともできるか

やっぱり break を使う構文に戻って考えてみると、有効利用できそうな機会が思いつかないが、条件式を空にすることもできる場合がある。

for(int i = 0; ; i++) {
    if(list.get(i).equals("DDD")) {
        break;
    }
    System.out.println(list.get(i));
}

必ず break できる条件が用意できてないと無限ループか IndexOutOfBoundsException あたりになるだろうけど。

やっぱり継続式も複数書ける

継続式は for ブロックの最後に行われる行が先頭に来ているもの、と思うと、色んなことができる。少し手前に出したサンプルを少し変えるとこんな感じ。

for(int i = 0, j = 10; i < 5; i++, j -= 2) {
    System.out.println(i + ", " + j);
}

// 出力結果 :
// 0, 10
// 1, 8
// 2, 6
// 3, 4
// 4, 2

意味分かんないコードだけどこんなこともできる。

int i;
String str;

for(i = 1, str = "" + i; i <= 5; i = i + 2 - 1, str = str + " " + i) {
    System.out.println(i + " : " + str);
}

// 実行結果 :
// 1 : 1
// 2 : 1 2
// 3 : 1 2 3
// 4 : 1 2 3 4
// 5 : 1 2 3 4 5

やっぱり継続式がなくてもいい

継続式を使わずに while 文チックに書いてみたり。

// list の要素を最後から順に表示
for(int i = list.size(); i != 0; ) {
    System.out.println(list.get(i - 1));
    --i; // デクリメント
}

インクリメントやデクリメントをしない継続式

Random rnd = new Random();
for(int i = rnd.nextInt(10); i != 0; i = rnd.nextInt(10)) {
    System.out.println(i);
}

Random#nextInt() は、「0」から「引数に与えた数値 - 1」までの数値をランダムに返すメソッド。nextInt(10) とすれば、0 から 9 のいずれかの数値が返される。

これを利用し、継続式部分でランダムな数値を生成させてみた。結果は「2, 6, 4, 5, 5, 3, 8, …」などランダムな数値が出力され、条件式に合わない 0 が出てきた時に終了する。

Random rnd = new Random();
for( int i  = rnd.nextInt(10);
         i != rnd.nextInt(10);
         i  = rnd.nextInt(10) ) {
    System.out.println(i);
}

こうすれば条件式に何が設定されたか分からないから、どの値が出るとループを終了するのかも分からなくなる。意味はない。

もう for 文を使わない・あるいは for 文のそれぞれの式の役割を見直す

初期化式、条件式、継続式の役割が分かったところで、いずれの式も使わない for ループを作ってみる。

int i = 0; // 初期化式
for( ; ; ) {
    if(i == list.size()) break; // 条件式
    System.out.println(list.get(i));
    i++; // 継続式
}

条件式は継続条件ではなく終了条件として記述しているが、基本的にこういうこと。for 文の式が使われなくて、心なしか泣いているように見える…。( ; ; )

しかしこれでとりあえず、for 文のカッコの中にどんなことが書けるのか、今までとちょっと違う理解の仕方ができた気がする。