8月に入り、いよいよ本格的な夏の到来という季節になりました。プライベートで趣味やアウトドアを楽しむにはいい季節ですが、仕事をする上ではちょっとつらいシーズンですね。会社によっては遅くまで残業してデバッグをしていたら、深夜の時間帯はビル全体の空調が切られてしまい、さらに過酷な環境でデバッグを強いられる、という経験をされた方もいらっしゃるのではないでしょうか。この連載が皆さんの残業時間を減らすのに少しでも貢献できていればいいのですが……。では今月もどうぞお付き合いください。
条件分岐で値が変化するはずなのに、いつも同じ値になってしまう
分類:仕様どおり動かない
プログラムを作成するとき、条件にかかわらずいつも同じ結果になるような要件でコードを書くことは、普段は少ないのではないかと思います。ユーザーが入力した内容であったり、外部にあるデータの状況によって結果が変わるようなプログラムが多いのではないでしょうか。そのような場合、制御文による条件分岐が使われます。しかし、条件分岐をして結果が同じにならないはずなのに、いつも同じ結果しか出ない、という状況に陥ってしまうこともあります。次の例をご覧ください。
package kx; public class Tips3_6 { public static void main(String[] args) { int dice = (int)(Math.random()*6)+1; String value = ""; switch(dice) { case 1: value = "一"; case 2: value = "二"; case 3: value = "三"; case 4: value = "四"; case 5: value = "五"; case 6: value = "六"; } System.out.println(value); } }
このプログラムは、Mathクラスのrandomメソッドを利用して1〜6の範囲の乱数を発生させ、その値に対応した漢数字を表示するという仕様のプログラムなのですが、何度実行してもなぜか必ず「六」と表示されてしまい、仕様どおりに動作しません。
このような場合、コンパイルエラーもランタイムエラーも発生しないので、プログラムの何行目がおかしいか、というような情報は表示されません。ですから、プログラムのどの部分で仕様どおりでない動作をしているのかは、自分で解明しなければなりません。このようなケースでは、「どこまでが正常に動作していて、どこからが正常に動作していないか」を見極めることが、すなわち不正な動作をしている個所を特定することであるといえます。このプログラムでも、上から順に処理を考えていきますと、以下のような流れになります。
(1)1〜6の乱数を発生させ、変数に格納(4行目)
(2)表示用の変数を空文字に初期化(5行目)
(3)switch〜case文で、乱数の内容に合わせて処理を分岐(6〜19行目)
(4)変数の内容をコンソールに表示(20行目)
どこからどこまでが正常で、どこからが正常でないかを考えてみましょう。とはいえ、(2)と(4)が間違っているとは考えにくいので、ここでは(1)と(3)に絞って考えてみます。
ではまず(1)の内容を検証してみましょう。このプログラムでは発生させた乱数をdiceという変数に代入していますので、この内容を実行時に確認できるようにしてみましょう。デバッガを使う方法もありますが、ここでは説明のため簡易な方法としてSystem.out.println()を使って値の表示をさせてみます。
package kx; public class Tips3_6 { public static void main(String[] args) { int dice = (int)(Math.random()*6)+1; System.out.println(dice); String value = ""; switch(dice) {
すると、図1のようになります(数字の部分は乱数によって変わる)。つまり、乱数は仕様どおりに生成できていることが分かります。つまり(1)は正常に動作しているので、この場合(3)で仕様を満たさない動作になっていることが予想されます。
もう、お分かりかとは思いますが、(3)のswitch〜case文には、break文が全く記載されていません。そのためswitchでどのcaseに分岐しても、breakでswitch〜case文が終了せずにその下のcaseが次々と最後まで実行されてしまい、変数valueにはいつも「六」が代入されてしまっているのです。
今回のプログラムの場合は、switch〜case文での分岐の結果が表示に直結しているために、原因の特定が割合分かりやすかったのですが、switch〜caseの分岐で、表示結果に直接かかわらない中間的な処理をしている場合には、そこが原因となっていることになかなか気が付かないこともあります。また、breakを入れ忘れるという一見分かりやすいミスでも、あるcaseに対してだけ付け忘れていたりすると、特定の条件下だけ仕様どおりに動作しないという場合も出てきますので、なかなか原因個所を特定できない場合もありますから、注意しましょう。
条件分岐しているのにいつも同じ結果になる=switch〜caseを使っている場合はbreakの入れ忘れに注意
バグの少ないコーディングのヒント
「特殊な戻り値」をなるべく作らない
仕様どおり動かないアプリケーションのデバッグをする場合、厄介なのが、常にうまく動かないわけではなく、特定の条件下のみうまく動かないようなケースです。処理の内容によっては、渡した引数の内容や実行時の諸条件が原因で、処理を正常に実行できなかったり、正常に値が返せなかったりすることがあると思います。そのような場合に、「特殊な戻り値」を返すようにして判断するのは、あまり良いプログラミングとはいえません。
例えば、次のような例を見てください。
package kx; public class Tips3_7_1 { public double triangleArea(int bottom,int height) { // 底辺または高さがマイナスの場合は、−1を返す if ((bottom<0)||(height<0)) { return -1; } // それ以外の場合は、底辺*高さ/2(三角形の面積)を返す return bottom*height/2; } }
このプログラムのtriangleAreaメソッドは三角形の面積を求めるメソッドですが、底辺や高さを表す引数bottomとheightの内容が不正(負の値)な場合に、「−1」を戻り値に返すように実装されています。確かに、triangleAreaメソッドは面積を求めるわけですから、正常な戻り値は正の値だけを返すので、「−1」という値が正常な値でないことはメソッドの内容を知っていれば理解できます。
ですが、裏を返せば「−1」が特殊な値であるかどうかはメソッドの内容に強く依存していることになります。もしも、このメソッドの仕様に変更が行われ、正常な戻り値にマイナスが含まれることになると、「−1」は特殊な値でなくなってしまうのです。またメソッドを呼び出す側も、メソッドの内容をよく見ていないと、その戻り値が特殊な値であるかどうかすら気が付かない可能性があります。
Javaでは幸いにして例外処理を行うことができるようになっています。このようなケースでは、例えば戻り値はあくまで処理が正常に終了した結果として扱い、処理が正常に実行できない場合には、例外を送出するなどして、正常時と異常時の状態を明確に区別することもできるのです。このメソッドを利用する側にとっても、処理が正常に行えなかった場合の対処をtry〜catch構文のcatch節に記述することで、正常な処理かそうでない処理かが明確になり分かりやすいコードにつながります。
package kx; public class Tips3_7_2 { public double triangleArea(int bottom,int height) throws Exception { // 底辺または高さがマイナスの場合は、例外を送出する if ((bottom<0)||(height<0)) { throw new Exception("底辺または高さが不正です。"); } // それ以外の場合は、底辺*高さ/2(三角形の面積)を返す return bottom*height/2; } }
switch〜caseでは必ずdefaultブロックを作る、if〜else if の分岐では必ず最後にelseブロックを作る
条件に応じて処理を分岐する場合、Javaではswitch〜case構文や、if〜elseの構文が使われます。これらを使って分岐処理を記述するとき、プログラマ本人は、仕様がよく分かっていて選択肢がどのようなものに限定されているかも網羅していることがほとんどだと思います。なので選択肢が限定的な場合、switch〜case構文にcase節しか作っていなかったり、if〜else if構文を使う場合にもelse節を作っていなかったりすることがあると思います。ですが実際には、作った本人が想定していない状況が発生し、本来あるはずのない条件で分岐を行う可能性がないとはいえません。次の例を見てください。
package kx; public class Tips3_8 { public static void main(String[] args) { int dice = (int)(Math.random()*6); String value = ""; switch(dice) { case 1: value = "一"; break; case 2: value = "二"; break; case 3: value = "三"; break; case 4: value = "四"; break; case 5: value = "五"; break; case 6: value = "六"; break; } System.out.println(value); } }
このプログラムでは、1〜6の範囲で乱数を発生させ、それを漢数字として表示するプログラムです。このプログラムには実はバグがあり、乱数は0〜5の範囲でしか発生しないようになっています。このような場合、プログラマが想定しなかった状況(発生する乱数に0が含まれていて、6は含まれない)になるため、漢数字が表示されないまま終了してしまうことがあります。このプログラムの場合は最後に表示を行うので異常に気付きやすいのですが、そうでない場合、switch〜caseの分岐に当てはまらない条件が与えられていることすら気付かない可能性があります。そのような状況を想定し、switch〜case構文にはdefault節を、if〜else if構文にはelse節をそれぞれ記述しておくようにしましょう。すると想定外の分岐が行われようとしているときに、プログラマにそういった状況になっていることを、default節やelse節の処理によって知らせることができるようになります。
package kx; public class Tips3_8_2 { public static void main(String[] args) { int dice = (int)(Math.random()*6); String value = ""; switch(dice) { case 1: value = "一"; break; case 2: value = "二"; break; case 3: value = "三"; break; case 4: value = "四"; break; case 5: value = "五"; break; case 6: value = "六"; break; default: System.out.println("dice変数が想定外の値です:"+dice); break; } System.out.println(value); } }
Copyright © ITmedia, Inc. All Rights Reserved.