
やってはいけないJavaプログラミング
|
|
1.これだけはやってはいけない
製品としてプログラムを記述する場合に「決して」やってはいけないのは、ソフトウエアに対する要求仕様を満たさないこと、つまり製品にバグを残すことです(注1)。仕様としてなにがどこまで定義されているかは、それぞれのプロジェクトによって異なるでしょう。シビアな場面では、メソッドの応答速度や使用メモリ量を定義することもあります。そこまでは掘り下げずに、画面仕様書とファイル仕様書、データベース仕様書だけで、「ボタンAが押されると、ファイルBに記述された設定にしたがってユーザの入力値を演算し、データベースのこのテーブルにこういう行を挿入、画面のこの個所に○○というメッセージを表示する」といった条件のみを定義することもあります。
いずれの場合でも、仕様を最終的にブレークダウンした結果である個々のプログラム、ソースコードが、これを満たしていることが最低限の完成条件となります。「製品に要件を満たすことを阻害するようなバグを残してしまうこと」は、最低限避けねばなりません。そこでまず「絶対やるべきではないプログラミング」として、「バグを発生させやすいプログラミング」、「バグを見つけにくい、対処しにくいバグになりやすいプログラミング」という観点から述べてみます。
もちろん「機能要件を満たしてさえいればそれでいい」とは限らないことは、みなさんもご承知のことと思います。スケジュールどおりに出荷できなければ何の意味もないソフトウエアもありますし、保守や二次開発の容易さは、要件として明文化されることは少ないものの、ソフトウエアの価値を大きく左右します。
そこで、後半では「やらないほうがいい」プログラミングとして、「これでもリリースはできるけど……でも本当はよくないよね」というプログラムについて述べようと思います。
Javaへの誤解
●メモリリークについての誤解
バグを発生させる原因は多々あるでしょうが、それでは間違いなくバグになるというものが、言語および環境に関する理解不足です。特にJavaの場合C/C++系の言語を元に文法を策定したため、似て非なる点への理解不足などがおきやすいようです。Java は言語(注2)でもあり環境(VM)(注3)でもあるので、学習にあたってはその2点を切り分けて考えることも重要になってきます。 C系言語の経験者がJavaを始めて一番衝撃を受けるのは、間違いなくGC(ガベージコレクション)についてでしょう。Java言語にはC系言語のようなmalloc()/free()/delete(注4)はありません。プリミティブ型以外のすべてのオブジェクトは、new 演算子によって生成され、全ての到達可能な参照がなくなった場合(注5)に、VMが提供するGC機構によってオブジェクトの破棄とメモリの解放が行われます。このGCの仕組みによって、C系言語で一番対処の難しいメモリリークの発生する危険を最小限にしています。
しかし、Javaにおいてもメモリリークの危険は0になったわけではありません。「到達可能な参照がなくなった場合にGCされる」ということは、意図しない参照を残してしまえばメモリリークが発生するということでもあります。次の例を見てください。
リスト1
package example;
import java.util.List;
import java.util.LinkedList;
import java.io.File;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class MemoryLeakTest {
private List list = new LinkedList();
private int index;
public void load(File file) throws IOException {
index = list.size();
BufferedReader reader = new BufferedReader(new FileReader(file));
String buf = reader.readLine();
while(buf != null) {
list.add(buf);
buf = reader.readLine();
}
}
public int printNextLine(OutputStream os) throws IOException {
if(index >= list.size())
return -1;
String line = (String)list.get(index++);
int len = line.length();
os.write((index + ":").getBytes());
os.write(line.getBytes());
os.write('\n');
return len;
}
}
このクラスは、load()メソッドでテキストファイルの行をバッファし、printNextLine()メソッドで行数付の行内容を出力する、というものです。しかし、このクラスを複数のファイル出力に使いまわそうとすると、listオブジェクトがload時に初期化されていないためGCはlistに含まれる各行を表すStringを到達可能と判断し、メモリリークが発生します。これは単純な例ですが、GCがメモリリークに対して万能でないことのひとつの例証にはなります。Javaでプログラミングを行う際にも、メモリを確保したら必ず解放されなくてはなりません。malloc()/free()に比べてGCは暗黙的に実行されるため、普段は特に気にしないことが多いと思いますが、暗黙的であるがゆえに、余計に発見しにくいバグを生む危険性があることは理解しておかねばなりません。
●equals/==
次に、インスタンスの比較について考えてみましょう。
プログラム中でふたつのインスタンスが同じモノかどうかを判定したい場面というのは多くあると思います。CDレンタルショップのシステムで、出払っているCDを借りたいのでいつ返却されるか調べたいと言われたときのことを考えてみます。各メンバーが借り出し中のCDから返却予定日までの日数を検索するために、たとえば次のようなコードが現れるでしょう。
リスト2
public int getDaysUntilTitleReturn(CDTitle wantToRent) {
Disk renting = null;
Iterator = getRentingDisks();
int nearest = Integer.MAX_VALUE;
while(rentingDisks.hasNext()) {
renting = (Disk)rentingDisks.next();
if(renting.getTitle() == wantToRent) { // [1]
if(renting.getDaysUntilReturn() < nearest) {
nearest = renting.getDaysUntilReturn();
}
}
}
return nearest;
}
[1]でCDTitleクラスのインスタンスについて比較を行って、一致する場合には返却予定日までの日数を取得しています。しかしこのプログラムは期待通りには動作しません。[1]の判定式が正しくないためです。次のリストを見てください。
リスト3
package example;
public class Equals1 {
static public void main(String args[]) {
String s1 = "string";
String s2 = "str";
String s3 = "ing";
String s4 = s2 + s3;
System.err.println("s1("+s1+") == s4("+s4+"):\t" + (s1 == s4));
}
}
このプログラムでは、実行時に"string"となっているString のインスタンスを比較しています。もちろん結果はtrueになることを期待しますが、出力結果は以下のようになります。
リスト4
C:\K2\tmp>java -classpath . example.Equals1 s1(string) == s4(string): false
なぜかfalseになってしまっています。
この理由について、「プログラミング言語Java第3版」の「6.7.2 関係演算子と等価演算子」に、以下のように記述があります。「等価演算子"=="は、参照の同一性(identity)を検査しますが、オブジェクトの同値性(equivalance)は検査しません。2つの参照は、同じオブジェクトを参照していれば同一です。2つのオブジェクトが論理的に同じ値を持っている場合には、その2つのオブジェクトは同値です。」つまり、s1とs4は、論理的には同じ値"string"を持ってはいるのですが、同じオブジェクト(注6)ではないということです。
このような間違いを侵すきっかけになりやすい例を見てみます。
リスト5
package example;
public class Equals2 {
static final private String s1 = "string";
static final private String s2 = "str";
static final private String s3 = "ing";
static final private String s4 = s2 + s3;
static public void main(String args[]) {
System.err.println("s1("+s1+") == s4("+s4+"):\t" + (s1 == s4));
}
}
ほとんどリスト4と変わりませんが、文字列をローカル変数からリテラルに変更しました。この出力結果は以下のようになります。
リスト6
C:\K2\tmp >java ?classpath . example.Equals2 s1(string) == s4(string): true
今度は期待通りの出力となりました。こういうケースを見た人は同値性の比較に”==”を使えるものだと誤解することがあります。しかしこれは偶然そうなったと考えるべきです。s1とs2、s5は同じリテラルの”string”を表すとコンパイル時に決定できたため、コンパイラが”たまたま”最適化を行った結果だからです。コンパイラの最適化によって挙動が変わるようなプログラムは行うべきではありません。
では、オブジェクトの同値性比較を行うためにはどのようにすればよいかというと、Objectクラスで定義されているequals(Object)メソッドを利用します。
リスト7
package example;
public class Equals3 {
static public void main(String args[]) {
String s1 = "string";
String s2 = "str";
String s3 = "ing";
String s4 = s2 + s3;
System.err.println("s1("+s1+") equals s4("+s4+"):\t" + (s1.equals(s4)));
}
}
この例では"=="の替わりにequals(Object)メソッドを利用しています。Stringクラスがオーバーライドしているequals()メソッドで、オブジェクトの同一性だけでなく同値性、すなわち「引数が自身と同じ文字列の内容をもつStringクラスのオブジェクトであるかどうか」を比較しているからです。オブジェクト同士の同値性比較には常にequals()メソッドを利用します。
では、ユーザ定義クラスでは、どのように同値性を比較すればよいでしょうか。アプリケーションの要件や設計によって異なってきますが、一般的には、属性すべてが同値であった場合にそのオブジェクト同士は同値であると言えるでしょう。先ほどのリスト2であったCDTitleの同値性比較について言えば、たとえば次のようなコードが実装されるでしょう。
リスト8
class CDTitle {
Artist artist;
Label label;
List songs;
Date saleDay;
int price;
String name;
public boolean equals(Object o) {
if(o == null || !o.getClass().equals(CDTitle.class))
return false;
CDTitle t = (CDTitle)o;
return
artist.equals(t.artist) &&
label.equals(t.label) &&
songs.equals(t.songs) &&
saleDay.equals(t.saleDay) &&
price == t.price && name.equals(name);
}
}
ArtistクラスやLabelクラスではそれらの同値性を検証するコードが実装されているものと考えて、メンバー全てについて再帰的に同値性を比較しています。
アプリケーションの要件によって、たとえば発売日やレーベルや価格が異なっても、同じ音源からの同じ曲目で、同じアーティストの作品が別名で再発売されたときには同じCDのタイトルと見なす、ということもあるかもしれません。その場合には上記のコードからlabelやsaleDay、priceの同値比較は不要になるでしょう(音源の比較はsongs中に収められているSongクラスが担うことになる)。このように、オブジェクト同士の同値性比較を行いたい場合は、開発者自身が要件に合わせてequals()メソッドをオーバーライド(注7)することになります。
(注1)製品でなくともバグがあっては使い物にはなりませんが、製品でなければ「使い物にならなくてはならない」ということもありません。趣味のプログラムでも、もちろん「使い物になるようにしたい」場合は同じことですね。
(注2)Java言語仕様 James Gosling , Bill Joy , Guy Steel , Gilad Bracha (著), 村上 雅章 (訳) ピアソン・エデュケーション ISBN: 4894713063
(注3)Java仮想マシン仕様 Tim Lindholm , Frank Yellin (著), 村上 雅章 (訳) ピアソン・エデュケーション ISBN: 489471356X
(注4)malloc()はCにおけるメモリ確保関数。free()はmalloc()で確保したメモリを解放する関数。malloc()で確保されたメモリがfree()されないと、マシンのメモリリソースをどんどん食いつぶし、ついにはOut of memory が発生する。deleteはC++においてnewによって生成したオブジェクトを解放する演算子。C++では明示的にdeleteされなくてはならない。
(注5)java.lang.ref パッケージに用意されている各種の参照オブジェクトを用いた場合はその限りではありません。
(注6)ここでは、「オブジェクト」は「インスタンス」と同じ意味で考えています。「メモリ上に存在するクラスの実体」といったほどの意味です。
(注7)ちなみにObjectクラスで定義されているequals()メソッドは同一性の比較、すなわち「return (this == obj);」です。少なくとも同一であれば同値ではあるためです。また、equals()とあわせてhashCode()メソッドもオーバーライドすることが推奨されますが、ハッシュについての詳細は本稿では割愛します。














