5月から始まったこの連載ですが、気が付くともう9月。まだまだ残暑も厳しい季節ではありますが、そんな中にも虫の声や朝晩の涼しさなどに少しずつ秋の気配が漂い始めますね。そして、秋と言えばスポーツの秋、食欲の秋、読書の秋、そしてデバッグの秋(?)ということで、秋の夜長に「読書」といえるかどうか分かりませんが、この記事を読んで知識を深めていただければと思います。今回も最後までどうぞお付き合いください。
分類:仕様どおり動かない
プログラムのデバッグをしていると、コードを追加したり修正したはずなのに、その処理が動作に反映されていない、そんな状況に遭遇することがあると思います。次の例を見てください。
package kx; public class Tips4_4 { public double calc(int a,int b) { return a*b/100; } }
このクラスのcalcメソッドは、int型の引数aとbを使って演算した結果を返すメソッドです。例えば、aに税抜き価格、bに税率(整数)を与えると、aの税込み金額を求めることができます。
次に、このクラスを継承して、Tips4_4_1というクラスを作ってみます。このクラスは、Tips4_4のcalcメソッドをオーバーライドしており、2つの引数を単純に足し算した結果を返すようにしました。
package kx; public class Tips4_4_1 extends Tips4_4 { public double calc(double a,int b) { return a+b; } }
そして、このTips4_4_1クラスを使って演算結果を表示するTips4_4_2クラスを作り、実行結果を確認してみます。
このクラスの実行結果は、以下のようになります(図1)。
package kx; public class Tips4_4_2 { public static void main(String[] args) { Tips4_4_1 t = new Tips4_4_1(); int a = 56; int b = 43; System.out.println("a+b="+t.calc(a,b)); } }
calcメソッドの引数には、それぞれ、56と43を与えています。Tips4_4_1でcalcメソッドをオーバーライドしたはずですので、メソッドの戻り値は2つの値の和、すなわち99になるはずなのですが、全く異なる値が実行結果として表示されています。これはどうしてでしょうか?
この例では、Tips4_4_1でのメソッドのオーバーライドに問題があります。親クラスのTips4_4では、calcメソッドはint型の引数が2つ定義されています。ですが継承クラスのTips4_4_1では、calcメソッドの引数が、double型とint型になっているのです。つまり、オーバーライドしたつもりのメソッドが、オーバーライドになっていなかったのです(図2)。Javaでは「メソッド名」「引数リスト」「戻り値」がすべて一致していないとオーバーライドとは見なされません。このケースでは引数リストの内容が一致していないために、オーバーライドではなくオーバーロードと見なされてしまい、呼び出しが行われなかったというわけです。
ここでご紹介したプログラムは戻り値を伴う例ですので、原因が分かりやすいといえるのですが、戻り値を返さないようなメソッドのオーバーライドですと、オーバーライドの誤りとすぐに分からない場合もあると思います。正しく書き直した、追加したはずなのに、そのとおりの動作をしない場合は、そもそも自分が変更した部分の処理が実行されているかどうかを、デバッガのステップ実行などで確かめてみるのも1つの方法です。もし、ステップ実行してみて、実行されるはずの追加・変更個所が実行されずに終了してしまう場合は、今回のようなオーバーライドの不備の可能性が高いといえます。フレームワークなどを利用して開発をしている場合などには、特定の親クラスの継承、メソッドのオーバーライドを行うことが多いと思いますので、そういったケースでも注意してみてください。
追加・変更したはずの処理が実行されない=クラスの継承関係を確認し、正しくメソッドのオーバーライドができているかを確認
分類:仕様どおり動かない
Javaを用いたシステム開発では、コードの開発はWindowsマシンで行い、実行(運用)はUNIXマシンで行う、というケースがよくあると思います。JavaはVM(仮想マシン)という仕組みを取ることによって、プラットフォームに依存しないプログラムを書くことができるのですが、やはり環境の違いを考慮しなければならないことも現実にはあります。次の例を見てください。
package kx; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; public class Tips4_5 { public static void main(String[] args) { try { File f = new File("sample.txt"); FileWriter fw = new FileWriter(f); BufferedWriter bw = new BufferedWriter(fw); bw.write("ナレッジエックス\r\n"); bw.write("デバッグのヒント教えます"); bw.close(); fw.close(); } catch(Exception e) { e.printStackTrace(); } } }
このプログラムは、実行するとsample.txtというテキストファイルに、文字列の書き出しを行います。一見すると何の問題もないプログラムのように見えますが、実はこのプログラムは、Windows環境とLinux環境で実行した場合に、異なる結果となってしまいます(図3と図4)。
同じコードなのに、どうして異なる結果になってしまったのでしょう。これは、Javaがファイルに文字列を出力する際の文字コードをどのように判定しているかがポイントです。先ほどのコードに、1行追加して再度実行してみます。
public static void main(String[] args) { try { System.out.println(System.getProperty("file.encoding")); //↑この行を追加 File f = new File("sample.txt"); (省略)
Windows上のEclipseで実行した場合、コンソールに「MS932」と表示されます(図5)。
次にLinux上で実行してみましょう。Linux上では「EUC-JP-LINUX」と表示され(図6、ファイルもEUCコードで出力されています。
WindowsとLinuxでは、ファイル入出力に使われるエンコーディング(Javaのシステムプロパティであるfile.encodingプロパティ)が異なるために、出力されたファイルの文字コードも異なるものになってしまいます。
このようにクラスの記述自体は同じであっても、環境によって挙動が異なるケースもいくつかあるということに注意してください。
ファイル出力で文字化け=環境の差異によってファイル出力のエンコーディングが異なることがあるので注意する
可読性やメンテナンス性の高いコードは、直接的にバグを撲滅するわけではありませんが、バグを未然に防ぐ意味では非常に重要です。ここでは可読性やメンテナンス性の高いコードを書くための注意点について、いくつかをご紹介します。
・同じ名前のフィールドとローカル変数を多用しない
フィールド(クラス変数、インスタンス変数)とローカル変数は、同じ名前の変数を定義することが可能ですが、可読性の面からはあまり好ましいことではありません。
次の例を見てください。
package kx; public class Tips4_6_1 { public String name = "ナレッジエックス"; public void output() { String name = "アイティメディア"; System.out.println(name); System.out.println(this.name); } }
Javaに習熟している方であれば、もちろんoutputメソッドを実行するとコンソールにどんな文字列が順に表示されるかはお分かりでしょう。しかし、一見するとnameという変数はフィールドとしても定義されていますし、outputメソッド内のローカル変数としても定義されていますので、不慣れな方は少し迷ってしまうかもしれません。それぞれの変数名が同じでないといけないような特別な理由があるのであれば別ですが、理由がないのであれば別の名称にして、むやみな混乱が生じないようにした方がよいでしょう。
・不必要にpublic定義しない
Javaには「可視性の修飾子」というものがあり、これを用いることによってフィールドやメソッドにアクセスできる範囲を限定することができます。特に理由がなければ、「public」を付けて定義することが多いのではないかと思いますが、この修飾子の使い方も、よく考慮する必要があるでしょう。何となく「public」定義しているフィールドやメソッドも、よく考えてみると自クラス内でしか使わないものだったり、継承先の子クラスでしか使わないものだったりするかもしれません。逆にそのようなフィールドやメソッドをpublic定義したままにしておくと、不必要にほかのクラスから呼ばれてしまい思わぬ不具合につながってしまうこともあります。
・ハードコーディングをしない
ハードコーディングとは、定数などの値そのものを、プログラム中に直接記述するようなコーディングのことを指します。次のような例がそれに当たります。
if (a.equals("01")) { b.execute(0); }
プログラムを作る本人にとっては値の意味がよく分かっているので、ついつい値をそのまま記述してしまいたくなりますが、後からメンテナンスすることを考えると、望ましいこととはいえません。
この例にあるa.equals("01")の「"01"」という値は、このプログラムにとっては意味のある値であると想像されますが、一見しただけでは何を意味する値かが分かりません。同様に、b.execute(0);の「0」も、何を意味するかが分かりません(このような値をマジックナンバーともいいます)。
一見して意味が分からないだけでも問題ですが、もっと深刻なのはプログラムに変更が加わり、この値のそのものが変更になってしまった場合です。この個所以外にも「"01"」や「0」という(プログラムにとって何らかの意味を持つであろう)値が記述されていると、それらすべてを漏れなく変更していかなければなりませんし(人間の作業には往々にしてミスが付きものです)、ほかにたまたま「"01"」という値を違う意味で用いていることがあると、変更すべきかそうでないかを、慎重に判断して変更しなければならなくなります。
このようなハードコーディングをなくすためには、意味を持った定数などは、1カ所にまとめて定義しておくことが効果的です。次の例は意味を持った定数を1つのクラスでまとめて定義したクラスです。このとき、定義する定数の名称を分かりやすいものにしておけば、その値が何を意味するものかを知ることも容易になり、プログラムの可読性をさらに向上させることができます。
なお、J2SE5.0からは、定数の表記のためのenumという新しい言語仕様が追加されましたので、J2SE5.0以上でプログラミングされる場合は、enumを利用することでより安全なプログラミングを行うことができます。
package kx; public class TipsCNS { public static final String STATUS_JUCHU? = "01"; public static final String STATUS_HACCHU = "02"; public static final int COMMAND_SHUKKA = 0; public static final int COMMAND_NYUKA = 1; }
if (a.equals(TipsCNS.STATUS_JUCHU)) { b.execute(TipsCNS.COMMAND_SHUKKA); }
さて、これまで数回にわたってお送りしてきた「デバッグのヒント教えます」ですが、いかがだったでしょうか。プログラマの心理として、誰しも最初から間違ったコードを書こうと思って書いている人はいないと思います。そう思って書いているコードの間違いは意外と見つけにくいもので、ある程度習熟している人でも、意外に簡単な間違いで長時間悩んでしまうものです。後から「どうしてこんなバグが見つけられなかったのか」と思ったりした経験があるのではないでしょうか。
この連載に登場した1つ1つのトピックスは、どれも簡単なものばかりであったことかと思います。実はバグの大半はそういった簡単なものが原因なのかもしれません。そんな簡単な落とし穴で忙しいエンジニアの皆さんの貴重な時間を無駄にしないために、この連載で紹介したパターンが少しでもお役に立てれば幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.