[Java入門]3.3 ガソリンスタンドのクラス
クラスには様々な内容を記述しますが、その中でも特に重要なのは名前です。 名前はクラスを特定するために用いられます。 そこでまず最初に、ガソリンスタンドのクラスに名前を付けることにしましょう。
クラス名の長さには特に制限はありませんが、使える文字は決まっています。 最初の1文字は英字かアンダーバーか$記号、2文字目以降はそれに加えて数字を使うことができます。 英字の大文字と小文字は区別されますから注意してください。 単語ごとに頭文字を大文字にすると、ソースコードの中に現れるクラス名が見やすくなります。 また、名前はそのクラスが何なのかを表すのですから、わかりやすいものを付けるように心がけましょう。
ガソリンスタンドのクラス名は、英語をそのまま使ってGasStationということにします。 名前が決まったので、さっそくJava言語で記述してみましょう。
/* ガソリンスタンド */
class GasStation {
}
classというキーワードに続けてクラス名を書きます。 クラスがどんなものを表すかは中括弧(‘{‘と’}‘)の中に記述しますが、今はまだクラス名だけしか決まっていないので何もありません。
/*から*/間での部分はコメントです。 コメントはコンパイルのときに無視されるので、プログラムの動作には影響しません。 自由に書き入れることができますから、そのソースコードが何を表しているかを説明するために使ってください。 コメントは複数行にわたって書くこともできます。
コメントの最後には必ず*/をつける必要があるので、ごく短いコメントを書きたいときには少し面倒です。そんな場合にはコメントのもう一つの書き方を使いましょう。 //に続けてコメントを書き始めれば、その行の終わりまでがコメントになります。 これを使うとGasStationクラスの定義は次のようになります。
// ガソリンスタンド
class GasStation {
}
3.3.1 ガソリンスタンドにできること: メソッド
クラスの中身を記述するためには、そのクラスがどんなものを表すのかをきちんと決めなくてはいけません。 そこでもう一度、ガソリンスタンドについて考えてみましょう。
まず、ガソリンスタンドができることについてです。 「ガソリンスタンドが」といっていることに注意してください。 オブジェクト指向では、それぞれのオブジェクトを擬人化すると、オブジェクト同士の関係がわかりやすくなります。 最初は少し抵抗があるかもしれませんが、段々慣れてくるのでそういう風にとらえるんだな、と思っておいてください。
ガソリンスタンドは、車やバイクなどにガソリンを入れることができます。 そして商売ですから、代金を請求して受け取ります。 お釣りを出すこともありますが、これは代金をもらうことに含めておいていいでしょう。 もちろん受け取った代金は売上として計上する必要がありますから、これも忘れずに。 とりあえずこの位にしておきましょう。
できることが決まったので、それぞれに名前を付けます。 ここでは次のようにしてみました。
| メソッド名 | 内容 |
|---|---|
refuel |
ガソリンを入れる |
price |
代金を計算する |
addSales |
代金を領収して売上に計上する |
これらの名前にも、クラスメイト同じように好きなものをつけることができますが、頭文字を小文字にして、クラス名と見間違えないようにするのが普通です。 もちろん、複数の単語からなる名前が付けられますので、addSalesのように2つ目の単語からは頭文字を大文字にした方が読みやすくなるでしょう。
これで「できること」は決まりました。 しかし、実際に実行するためには具体的な手順が必要です。 ガソリンスタンドで働く人は、様々なことを覚えなければなりませんね。 例えば、ガソリンを入れる機械の操作方法やお客さんへの対応の仕方などです。 このようなことはマニュアルになっているでしょう。 その中の「ガソリンの入れ方」などの説明が手順です。 この手順のことをメソッドといいます。 先ほど決めたrefuelなどはメソッドの名前なのです。 そのクラスのオブジェクトに何ができるかはそのクラスにどんなメソッドを定義するかによって決まることになります。
メソッドの型
先程、料金を計算するメソッドを考えましたが、いくらになったのかをはっきりさせておかないと、受け取って売上に計上できません。 つまりこのメソッドは結局いくらになったのかを誰かに知らせる必要があります。 こんなときのために、メソッドは結果の値を返すことができるようになっています。 どんな値が返るかはメソッドによって異なりますから、これはメソッドごとに指定しなければなりません。 これを表すのが型です。 ひとつひとつのメソッドについて、型を決めるのです。 料金の場合は数値で表せますが、例えばそこの店長といった人を表すオブジェクトを返すこともあります。 また、その日が営業日かどうかといったYes/No (true/false, 真偽値あるいはbooleanと呼ばれます) などのような単純な値を表す基本型と、どんなオブジェクトかをあらわすための参照型があります。 文字列やオブジェクトは参照型の値です。 すべてのクラスは型として使うことができます。 基本型についてはこの先必要に応じて紹介していきます。 また、第7章に一覧がありますので詳しく知りたい方はそちらを参照してください。 なお、売上を計上するメソッドのように結果を返す必要のないものの場合は、値を返さないということを型で指定します。 この型のことをvoid型といいます。
3.3.2 ガソリンスタンドの情報: 変数
さて、ガソリンスタンドにできることは決まりましたので、今度はそれらを実行するためにどんな情報が必要になるかを考えてみましょう。
まず、ガソリンを入れるためには、どれだけ入るかがわからなくてはいけません。 しかし、これはお客さんによって違うので、ガソリンスタンド側の情報ではありません。
次にガソリン代の計算ですが、これは単価と入れた量がわかればよさそうです。 ガソリンの単価は時期や店によっても変わりますが、問題を簡単にするためにすべてのガソリンスタンドで同じ、ということにしましょう。
また、各ガソリンスタンドにはガソリンタンクがあります。 このタンクの容量と、残っているガソリンの量も情報として持つことにします。 ただし、タンクの容量は通常変化しません。 そしてまた、すべてのガソリンスタンドで同じということにしておきます。
最後に、その日の売上がどのくらいあったかを、各スタンドが情報として持つことにしましょう。 それぞれ名前を付けてまとめると次のようになります。
■ 各変数/メソッドには原書にはなかったアクセス制限: private/public を付加しました。
private な変数/メソッドはクラスの外部から参照/呼び出しができなくなります。
public な変数/メソッドはクラスの外からアクセスできます。 □
■ 原書では tankCapacity, unitPrice および totalSales に対するアクセス(参照/設定)メソッドを定義していません。 が、クラス内の変数がその外部から自由にアクセスできるのがどうにも居心地が悪かったので、変数はすべて private とし、必要に応じて参照/設定メソッドを定義しました。□
| 変数名 | 内容 |
|---|---|
unitPrice_ |
ガソリンの単価。全ガソリンスタンドで共通。日によって変化する。 |
remain_ |
タンクに残っているガソリンの量。各ガソリンスタンドで異なる。 |
tankCapacity_ |
タンクの容量。全ガソリンスタンドで共通。変化しない。 |
totalSales_ |
売上の合計。各ガソリンスタンドで異なる。 |
クラスが持つ情報は変数と呼ばれる、いわば箱のようなものに保持されます。 今、名前を付けたunitPrice_, remain_ などが変数です。 ただし、変数は値を保持するものなので、メソッドで使うことのできる値を返さない型, void型を指定することはできません。
3.3.3 GasStation クラス
必要なメソッドと変数が決まったので、これをクラスに記述してみましょう。 とはいえ、まだ各メソッドの具体的手順については考えていないので、メソッドの中身はコメントにしておきます。 なお、このソースは $chapter3/ex1/GasStation.java に収録されています。
■ メンバ変数名にはその末尾に ‘_‘ を付加しました。□
- Java: GasStation.java
-
/* ガソリンスタンドクラス */ class GasStation { private static final int tankCapacity_ = 1000; private static int unitPrice_; private int remain_; private int totalSales_; public int refuel(int amount) { // 給油する } public int price(int amount) { // 代金を計算する } public void addSales(int price) { // 売上を計上する } public static int tankCapacity() { // タンクの容量を返す } public int totalSales() { // 売上を返す } static public void setUnitPrice(int price) { // 単価を設定する } static public int unitPrice() { // 単価を返す } }
intとvoidが型を表しています。 intは整数です。 本当はガソリンの量は小数で表されるのでintではなくfloatまたはdoubleとするべきなのですが、説明を簡単にするために整数ということにしています。 voidは先ほど述べたように何も返さない型です。
■
- C++: GasStation.h
-
#ifndef GASSTATION_H__ #define GASSTATION_H__ /* ガソリンスタンドクラス */ class GasStation { private: static const int tankCapacity_; static int unitPrice_; int remain_; int totalSales_; public: int refuel(int amount); int price(int amount) const; void addSales(int price); static int tankCapacity(); int totalSales() const; static void setUnitPrice(int price); static int unitPrice(); }; #endif - C++: GasStation.cpp
-
#include "GasStation.h" const int GasStation::tankCapacity_ = 1000; int GasStation::unitPrice_; int GasStation::refuel(int amount) { // 給油する } int GasStation::price(int amount) const { // 代金を計算する } void GasStation::addSales(int price) { // 売上を計上する } int GasStation::tankCapacity() { // タンクの容量を返す } int GasStation::totalSales() const { // 売上を返す } void GasStation::setUnitPrice(int price) { // 単価を設定する } int GasStation::unitPrice() { // 単価を返す }
C#/VB ではtankCapacity, unitPrice および totalSales へのアクセスをプロパティで実装しました。
- C#: GasStation.cs
-
/* ガソリンスタンドクラス */ class GasStation { private static int tankCapacity_ = 1000; private static int unitPrice_; private int remain_; private int totalSales_; public int refuel(int amount) { // 給油する } public int price(int amount) { // 代金を計算する } public void addSales(int price) { // 売上を計上する } public static int tankCapacity { get { // タンクの容量を返す } } public int totalSales { get { // 売上を返す } } static public int unitPrice { set { // 単価を設定する } get { // 単価を返す } } } - VB: GasStation.vb
-
' ガソリンスタンドクラス Class GasStation Private Shared tankCapacity_ As Integer = 1000 Private Shared unitPrice_ As Integer Private remain_ As Integer Private totalSales_ As Integer Public Function refuel(ByVal amount As Integer) As Integer ' 給油する End Function Public Function price(ByVal amount As Integer) As Integer ' 代金を計算する End Function Public Sub addSales(ByVal price As Integer) ' 売上を計上する End Sub Public Shared ReadOnly Property tankCapacity() As Integer Get ' タンク容量を返す End Get End Property Public ReadOnly Property totalSales() As Integer Get ' 売上を返す End Get End Property Public Shared Property unitPrice() As Integer Set(ByVal Value As Integer) ' 単価を設定する End Set Get ' 単価を返す End Get End Property End Class
□
3.3.4 メソッドの中身を書こう
各メソッドでどんなことをすればいいのかがわかったので、中身を記述してみることにしましょう。 3つのメソッドについて順番に考えていきます。
まず、refuelメソッドです。 ガソリンを入れたらタンクの残量は入れた分だけ減らす必要があります。 これは変数reamin_の値を変更すればよさそうです。 このメソッドは実際に入れた量を返すことになっていました。 メソッドが値を返すためにはreturnを使います。 これらを記述するとrefuelメソッドは以下のようになります。 ただし、ここではタンクの残量を考慮していません。
public int refuel(int amount) {
remain_ = remain_ - amount;
return amount;
}
代金を計算するには実際に入れたガソリンの量に単価を掛けます。 したがって、priceメソッドは次のようになります。
public int price(int amount) {
return amount * unitPrice_;
}
最後に代金を売上に計上します。 単純に変数totalSales_に代金を足せばよさそうです。addSalesメソッドは次のようになります。
public void addSales(int price) {
totalSales_ = totalSales_ + price;
}
3.3.5 ガソリンスタンドを建てる: コンストラクタ
せっかくガソリンスタンドのクラスを作ったのですから、使うことを考えなければいけません。 使うというのは、ガソリンを入れたり、代金を受け取ったりしてみることです。 これは、ガソリンスタンドのシミュレーションと言ってもいいでしょう。
ガソリンスタンドをシミュレーションするためには、まずガソリンスタンドのインスタンスを用意する必要があります。 クラスは定義できましたが、インスタンスはどのように作ればよいのでしょうか。
現実の世界では、工事をしてガソリンスタンドを建てます。 建物などを建設することをコンストラクトと言いますが、Javaの世界でも同じようにこの言葉を使います。 コンストラクトという言葉には組み立てるという意味もありますが、人によってはコチラの方がイメージしやすいかも知れません。
ガソリンスタンドを建設するためには、どんなガソリンスタンドを作るのかといったことがわかっていなくてはなりません。 建物の内装工事をしたり、営業を開始する前にタンクにガソリンを用意したりもするでしょう。 どこかにその手順を示しているものが必要です。 これがコンストラクタです。 コンストラクタは一種のメソッドのようなもので、そのクラスのインスタンスを作るときに呼び出されます。
コンストラクタには次のような特長があります。
new演算子を使って呼び出す- 必ずクラスと同じ名前である
- 返す値の型は指定しない
- 引数を渡すことができる
我々のGasStationクラスにもコンストラクタを記述してみましょう。 コンストラクタであらかじめタンクにガソリンを入れておくことにします。 入れておくガソリンの量を引数として渡すことにすると、コンストラクタは次のようになります。
public GasStation(int initial) {
remain_ = initial;
}
3.3.6 GasStation クラスのソース
これでガソリンスタンドを表すGasStationクラスの定義が完成しました。 ここで、今までに出てきたソースをまとめておきます。 このソースは$chapter3/ex2/GasStation.javaに格納されています。
- Java: GasStation.java
-
/* ガソリンスタンドクラス */ class GasStation { // 変数定義 private static final int tankCapacity_ = 1000; private static int unitPrice_; private int remain_; private int totalSales_; // コンストラクタ定義 public GasStation(int initial) { remain_ = initial; } // メソッド定義 public int refuel(int amount) { remain_ = remain_ - amount; return amount; } public int price(int amount) { return amount * unitPrice_; } public void addSales(int price) { totalSales_ = totalSales_ + price; } public static int tankCapacity() { return tankCapacity_; } public int totalSales() { return totalSales_; } static public void setUnitPrice(int price) { unitPrice_ = price; } static public int unitPrice() { return unitPrice_; } }
■
- C++: GasStation.h
-
#ifndef GASSTATION_H__ #define GASSTATION_H__ /* ガソリンスタンドクラス */ class GasStation { private: static const int tankCapacity_; static int unitPrice_; int remain_; int totalSales_; public: explicit GasStation(int initial); int refuel(int amount); int price(int amount) const; void addSales(int price); static int tankCapacity(); int totalSales() const; static void setUnitPrice(int price); static int unitPrice(); }; #endif - C++: GasStation.cpp
-
#include "GasStation.h" const int GasStation::tankCapacity_ = 1000; int GasStation::unitPrice_; GasStation::GasStation(int initial) : remain_(initial) { } int GasStation::refuel(int amount) { remain_ = remain_ - amount; return amount; } int GasStation::price(int amount) const { return amount * unitPrice_; } void GasStation::addSales(int price) { totalSales_ = totalSales_ + price; } int GasStation::tankCapacity() { return tankCapacity_; } int GasStation::totalSales() const { return totalSales_; } void GasStation::setUnitPrice(int price) { unitPrice_ = price; } int GasStation::unitPrice() { return unitPrice_; } - C#: GasStation.cs
-
class GasStation { private static int tankCapacity_ = 1000; private static int unitPrice_; private int remain_; private int totalSales_; public int refuel(int amount) { remain_ = remain_ - amount; return remain_; } public int price(int amount) { return amount * unitPrice_; } public void addSales(int price) { totalSales_ = totalSales_ + price; } public static int tankCapacity { get { return tankCapacity_; } } public int totalSales { get { return totalSales_; } } static public int unitPrice { set { unitPrice_ = value; } get { return unitPrice_; } } } - VB: GasStation.vb
-
' ガソリンスタンドクラス Class GasStation Private Shared tankCapacity_ As Integer = 1000 Private Shared unitPrice_ As Integer Private remain_ As Integer Private totalSales_ As Integer Public Sub New(ByVal initial As Integer) remain_ = initial End Sub Public Function refuel(ByVal amount As Integer) As Integer remain_ = remain_ - amount Return amount End Function Public Function price(ByVal amount As Integer) As Integer Return amount * unitPrice_ End Function Public Sub addSales(ByVal price As Integer) totalSales_ = totalSales_ + price End Sub Public Shared ReadOnly Property tankCapacity() As Integer Get Return tankCapacity End Get End Property Public ReadOnly Property totalSales() As Integer Get Return totalSales_ End Get End Property Public Shared Property unitPrice() As Integer Set(ByVal Value As Integer) unitPrice_ = Value End Set Get Return unitPrice_ End Get End Property End Class
□
[Java入門]3.2 Java 言語はオブジェクト指向だ
オブジェクト指向というと、なんだか難しいなあ、と思う方もたくさんおられるでしょう。 しかし、実はオブジェクト指向の考え方はとても単純なのです。 オブジェクト指向の考え方を用いると私たちの暮らしている世界をより自然に表現することができます。 オブジェクトとは「物」という意味ですから、「オブジェクト指向」という言葉には、現実にある「物」を思い描きながらプログラミングができるという意味があります。 つまり、現実の世界とコンピュータの中の仮想的な世界との橋渡しをしてくれるとても有効な考え方なのです。
はたしてそれはどんなものなのでしょうか。 まずはJava言語の基本となる、オブジェクト指向の考え方を紹介しましょう。
3.2.1 オブジェクトってなんだろう
皆さんの部屋には電気製品がどのくらいありますか?
テレビ、ビデオ、カセットデッキ、CDプレーヤー、LDプレーヤー…。 きっとたくさんのものが並んでいるでしょう。 すべての機能を使いこなしているとは言えないまでも、それなりにちゃんと使えていますね。 時々、器械が苦手という人がいますが、大抵はビデオの録画予約ができない程度で、再生もできないという人は少ないと思います。 再生、PLAY、または’右向き三角マーク’の書かれたボタンを押すだけなのですから。
しかし、考えてみると、これらの器械は普通の人にはなかなか理解できない難しい技術で作られています。 たとえビデオデッキ本体のカバーを開けて中を見ても、大抵の人にとってはチンプンカンプンでしょう。 さらに、A社のビデオとB社のビデオでは中身が違っているはずです。 それでも、再生ボタンひとつで簡単にビデオを観ることができます。
また、例えばビデオとCDプレーヤーは使う目的も動く仕組みも異なっています。 それなのに、一台一台の取り扱い説明書を一所懸命読まなくても、混乱なく再生できるのはなぜでしょう。 きっと、すべての機械が再生ボタンを押せばいいようになっているからでしょう。 単純な操作だといっても、それぞれの機械で少しずつ違っていたり、再生ボタンの表示が違っていたらそうはいかなくなってしまいます。 物が違っても同じように再生ボタンがついているから、ボタンを押して「再生」と命令すれば、その機械が知っている、その機械なりの「再生」を実行するのです。 そしてそれが映像を映し出すことだったり、音楽を奏でることであったりするわけです。
ソフトウェアを作るときも、コレと同じことをしたいと思いませんか。 つまり、誰かが作ってくれたプログラムがあったとします。 すると、その中身は知らなくても、それが何をしてくれるものでどんな命令を理解できるかがわかれば利用できるはずです。 例えばビデオ、カセットデッキ、CDプレーヤーのように、よく似た機能を持っているソフトウェアは、同じ命令を聞いてくれるとすれば、プログラミングはずっと簡単になるでしょう。
さて、先ほど例にあげた電気製品はもちろん「物」です。 つまりオブジェクトであるわけです。 オブジェクトというのは、自分の動き方、つまり自分がどう機能すればよいのかということについての具体的な手続きを知っています。 また、そのために必要な情報も持っています。 例えば、ビデオが再生する情報はビデオテープに入っていますが、これはビデオデッキの中にあるわけです。 この2つ、手続きと情報によって物は機能を果たすことができます。 そして、オブジェクトとは、手続きと情報をひとつにしたものなのです。
では、この手続きと情報をどのようにプログラミングに取り入れることができるでしょうか。
3.2.2 クラスとインスタンス
いきなりですが、ガソリンスタンドときいてどんな場所を思い浮かべますか? できるだけたくさん挙げてみてください。
ガソリンを入れるところ、道をたずねるところ、飲み物を買ったりトイレを借りたりするところ…。 他にもたくさん思いつくかも知れません。 でも、少なくとも「ガソリンを入れるところ」というのは誰でも思い浮かぶでしょう。 これは単なる言葉遊びですが、全員が納得する答である「ガソリンを入れるところ」というのが「ガソリンスタンド」という言葉の定義といってもよさそうです。 つまりコレが一般に、ガソリンスタンドとはどんなところなのかを表しているのです。
ところで、この質問の答を考えるとき、皆さんはきっと、今までに行ったことのあるたくさんのガソリンスタンドを思い浮かべたでしょう。 そしてなるべく多くのガソリンスタンドに共通の事項を選び出したはずです。 今までに一度しか見たことのない特別なガソリンススタンド、例えば温泉付きのガソリンスタンドなどを挙げた人は少なかったのではないでしょうか。 本当にあるのかどうか疑われてしまいますからね。
しかし、現実にあるのなら、それも間違いなくガソリンスタンドですし、そんなに特殊なところでなくても本当は一軒一軒のガソリンスタンドは必ずどこかが違っています。 世の中にはたくさんのガソリンスタンドがあって、それぞれに値段も違えば広さやサービスも違っているのです。
このように、同じ種類のものすべてに共通な性質や情報と、個々に違いを持った実体に分けるというとらえ方は、多くのものに当てはめることができます。 オブジェクト指向はこの考え方に従って様々なものを自然にソフトウェアの世界に取り込んでいきます。 そして、共通部分を記述したものをクラス、それぞれに違いを持った個々の実体をインスタンスといいます。 とある街はずれにガソリンスタンドがあるとすると、それがガソリンスタンドというクラスのインスタンスになるわけです。
さて、ガソリンスタンドについてどんなに良く定義できたとしても、現実のガソリンスタンドがなければ役にたちません。 つまり、ガソリンスタンドを作る必要があるのです。 ガソリンスタンドがどんなものであるかはクラスに書いてあるので、その内容に従えばガソリンスタンドを作ることができるはずです。 そして、ガソリンの値段などの、各ガソリンスタンドそれぞれに違う部分を決めます。 このように、現実のガソリンスタンド、つまりインスタンスを作成することを、クラスからインスタンスを生成する(インスタンシエーション) といいます。 そして、実際にプログラムの中でオブジェクトとして使用するのは、実体であるインスタンスです。
どうでしょう? オブジェクト指向の考え方が、なんとなくわかってきたでしょうか? Java言語に限らず、オブジェクト指向といわれる言語やシステムはすべて上記のような考え方を基本思想にしています。 しかし、その性格はそれぞれ大きく異なっています。 それらの中で、Java言語は非常にバランスのとれた、実用的なオブジェクト指向言語であるといえます。
オブジェクト指向言語のプログラミングはクラスを作ることだといっても過言ではありません。 Java言語でも、クラスを作ることがすなわちプログラミングをすることになります。 クラスを作るときには、クラスのもつ情報や機能をそのプログラミング言語で記述する必要があります。 これを、クラスを定義すると言います。
さあ、それではいよいよプログラミングです。 この節で取り上げたガソリンスタンドを例に、実際にクラスを作ってみましょう。
[Java入門]3.1 Javaプログラミングの手順
Java言語でのプログラミング作業は3つの段階に分けることができます。 作業の流れと各段階で使うコマンドは図3.1のようになります。
実際にこの流れを試してみましょう。
| プログラミング作業 | 使用するコマンド |
|---|---|
| ソースファイルを編集 | テキストエディタ (vi, メモ帳 など) |
| コンパイル | コンパイラ (javac) |
| 実行 | インタプリタ (java) |
- C++ のコンパイラは様々です。Visual C++ならcl, Borland C++ならbcc32, GNU GCCならg++ など。
- C#, VBのコンパイラはそれぞれ csc, vbc ですが、通常Visual Studio .net環境下でプログラミングを行なう際には気にしませんね。
- C++/C#/VB はコンパイル(+リンク)が完了するとそのまま実行可能なロードモジュールが生成されます。なのでインタプリタは必要としません。
C#/VB(.net)の場合、実はインタプリタによる実行に近いのですが、.net frameworkがOSに組み込まれることによってインタプリタがOSに内蔵されるので、インタプリタを明示的に起動する必要はありません。
3.1.1 ソースファイルの編集
まず、プログラムを書いたソースファイルを作成する必要があります。
テキストエディタ(UNIXではviやEmacs, Windowsではメモ帳や秀丸エディタなど)を用いて次の内容を記述したァイルを作成してください。
これは画面にメッセージを表示するプログラムです。
- Java : HelloWorld.java
-
class HelloWorld { static public void main(String[] args) { System.out.println("Hello World!"); } }
ソースプログラムの中の class の後ろにある語、このばあいでは HelloWorld がこのクラスの名前(クラス名)です。
ソースファイルの名前はクラス名に「.java」を付けたものにするのが普通です。
このプログラムでは HelloWorld ですから HelloWorld.java としましょう。
■
- C++ : HelloWorld.cpp
-
#include <iostream> int main() { std::cout << "Hello World!" << std::endl; return 0; } - C# : HelloWorld.cs
-
class HelloWorld { public static void Main(string[] args) { System.Console.WriteLine("Hello World!"); } } - VB : HelloWorld.vb
-
Class HelloWorld Public Shared Sub Main() System.Console.WriteLine("Hello World!") End Sub End Class
□
3.1.2 コンパイル
ソースファイルができたら、そのプログラムをコンピュータが実行できるように、コンパイルします。
ソースファイルのコンパイルは javac コマンドで行ないます。
Solaris 2.X の場合はシェルのコマンドラインから次の下線部を入力します。
Windows 95/NTではいわゆるDOS窓(95では「MS-DOSプロンプト」、NTでは「コマンドプロンプト」)から、同じように入力します。
なお、本書では、コマンドラインのプロンプトはSolaris 2.Xの標準的な環境に合わせて「%」としています。
Windowsの場合は「C:\」というようなプロンプトに続いて下線部を入力してください。
% javac HelloWorld.java
コンパイルや実行を行なうには、$PATHや$CLASSPATHなどの環境変数をあらかじめ正しく設定しておく必要があります。
具体的な設定方法については「【付録】JDKの展開をディレクトリ構成について」を参照してください。
■
- Visual C++ :
-
C:\ cl -GX HelloWorld.cpp - Borland C++ :
-
C:\ bcc32 HelloWorld.cpp - GNU GCC :
-
% g++ -o HelloWorld HelloWorld.cpp - C# :
-
C:\ cscDDD HelloWorld.cs - VB :
-
C:\ vbc HelloWorld.cpp
ソースファイルに何か間違いがあって正常にコンパイルできなかったときには、コンパイラがエラーメッセージを出力します。
例えばclassと記述するべきところをcrassとしてしまった場合、次のようなエラーメッセージが表示されます。
もう一度エディタを使って修正してください。
HelloWorld.java:1: 'class' または 'interface' がありません。
crass HelloWorld {
^
エラー 1 個
ソースファイルに間違いがなく、正常にコンパイルが終了したときには、コンパイラはメッセージを出力しません。
そしてHelloWorld.classというファイルが生成されます。
このファイルのことを「クラスファイル」と呼びます。
この場合、HelloWorldクラスのクラスファイルということになります。
3.1.3 実行
ソースファイルをコンパイルして作成されたクラスファイルはインタープリタを使って実行することができます。
インタープリタはjavaコマンドです。
HelloWorldクラスを実行するには次のようにします。
% java HelloWorld (下線部を入力する)
すると画面に「Hello World!」と表示されます。
以上がJava言語でプログラミングし、ソフトウェアを作る作業の流れです。
実際には間違いの修正などで追加や変更を行うことが多いので、これらの作業を繰り返すことになります。
[Java入門]第3章 Java 言語でのプログラミング入門
本書の読者の多くはアプレットを作成することを目的としていることでしょう。
しかし、この章ではアプレットのことは忘れて、簡単なプログラムを作りながらJava言語でプログラミングを行なうために必要となる基本的な知識を解説します。
プログラミングが初めてという方から、他の言語でプログラミングをしているという方まで、多くの皆さんに役立つように例を交えて進めていきます。
実際のソースコードを示しますのでぜひ試してみてください。
この章では、まずプログラミング作業の流れを示し、その後オブジェクト指向とJava言語の基礎知識、プログラミングの考え方を例を通じて説明します。
最初はアプレットではなく、単独のプログラムを作ってみましょう。
アプレットの作成については第4章で説明します。
もちろん、アプレットを作成する際にも本章で説明している知識が必要となりますので、一読しておいてください。
また、文法や言語仕様についてはこの章では特に説明しません。 第7章を参照してください。
この章で説明するプログラムは添付CD-ROMに入っています。 Windows 95/NT版はsample¥win32¥chapter3.zip を展開するとchapter3 というディレクトリが作られ、そこにファイルが展開されます。
展開方法については「【付録】添付CD-ROMの使い方」を御覧ください。 また、Solaris 2.x版はsample/solaris2/chapter3に、そのまま読める形式で格納しています。
この章で$chapter3/ファイル名と表現した場合、それぞれのchapter3というディレクトリの下のファイルを表しています。
また文章中では紙面の都合上、リストのコメントを省略している場合もあります。
その場合文章中で解説をしていますので、そちらを参照してください。
State Map Compiler
状態遷移表
FSM(Finite State Machine:有限状態機械)は、アプリケーションの分析・設計において重要なパートのひとつです。非常に多くのアプリケーションが、その動作をFSMによって記述することができます。
FSMは”State Map:状態表”あるいは”State Transition Table:状態遷移表”と呼ばれる表の形で表現されます。
“(門番のいる)ゲート”を例に、FSMを作ってみましょう。以下のようなState Mapを用意しました。
| 現在の状態 | イベント | 次の状態 | アクション |
|---|---|---|---|
| Locked | Coin | Unlocked | Unlock |
| Pass | Locked | Alarm | |
| Unlocked | Coin | Unlocked | ThankYou |
| Pass | Locked | Lock |
このState Mapはゲートの制御モデルを表現しています。ゲートは2つの状態Locked(閉じている)とUnlocked(開いている)をもち、2つのイベントCoinとPassを受け付けます。
- Coinイベント
- 門番が入場料を受け取った
- Passイベント
- 誰かがゲートを通過した
また、ゲートには4つのアクションが定義されています。
- Unlock
- ゲートを開ける
- Lock
- ゲートを閉じる
- Alarm
- 警報を鳴らす
- ThankYou
- 余分なお金をもらったことに礼を言う
このState Mapの読み方は以下のとおりです:
- ゲートがLocked状態のとき:
- Coinイベントが発生したら、Unlocked状態に遷移してUnlockアクションを起こす。
- Passイベントが発生したら、閉じているゲートを無理矢理誰かが通過したことに対しAlarmアクションを起こす
- ゲートがUnlocked状態のとき:
- Passイベントが発生したら、Locked状態に戻してLockアクションを起こす
- Coinイベントが発生したら、余分なお金を頂いた事に礼を言う(ThankYouアクションを起こす)。
さて、このState Mapに表された動作を行なうプログラムをどのように書きましょうか。
いちばん単純なのは二重のswitch文です。つまり:
enum { Locked, Unlocked } state;
enum { Coin, Pass } event;
state s = Locked; // 初期状態
event e;
while ( true ) { // 無限ループ
e = 発生したイベント;
switch ( s ) {
case Locked :
switch ( e ) {
case Coin :
s = Unlocked;
Unlock();
break;
case Pass :
s = Locked;
Alarm();
break;
}
break;
case Unocked :
switch ( e ) {
case Coin :
s = Unlocked;
ThankYou();
break;
case Pass :
s = Locked;
Lock();
break;
}
break;
}
}
実に素直なコードです。この例では状態とイベントが2つづつだからまだマシですが、これが状態10コ/イベント10コに増えたとしたら500 行を越える長大なswitch文になってしまいます。書くのも大変読むのも大変デバッグするのはもっと大変、とてもやってられません。
二次元配列を使えばよりエレガントになります:
enum { Locked, Unlocked } state;
enum { Coin, Pass } event;
state Locked_Coin() {
Unlock();
return Unlocked;
}
state Locked_Pass() {
Alarm();
return Locked;
}
state Unlocked_Coin() {
ThankYou();
return Unlocked;
}
state Unlocked_Pass() {
Lock();
return Locked;
}
...
state s = Locked; // 初期状態
event e;
typedef state (*transition)();
transition table[2][2] = {
Locked_Coin, Locked_Pass,
Unlocked_Coun, Unlocked_Pass
};
while ( true ) {
e = 発生したイベント;
s = (*table[s][e])();
}
状態とイベントの組それぞれに対し関数を用意し、その中でアクションの実行と次の状態の設定を行ないます。二重のswitch文よりはずっと奇麗ですが、状態数xイベント数と同数の関数定義が必要です。もっとエレガントなコードは書けないものでしょうか…
State Map Compiler
僕の愛用するクラスライブラリのひとつに Rogue Wave社のTools.h++ Professionalというのがあります。このクラスライブラリはFTP/HTTP/SMTP/POP3のクライアントをいとも簡単に作れるクラス群を提供しています。readmeドキュメントの中に、「プロトコルの実装にはSMC(State Map Compiler)を使いました。SMCはhttp://www.oma.com/Offerings/catalog.htmlから無料で入手できます。」とありました。
SMCによるFSMの実装手順を説明しましょう。まず、状態遷移表に現われるアクションを定義するクラス(“Context:コンテキスト”と呼んでいます)を作ります。
// GateContext.h
#ifndef __GATECONTEXT_H__
#define __GATECONTEXT_H__
class GateContext {
public:
void Lock();
void Unlock();
void Alarm();
void ThankYou();
void FSMError(const char*, const char*);
};
#endif
// GateContext.cpp
#include "GateContext.h"
#include <iostream>
using namespace std;
void GateContext::Lock() { cout << "ゲートを閉じます" << endl; }
void GateContext::Unlock() { cout << "ゲートを開けます" << endl; }
void GateContext::Alarm() { cout << "タダで入っちゃいけませんよ!" << endl; }
void GateContext::ThankYou() { cout << "これはどうも...有り難く頂戴します" << endl; }
void GateContext::FSMError(const char* event, const char* state) {
cout << state << " 状態で "
<< event << " が発生するはずないのに!"
<< endl;
}
次に、状態遷移表を表現するStateMapファイルを作ります:
// GateSFM.sm
fsmname GateFSM
context GateContext
header GateContext.h
initial Locked
{
Locked {
Coin Unlocked Unlock
Pass Locked Alarm
}
Unlocked {
Coin Unlocked ThankYou
Pass Locked Lock
}
}
このファイルGateFSM.smをSMCに食わせます:
smc < GateFSM.sm
するとSMCはgateFSM.hとgateFSM.ccを出力します。簡単なテストドライバをこさえましょう:
// gate.cpp
#include "gateFSM.h"
#include
using namespace std;
int main() {
GateFSM fsm;
for ( bool cont = true; cont; ) {
char input[32];
cout << "現在の状態は : " << fsm.GetState().StateName() << endl;
cout << "Coin or Pass (c/p) ? " << flush;
cin >> input;
switch ( *input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
return 0;
}
これで完了です。gate.cpp, gateFSM.cc, GateContext.cppをコンパイル/リンクすれば実行形式ができあがります。実行結果を以下に示します。
現在の状態は : Locked Coin or Pass (c/p) ? c ゲートを開けます 現在の状態は : Unlocked Coin or Pass (c/p) ? p ゲートを閉じます 現在の状態は : Locked Coin or Pass (c/p) ? p タダで入っちゃいけませんよ! 現在の状態は : Locked Coin or Pass (c/p) ? c ゲートを開けます 現在の状態は : Unlocked Coin or Pass (c/p) ? c これはどうも...有り難く頂戴します 現在の状態は : Unlocked Coin or Pass (c/p) ? p ゲートを閉じます 現在の状態は : Locked Coin or Pass (c/p) ? /
GateFSMは受理できるイベントと同名のメソッドを持っており、発生したイベントに対応するメソッドのコールを繰り返すだけです。アクションを実装し、状態遷移表を書くだけでFSMによるアプリケーションができちゃうです。
FSMのからくり
以下に示した、SMCが吐いたコードを追いかけてからくりをあばいてみましょう。
// gateFSM.h
#ifndef _H_GateFSM
#define _H_GateFSM
#include <stddef.h>
#include "GateContext.h"
class GateFSM;
class GateFSMState {
public:
virtual const char* StateName() const = 0;
virtual void Pass(GateFSM& s);
virtual void Coin(GateFSM& s);
};
class GateFSMUnlockedState : public GateFSMState {
public:
virtual const char* StateName() const
{return("Unlocked");};
virtual void Pass(GateFSM&);
virtual void Coin(GateFSM&);
};
class GateFSMLockedState : public GateFSMState {
public:
virtual const char* StateName() const
{return("Locked");};
virtual void Pass(GateFSM&);
virtual void Coin(GateFSM&);
};
class GateFSM : public GateContext {
public:
static GateFSMUnlockedState UnlockedState;
static GateFSMLockedState LockedState;
GateFSM();// default constructor
void Pass() {itsState->Pass(*this);}
void Coin() {itsState->Coin(*this);}
void SetState(GateFSMState& theState) {itsState=&theState;}
GateFSMState& GetState() const {return *itsState;};
private:
GateFSMState* itsState;
};
#endif
#include "gateFSM.h"
static char _versID[] = "No Version.";
GateFSMUnlockedState GateFSM::UnlockedState;
GateFSMLockedState GateFSM::LockedState;
void GateFSMState::Pass(GateFSM& s)
{s.FSMError("Pass", s.GetState().StateName());}
void GateFSMState::Coin(GateFSM& s)
{s.FSMError("Coin", s.GetState().StateName());}
void GateFSMUnlockedState::Pass(GateFSM& s) {
s.SetState(GateFSM::LockedState);
s.Lock();
}
void GateFSMUnlockedState::Coin(GateFSM& s) {
s.SetState(GateFSM::UnlockedState);
s.ThankYou();
}
void GateFSMLockedState::Pass(GateFSM& s) {
s.SetState(GateFSM::LockedState);
s.Alarm();
}
void GateFSMLockedState::Coin(GateFSM& s) {
s.SetState(GateFSM::UnlockedState);
s.Unlock();
}
GateFSM::GateFSM() : itsState(&LockedState) {}
GateFSMは全アクションが定義されたコンテキストGateContextから導出されています。このことにより、GateFSMは必要とするアクションをすべて手に入れた(コールできる)ことになります。privateメンバitsStateの型GateFSMStateは仮想関数 Coin()とPass()を持っていて、GateFSMのメソッドCoin()/Pass()はそのままitsStateの同名のメソッドCoin() /Pass()をコールします。
さらにGateFSMは、定義されている状態に対応する2つのクラスGateFSMLockedState,GateFSMUnlockedStateをstaticメンバに持ち、そのいずれもGateFSMStateの派生クラスです。
現在の状態がLockedである、すなわちitsState == &GateFSM::GateFSMLockedStateであるGateFSMのインスタンスに対しイベントCoinを発生させる(GateFSM::Coin()をコールする)と、
void GateFSM::Coin() {
itsState->Coin(*this);
}
と定義されているので、GetFSMLockedState::Coin()がコールされます。GetFSMLockedState::Coin()にはLocked状態でCoinイベントが発生したときの処理が定義されています。
void GateFSMLockedState::Coin(GateFSM& s) {
s.SetState(GateFSM::UnlockedState);
s.Unlock();
}
状態遷移表GateFSM.smに記述したとおり、Unlocked状態に遷移した後にUnlockアクションをコールしています。他の状態/イベントの組み合わせに対しても同様です。
SMCはこれらのクラスおよび各メソッドを状態遷移表から生成してくれるわけです。
SMCの文法
ではもう少し大きなFSMを作りながら、SMCの文法を説明します。C++のソースコードからコメントを除去するアプリケーション”stripper”を作りましょう。
C++でのコメントは、
- “//”から始まり、改行コードまで。
- “/*”から始まり、”*/”まで
です。ソースコードから1文字づつ読み出し、FSMに食わせることでコメントを除去します。
この状態遷移図と等価な状態遷移表を以下に示します。
| 現在の状態 | イベント | 次の状態 | アクション | |
|---|---|---|---|---|
| outside | Slash | startingSlash | ||
| Star | outside | PutChar | ||
| EOL | outside | PutChar | ||
| Other | outside | PutChar | ||
| ]startigSlash | Slash | secondSlash | ||
| Star | starAfterSlash | |||
| EOL | outside | PutSlash PutChar | ||
| Other | outside | PutSlash PutChar | ||
| secondSlash | Slash | secondSlash | ||
| Star | secondSlash | |||
| EOL | outside | PutChar | ||
| Other | secondSlash | |||
| starAfterSlash | Slash | starAfterSlash | ||
| Star | startingStar | |||
| EOL | starAfterSlash | * | ||
| Other | starAfterSlash | * | ||
| startingStar | Slash | outside | ||
| Star | starAfterSlash | |||
| EOL | starAfterSlash | * | ||
| Other | starAfterSlash | * |
これを基にSMCに食わすスクリプトファイルStripFSM.smを作ります。
FSMName StripFSM
Context StripperContext
Header stContext.h
Initial outside
Version stripFSM.sm version 1.0
{
outside {
Slash startingSlash {}
Star outside PutChar
EOL outside PutChar
Other outside PutChar
}
startingSlash {
Slash secondSlash {}
Star starAfterSlash {}
EOL outside { PutSlash PutChar }
Other outside { PutSlash PutChar }
}
secondSlash {
EOL outside PutChar
Other secondSlash {}
Star secondSlash {}
Slash secondSlash {}
}
(inStarComment) {
Other starAfterSlash {}
EOL starAfterSlash {}
}
starAfterSlash : inStarComment {
Star startingStar {}
Slash starAfterSlash {}
}
startingStar : inStarComment {
Slash outside {}
Star startingStar {}
}
}
スクリプトはヘッダ部と状態遷移部に分かれていて、
ヘッダ部
{
状態遷移部
}
の形式で記述します。
- ヘッダ部
-
FSMName FSM名
生成するFSMの名前を指定します。ここで与えた名前がクラス名となり、最初の1文字を小文字に置き換え、拡張子”.cc”,”.h”を付けたコードが出力されます。
- Context コンテキスト名
アクションを定義したコンテキストを指定します。FSMはこのコンテキストから導出されます。 - Header ヘッダ名
FSMヘッダで#includeするファイル(通常コンテキストが宣言されたヘッダ)を指定します。 - Initial 初期状態
FSMのコンストラクタで、ここに指定した初期状態をセットします。これを指定しなかったときは、SetStateメソッドで明示的に初期状態を設定しなければなりません。 - Version バージョン
任意の文字列をバージョンIDとして指定できます。
-
- 状態遷移部
状態遷移部は、状態 イベント 次の状態 アクション
を並べたものです。ひとつの状態に対しイベント/次の状態/アクションをまとめて記述する、
状態 { イベント1 次の状態1 アクション1 イベント2 次の状態2 アクション2 ... }の書式が許されています。
状態の遷移のみで、実行すべきアクションがないときは、
状態 イベント 次の状態 {}としてください。また、複数のアクションを実行したいときは、
状態 イベント 次の状態 { アクション1 アクション2 }のように{}で囲んでください。
また、いくつかの状態で、イベント/次の状態/アクションが完全に一致するとき、それらを一つにまとめることができます。たとえば上記の状態遷移表での*の付けられた行は一致していますから、共通の状態遷移を()で囲み、
(inStarComment) { Other starAfterSlash {} EOL starAfterSlash {} }そしてそれを継承するような形式でそれぞれの差分を記述することができます。
starAfterSlash : inStarComment { Star startingStar {} Slash starAfterSlash {} } startingStar : inStarComment { Slash outside {} Star startingStar {} }
コンテキストおよびメインルーチンを以下に示します。
// stContext.h
#ifndef _H_stripperContext
#define _H_stripperContext
#include <iostream.h>
class StripperContext {
private:
char itsChar;
istream *itsIStream;
ostream *itsOStream;
public:
StripperContext() { }
void SetStreams(istream& i, ostream& o) {
itsIStream = &i;
itsOStream = &o;
}
void FSMError(const char* t, const char* s) {
cerr << "Transition error: " << t
<< " in state " << s << endl;
}
int ReadChar() {
int c;
c = itsIStream->get();
itsChar = c;
return c;
}
void PutChar() { *itsOStream << itsChar; }
void PutSlash() { *itsOStream << '/'; }
};
#endif
// stripper.cpp
#include <iostream.h>
#include <stdlib.h>
#include "stripFSM.h"
void main() {
StripFSM myStripper;
myStripper.SetStreams(cin,cout);
while ( cin )
switch( myStripper.ReadChar() ) {
case EOF : exit(0); break;
case '/' : myStripper.Slash(); break;
case '*' : myStripper.Star(); break;
case '\n': myStripper.EOL(); break;
default : myStripper.Other(); break;
}
}
SMC(State Map Compiler) の拡張
はじめに
C Magazine 1999/9 で、SMC(State Map Compiler)を紹介しました。 SMCに状態遷移表を記述したスクリプト(テキストファイル)を食わせると、状態遷移表に書かれた通りに動作するFSM(Finite State Machine:有限状態機械) クラスを生成してくれます。
僕はこのSMCがいたく気に入り、更なる機能拡張を試みました。![]()
スクリプト拡張
まず、SMCに食わすスクリプトの文法を少しばかり拡張しました。
- namespace/package指定
SMCが吐くFSMおよびFSMが参照するコンテキストをnamespaceで囲む機能を追加しました。
スクリプトのヘッダ部に、
namespace <FSM-名前空間> package <FSM-package>
と書いておけば、FSMを指定したnamespaceに置くことができます。
package指定は後述する”Javaコード生成”時の package名を指定します。
同様にコンテキストに対しては、
contextnamespace <Context-名前空間> contextpackage <Context-package>
と記述します。
- 状態が遷移しないイベント
スクリプトの状態遷移部には、
状態 { イベント 次の状態 { アクション } ... }という形式で状態遷移を記述します。状態の遷移を伴わない場合、すなわち’次の状態’が’状態’に等しいときは、
状態 { イベント ----- { アクション } ... }のように、ひとつ以上の’-'を書くことができます。
- 遷移/アクションが一致するイベント
複数のイベントに対し、まったく同じ遷移/アクションを行なうとき、たとえば、
状態 { イベント-1 次の状態 { アクション } イベント-2 次の状態 { アクション } イベント-3 次の状態 { アクション } ... }のような記述は、
状態 { イベント-1 次の状態 { アクション } イベント-2 = イベント-1 イベント-3 = イベント-1 ... }と書くことで、イベント-2/イベント-3に対する遷移/アクションはイベント-1と同じであることを表現できます。
Javaコード生成
SMCにJavaコードを生成させたのが今回の拡張の目玉です。
fsmname GateFSM
package jp.gr.java_conf.episteme.fsm
namespace fsm
context GateContext
contextpackage jp.gr.java_conf.episteme.ct
contextnamespace ct
initial Locked
{
Locked {
Coin Unlocked Unlock
Pass Locked Alarm
}
Unlocked {
Coin Unlocked ThankYou
Pass Locked Lock
}
}
というスクリプトGateFSM.smをSMCに食わせます:
smc -j GateFSM.sm ... -j : Javaコード生成
すると、smcは以下のようなJavaソース’GateFSM.java’を生成します。
/* produced by SMC-Deluxe */
package jp.gr.java_conf.episteme.fsm;
class GateFSMState {
public String name() { return "(GateFSMState)"; }
public void Pass(GateFSM s) { s.getContext().FSMError("Pass", s.getState().name()); }
public void Coin(GateFSM s) { s.getContext().FSMError("Coin", s.getState().name()); }
};
class GateFSMUnlockedState extends GateFSMState {
// 省略...
};
class GateFSMLockedState extends GateFSMState {
// 省略...
};
public class GateFSM extends jp.gr.java_conf.episteme.ct.GateContext {
public GateFSM() { itsState = LockedState; }
public void Pass() {itsState.Pass(this);}
public void Coin() {itsState.Coin(this);}
public void setState(GateFSMState theState) {itsState=theState;}
public GateFSMState getState() {return itsState;};
public String getStateName() {return itsState.name();};
// 省略...
}
GateFSMのベースクラスjp.gr.java_conf.episteme.ct.GateContextとテストルーチンは以下のようになります。
/*
* GateContext.java
*/
package jp.gr.java_conf.episteme.ct;
public class GateContext {
public void FSMError(String event, String state)
{ System.out.println(state + " 状態で " +
event + " が発生するはずないのに!");
}
public void Lock()
{ System.out.println("ゲートを閉じます"); }
public void ThankYou()
{ System.out.println("これはどうも...有り難く頂戴します"); }
public void Alarm()
{ System.out.println("タダで入っちゃいけませんよ!"); }
public void Unlock()
{ System.out.println("ゲートを開けます"); }
}
/*
* gate.java
*/
import jp.gr.java_conf.episteme.fsm.*;
public class gate {
public static void main(String[] arg) throws Exception {
GateFSM fsm = new GateFSM();
for ( boolean cont = true; cont; ) {
System.out.println("現在の状態は " + fsm.getStateName());
System.out.print("Coin or Pass (c/p) ? ");
int input = System.in.read();
System.in.skip(System.in.available());
switch ( input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
}
}
FSMとコンテキストの分離
FSMはコンテキストの派生クラスというオブジェクトモデルではなく、 FSMとコンテキストを独立させるオプション’-s’を追加しました。
smc -s GateFSM.sm ... C++ smc -j -s GateFSM.sm ... Java
によって生成されたコードGateFSM.hおよびGateFSM.javaは以下のようになります。
/*
* GateFSM.h
*/
#include "GateContext.h"
namespace fsm {
...
class GateFSM {
...
private:
GateFSMState* itsState;
ct::GateContext* itsContext;
public:
void setContext(ct::GateContext& context) { itsContext = &context; }
ct::GateContext& getContext() { return *itsContext; }
...
};
}
#endif
/*
* GateFSM.java
*/
package jp.gr.java_conf.episteme.fsm;
...
public class GateFSM {
...
public void setContext(jp.gr.java_conf.episteme.ct.GateContext context)
{ itsContext = context; }
public jp.gr.java_conf.episteme.ct.GateContext getContext()
{ return itsContext; }
}
このように、FSMにメソッドsetContext()が追加されます。
FSMのコンストラクトの後、setContext()によって、コンテキストを設定してください。
実行途中でsetContext()することで、コンテキストをダイナミックに入れ換えることも可能です。
コンテキスト生成
オプション’-c’で、コンテキストにひな型(C++では”コンテキスト名.h” , Javaでは “コンテキスト名.java”)を生成します。
/*
* GateContext.h
*/
#ifndef _H_GateContext
#define _H_GateContext
namespace ct {
class GateContext {
public:
void FSMError(const char*, const char*);
void Lock();
void ThankYou();
void Alarm();
void Unlock();
};
}
#endif
/*
* GateContext.java
*/
package jp.gr.java_conf.episteme.ct;
public class GateContext {
public void FSMError(String event, String state) {
// code here
}
public void Lock() {
// code here
}
public void ThankYou() {
// code here
}
public void Alarm() {
// code here
}
public void Unlock() {
// code here
}
}
抽象コンテキスト
‘-a’オプションでコンテキストが抽象クラスとなります。
ただし、このオプションは’-s’と併用しなければなりません。
C++では全メソッドを純粋仮想関数とします。
/*
* GateContext.h
* (smc -c -a -s GateFSM.sm)
*/
#ifndef _H_GateContext
#define _H_GateContext
namespace ct {
class GateContext {
public:
virtual void FSMError(const char*, const char*) =0;
virtual void Lock() =0;
virtual void ThankYou() =0;
virtual void Alarm() =0;
virtual void Unlock() =0;
};
}
#endif
Javaではinterfaceを定義します。
/*
* GateContext.java
* (smc -j -c -a -s GateFSM.sm)
*/
package jp.gr.java_conf.episteme.ct;
public interface GateContext {
void FSMError(String event, String state);
void Lock();
void ThankYou();
void Alarm();
void Unlock();
}
抽象コンテキストの場合、C++/Javaそれぞれのテストルーチンは以下のようになります。
/*
* gate.cpp
*/
#include "GateContext.h"
#include "GateFSM.h"
#include <iostream>
using namespace std;
class TheGateContext : public ct::GateContext {
public:
virtual void FSMError(const char*, const char*);
virtual void Lock();
virtual void ThankYou();
virtual void Alarm();
virtual void Unlock();
};
void TheGateContext::Lock()
{ cout << "ゲートを閉じます" << endl; }
void TheGateContext::Unlock()
{ cout << "ゲートを開けます" << endl; }
void TheGateContext::Alarm()
{ cout << "タダで入っちゃいけませんよ!" << endl; }
void TheGateContext::ThankYou()
{ cout << "これはどうも...有り難く頂戴します" << endl; }
void TheGateContext::FSMError(const char* event, const char* state) {
cout << state << " 状態で "
<< event << " が発生するはずないのに!"
<< endl;
}
int main() {
TheGateContext context;
fsm::GateFSM fsm;
fsm.setContext(context);
for ( bool cont = true; cont; ) {
char input[32];
cout << "現在の状態は : " << fsm.getState().name() << endl;
cout << "Coin or Pass (c/p) ? " << flush;
cin >> input;
switch ( *input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
return 0;
}
/*
* gate.java
*/
import jp.gr.java_conf.episteme.fsm.*;
import jp.gr.java_conf.episteme.ct.*;
class TheGateContext implements GateContext {
public void FSMError(String event, String state) {
System.out.println(state + " 状態で " +
event + " が発生するはずないのに!");
}
public void Lock()
{ System.out.println("ゲートを閉じます"); }
public void ThankYou()
{ System.out.println("これはどうも...有り難く頂戴します"); }
public void Alarm()
{ System.out.println("タダで入っちゃいけませんよ!"); }
public void Unlock()
{ System.out.println("ゲートを開けます"); }
}
public class gate {
public static void main(String[] arg) throws Exception {
GateFSM fsm = new GateFSM();
GateContext context = new TheGateContext();
fsm.setContext(context);
for ( boolean cont = true; cont; ) {
System.out.println("現在の状態は " + fsm.getStateName());
System.out.print("Coin or Pass (c/p) ? ");
int input = System.in.read();
System.in.skip(System.in.available());
switch ( input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
}
}
時空を越えるオブジェクト
時空を越えるオブジェクト
シリアライズってなに?
シリアライズ(あるいはストリーミング)とは、オブジェクトの状態をバイト列に変換すること、そしてバイト列から元のオブジェクトを復元することです。バイト列に変換できればそれをファイルに保存することで、一旦終了したアプリケーションが再度起動したときに前回の状態に戻すことができますし、ネットワーク上で共有すれば複数のアプリケーションが同じオブジェクトを利用できます。また、変換されたバイト列をソケットやRS232Cに流し込めば遠く離れたマシン上で復元することもできるでしょう。簡単に言えばオブジェクトをファイルに書き込むこと、そしてファイルから読み込むことです。ソケットやRS232Cもバイト列の転送媒体と言う観点からは広義のファイルと考えていいでしょうからね。
Step-0:簡単なシリアライズ
シリアライズなんて御大層な用語ですけど、要するにファイルに対するsaveとloadです。そう難しくかんがえることはありません。
class Foo {
int n_;
public:
explicit Foo(int n =0) : n_(n) {}
void set(int n) { n_ = n; }
void printOn(std::ostream& strm) cons {
strm << "Foo:" << n_;
}
};
なんてなクラスにsave/loadメソッドを追加して、シリアライズを実現しましょう。
class Foo {
int n_;
public:
explicit Foo(int n =0) : n_(n) {}
void set(int n) { n_ = n; }
void printOn(std::ostream& strm) const
{ strm << "Foo:" << n_ };
void save(FILE*) const;
void load(FILE*);
};
void Foo::save(FILE* fp) const {
fwrite(&n_,sizeof(n_),1,fp);
}
void Foo::load(FILE* fp) {
fread(&n_,sizeof(n_),1,fp);
}
/* お試し */
int main() {
FILE* fp;
// save
Foo x(5);
x.printOn(cout);
fp = fopen("foo.dat","wb");
x.save(fp);
fclose(fp);
// load
Foo y;
fp = fopen("foo.dat","rb");
y.load(fp);
fclose(fp);
y.printOn(cout);
return 0;
}
どうということはありませんな。
Step-1:メディアの抽象化
上の例ではファイルストリーム(FILE*)に対するsave/loadを実装しました。同様にfstreamやソケット、RS232Cなどなどに対するsave/loadを追加すれば様々なメディアにシリアライズできますが、各メディア毎にメソッドが2つづつ追加されるのもカッコよくありません。メディアの違いを吸収するクラスSaverとLoaderを作りましょう。
namespace ser {
class Loader {
public:
virtual void read(void*,size_t) =0;
void read(bool& v) { read(&v,sizeof(v)); }
void read(char& v) { read(&v,sizeof(v)); }
void read(unsigned char& v) { read(&v,sizeof(v)); }
void read(short& v) { read(&v,sizeof(v)); }
void read(unsigned short& v) { read(&v,sizeof(v)); }
void read(int& v) { read(&v,sizeof(v)); }
void read(unsigned int& v) { read(&v,sizeof(v)); }
void read(long& v) { read(&v,sizeof(v)); }
void read(unsigned long& v) { read(&v,sizeof(v)); }
void read(float& v) { read(&v,sizeof(v)); }
void read(double& v) { read(&v,sizeof(v)); }
};
class Saver {
public:
virtual void write(const void*,size_t) =0;
void write(bool v) { write(&v,sizeof(v)); }
void write(char v) { write(&v,sizeof(v)); }
void write(unsigned char v) { write(&v,sizeof(v)); }
void write(short v) { write(&v,sizeof(v)); }
void write(unsigned short v) { write(&v,sizeof(v)); }
void write(int v) { write(&v,sizeof(v)); }
void write(unsigned int v) { write(&v,sizeof(v)); }
void write(long v) { write(&v,sizeof(v)); }
void write(unsigned long v) { write(&v,sizeof(v)); }
void write(float v) { write(&v,sizeof(v)); }
void write(double v) { write(&v,sizeof(v)); }
};
}
FILE*に対するシリアライズにはSaver/Loaderから導出したFileSaver/FileLoaderを作り、純粋仮想関数write(const void*,size_t)/read(void*,size_t)を再定義します。
namespace ser {
class FileLoader : public Loader {
FILE* fp_;
public:
explicit FileLoader(FILE* fp) : fp_(fp) {}
virtual void read(void*,size_t);
};
void FileLoader::read(void* v, size_t n) {
fread(v,n,1,fp_);
}
class FileSaver : public Saver {
FILE* fp_;
public:
explicit FileSaver(FILE* fp) : fp_(fp) {}
virtual void write(const void*,size_t);
};
void FileSaver::write(const void* v, size_t n) {
fwrite(v,n,1,fp_);
}
}
iostreamに対しても同様に、
namespace ser {
class StreamLoader : public Loader {
std::istream& s_;
public:
explicit StreamLoader(std::istream& s) : s_(s) {}
virtual void read(void*,size_t);
};
void StreamLoader::read(void* v, size_t n) {
s_.read(static_cast<char*>(v), n);
}
class StreamSaver : public Saver {
std::ostream& s_;
public:
explicit StreamSaver(std::ostream& s) : s_(s) {}
virtual void write(const void*,size_t);
};
void StreamSaver::write(const void* v, size_t n) {
s_.write(static_cast<const char*>(v), n);
}
}
これに伴い、Fooを書き換えます。
class Foo {
int n_;
public:
explicit Foo(int n =0) : n_(n) {}
void set(int n) { n_ = n; }
void printOn(std::ostream& strm) const
{ strm << "Foo:" << n_; }
void save(ser::Saver&) const;
void load(ser::Loader&);
};
void Foo::save(ser::Saver& s) const {
s.write(n_);
}
void Foo::load(ser::Loader& l) {
l.read(n_);
}
/* お試し */
int main() {
FILE* fp;
// save
Foo x(5);
x.printOn(cout);
fp = fopen("foo.dat","wb");
ser::FileSaver s(fp);
x.save(s);
fclose(fp);
// load
Foo y;
fp = fopen("foo.dat","rb");
ser::FileLoader l(fp);
y.load(l);
fclose(fp);
y.printOn(cout);
return 0;
}
さて、これでメンバ変数がchar,intなどの単純な型で構成されているクラスであれば正しくシリアライズできるでしょうよ。
では、メンバ変数にポインタを含む場合はどうでしょう。
class Link {
int n_;
Link* next_;
public:
explicit Link(int n =0, Link* next =0) : n_(n), next_(next) {}
‾Link() { delete next_; }
void save(ser::Saver&) const;
void load(ser::Loader&);
};
このオブジェクトをsave/loadするにはどうすればいいでしょうか。
void Link::save(ser::Saver& s) const {
s.write(n_);
next_->save(s);
}
void Link::load(ser::Loader& l) {
l.read(n_);
delete next_;
next_ = new Link;
next_->load(l);
}
…残念でした。このコードではおそらくまともには動かないでしょうよ。next_が0であるときの考慮がなされてませんからね。正しくは、ポインタが0であるか否かのフラグを書き込んでおき、load時にチェックしないとね。
void Link::save(ser::Saver& s) const {
s.write(n_);
bool flag = (next_ != 0);
s.write(flag);
if ( flag ) next_->save(s);
}
void Link::load(ser::Loader& l) {
l.read(n_);
delete next_;
next_ = 0;
bool flag;
l.read(flag);
if ( flag ) {
next_ = new Link;
next_->load(l);
}
}
Step-2:Polymorphicなシリアライズ
ポインタをシリアライズするときの問題はまだあります。たとえばさきほどのLinkからLink2を導出します。
class Link2 : public Link {
public:
explicit Link2(int n =0, Link* next =0) : Link(n,next) {}
...
};
Linkのメンバ変数Link* next_にはLinkおよびその派生クラスのポインタが代入できますから、
Link lnk(1,new Link2(2));
何てことやっても構いません。
lnkをsaveし、loadすると正しく復元されるでしょうか?
ま、ダメでしょうね。Link::loadの中でnewできるのはLinkだけですからね。
これを正しくシリアライズするには、書き込んだオブジェクトが何であるかを表すIDをsave時に打ち込み、load時には読み込んだIDに対応するオブジェクトを生成しなければなりません。
そのために、まずシリアライズ可能なオブジェクトのベースクラスを用意します。
namespace ser {
class Saver;
class Loader;
class Object {
public:
virtual ‾Object() {}
virtual long id() const =0;
virtual void save(Saver&) const =0;
virtual void load(Loader&) =0;
};
}
シリアライズしたいすべてのクラスはこのser::Objectから導出し、仮想関数id()がクラス毎に0でないユニークな値を返すよう再定義します(0はnullポインタを表すために用います)。
class Foo : public ser::Object {
public:
virtual long id() const { return 1; }
...
};
class Bar : public ser::Object {
public:
virtual long id() const { return 2; }
...
};
Saverには新たなメソッドwriteObjectを追加します。
namespace ser {
class Saver {
public:
void writeObject(const Object*);
...
};
void Saver::writeObject(const Object* obj) {
long id = obj ? obj->id() : 0;
write(id);
if ( id ) obj->save(*this);
}
}
次に、IDに対応するオブジェクトを生成するためのクラスFactoryをこさえます。
namespace ser {
class Factory {
typedef Object* (*create_fun)();
typedef std::map<long,create_fun> omap;
typedef std::auto_ptr<Factory> instance_ptr;
friend instance_ptr;
static instance_ptr instance_;
omap map_;
Factory() {}
‾Factory() {}
public:
static Factory* instance();
void regist(long,create_fun); // 登録
Object* create(long); // 生成
};
Factory* Factory::instance() {
if ( !instance_.get() )
instance_ = instance_ptr(new Factory);
return instance_.get();
}
void Factory::regist(long id, create_fun fn) {
map_[id] = fn;
}
Object* Factory::create(long id) {
omap::iterator it = map_.find(id);
return ( it == map_.end() ) ? 0 : it->second();
}
std::auto_ptr<Factory> Factory::instance_;
}
Loaderに追加するメソッドreadObjectはFactoryの助けを借りてオブジェクトの生成と読み込みを行ないます。
namespace ser {
class Loader {
public:
Object* readObject();
...
};
Object* Loader::readObject() {
Object* obj = 0;
long id;
read(id);
if ( id ) {
obj = Factory::instance()->create(id);
obj->load(*this);
}
return obj;
}
}
アプリケーションはシリアライズに先立ってFactoryへのオブジェクトの登録を行なっておかなければなりません。
ser::Object* foo_() { return new Foo; }
ser::Object* bar_() { return new Bar; }
void init() {
ser::Factory* factory = ser::Factory::instance();
factory->regist(1, &foo_);
factory->regist(2, &bar_);
}
int main() {
init();
...
return 0;
}
Foo/Barのシリアライズは以下のようなコードで実現できます。
init(); // 少なくともloadの前にはコールしておくこと!
// save
Foo* foo = new Foo(1);
Bar* bar = new Bar(2);
ofstream strm("objcts.dat",ios_base::binary);
ser::StreamSaver s(strm);
s.writeObject(foo);
s.writeObject(bar);
// load
ifstream strm("objects.dat",ios_base::binary);
ser::StreamLoader l(strm);
Foo* foo = static_cast<Foo*>(l.readObject());
Bar* bar = static_cast<Bar*>(l.readObject());
Step-3:共有オブジェクトのシリアライズ
シリアライズはこれでカンペキ?
いや、もうひとつ、ややこしい問題が残っています。
class Human : public ser::Object {
char* name_; // 名前
Human* spouse_; // 配偶者
public:
explicit Human(const char*);
virtual‾ Human() { delete[] name_; }
void marry(Human* h) { spouse_ = h; }
const char* name() const { return name_; }
void printOn(std::ostream&) const;
virtual long id() const;
void save(ser::Saver&) const;
void load(ser::Loader&);
};
Human::Human(const char* n) : spouse_(0) {
name_ = new char[strlen(n)+1];
strcpy(name_,n);
}
long Human::id() const {
return 3;
}
void Human::printOn(std::ostream& strm) const {
strm << "名前:" << name()
<< " 配偶者:" << ( spouse_ ? spouse_->name() : "なし");
}
void Human::save(ser::Saver& s) const {
long len = strlen(name())+1;
s.write(len);
s.write(name(),len);
s.writeObject(spouse_);
}
void Human::load(ser::Loader& l) {
long len;
l.read(len);
delete[] name_;
name_ = new char[len];
l.read(name_,len);
delete spouse_;
spouse_ = static_cast<Human*>(l.readObject());
}
クラスHumanは名前、そして配偶者へのポインタを持っています。
ofstream strm("serialize.dat",ios_base::binary);
ser::StreamSaver s(strm);
Human* he = new Human("アダム");
Human* she = new Human("イヴ");
he->marry(she);
she->marry(he);
s.writeObject(he);
s.writeObject(she);
delete he;
delete she;
"アダム"と"イヴ"はめでたく夫婦になりました。市役所の役人は"アダム"と"イヴ"をシリアライズします。
"アダム"が書き込まれるとき、彼の配偶者"イヴ"を書き込みます。
すると"イヴ"の書き込みの中で、彼女の配偶者"アダム"を書き込みます。
はたまた"アダム"の書き込みの中で、彼の配偶者"イヴ"を書き込みます。
…そう、無限ループに陥るのです。
この問題を回避するには、Loader::writeObject/Saver::readObjectにもうひとヒネリしなければなりません。一度書き込まれたオブジェクトは二度と書き込まないからくりが必要です。
Saver::writeObjectでは、オブジェクトを書き込んだときそれが通算何番目に書き込まれたかを示すシリアル番号を記録しておきます。そして二度目以降の書き込みでは記録をたどってシリアル番号だけを書き込みます。
namespace ser {
class Saver {
typedef std::map<Object*,long> omap;
omap map_;
long cnt_;
public:
Saver() : cnt_(0) {}
void writeObject(const Object*);
...
};
void Saver::writeObject(const Object* obj) {
if ( obj ) {
omap::iterator it = map_.find(const_cast<Object*>(obj));
if ( it != map_.end() ) {
write(it->second);
} else {
write(-obj->id());
map_[const_cast<Object*>(obj)] = ++cnt_;
obj->save(*this);
}
} else {
long id = 0;
write(id);
}
}
}
Loader::readObjectでは、オブジェクトを読み込んだときそれが通算何番目に読み込んだかを記録しておきます。そしてシリアル番号を読み込んだら記録をたどってポインタ値を検索します。
namespace ser {
class Loader {
typedef std::vector<Object*> ovec;
ovec vec_;
long cnt_;
public:
Loader();
Object* readObject();
...
};
Loader::Loader() {
vec_.push_back(0);
}
Object* Loader::readObject() {
Object* obj = 0;
long id;
read(id);
if ( id < 0 ) {
obj = Factory::instance()->create(-id);
vec_.push_back(obj);
obj->load(*this);
} else
if ( id > 0 ) {
obj = vec_[id];
}
return obj;
}
}
Java入門 : C++/C#/VB port
Java入門 ── 最新オブジェクト指向プログラミングの深き味わい──
翔泳社 ISBN4-88135-351-9この本が発行されたのは1996年2月、Javaはまだそれほどメージャーになっていなかった頃です。
日本でのJavaブームの火付け役となった一冊といえるでしょう。
当時Web-pageを飾るAppletの解説に終始する参考書が多い中、Javaによるオブジェクト指向プログラミングの肝を押さえた内容には素晴らしいものがあります。
とりわけ第3章はオブジェクト指向の三本柱といわれる抽象データ型・継承・他態のそれぞれについて、ガソリンスタンドのシミュレーションを題材に極めて平易かつ明快に解説しています。著者の一人である佐藤治氏の快諾を頂き、Java入門:第3章をWeb公開します。 公開にあたり、原書で示された当時のJava
JDKβ対応コードをJava 2
SDK対応に改めるとともに、C++/C#/VisualBasic.netで実装したコードも用意しました。各言語によるコードはsrc.zip
に納めてあります。επιστημηによる加筆箇所は■と□で囲んであります。
-
第3章
Java言語でのプログラミング入門
はじめてのDBTools.h++ (part-1)
RDBとDBTools.h++
僕は今まで、いわゆるRDB(Rerational DataBase)を使ったアプリケーションを書いたことがありません。好き嫌いはともかく、RDBはソフトウェア世界で最も広く利用されているでしょうし、ソフト屋たるもの、RDBを知らないというわけにもいかないでしょう。
RDBを使ったアプリケーションを、僕の得意とするC++で書くために選んだのがRogue Wave社のDBTools.h++というクラスライブラリです。 DBTools.h++はSybase,Oracle,MS SQL ServerといったRDBエンジンをC++から利用するために作られたクラスライブラリです。
DBTools.h++をインストールし、User's Guideが用意してくれたチュートリアルに取り組んだまではよかったのですが、そこに紹介されているサンプルコードがあまりにキレイすぎて、なんだかよくわからなかったのです。というのもこれらサンプルコードではDBTools.h++によるRDBへのアクセス部がいろんなクラスの中に巧妙に埋め込まれていたため、ソースコードを上から下へ追っかけていけばDBTools.h++の基本的な使い方がわかるというようなシロモノではなかったのです。
そこで僕はUser's Guideが用意してくれたサンプルコードを一旦バラバラに分解し、上から下に追っかければDBTools.h++の大まかな機能と使い方が理解できるようなサンプルを新たに起こすことから始めることにしました。
RDBって何なんだ?
RDBは簡単に言えば複数の「表」に対する様々な操作を行なうソフトウェアです。ここで「表」というのは、まぁぶっちゃけていえば文字列や数をメンバとする構造体の巨大な集合と考えればいいでしょう。たとえばとっても簡単な電話番号簿を作りたい、としましょうか。
struct Person {
char name[50]; // 名前
char phone[50]; // 電話番号
};
vector<Person> person;
データベースを使わずに、すべてをon-memoryで処理するつもりなら、上のような構造体(Person)とその集合(person)、およびpersonに対する操作(挿入/削除/変更/検索)を用意することになるでしょう。
personに納めることのできるデータの量はそのアプリケーションに許されるメモリの大きさで決まります。個人が使う電話番号簿ならこれでも十分かもしれません。しかし数百、数千あるいは数万のデータを処理するとなるとディスクの助けを借りないととても歯が立ちません。巨大なデータの塊を処理するのがデータベースなんですね。
プログラマにとってみればディスク上のファイルをシークし、読み出し、書き込むといったディスクアクセスルーチンをちまちまと積み重ねるよりは、データの巨大な集合体があたかもメモリ上に置かれているかのように扱える方が楽に決まっています。
ですからほとんどのRDBは標準となったSQL(System Query Language)でデータベースを操作することで複雑極まりない実際のディスクアクセスを包み隠しています。
しかしながらSQLとオブジェクト指向プログラミングとの間のギャップが少なからず存在するのも事実です。
DBTools.h++はSQLとC++とのギャップをエレガントに埋めてくれるライブラリだと、僕は思っています。
データベースとの接続
ファイル上にある巨大なデータの集合はデータベースソフトウェアの管理下にあり、アプリケーションとは別の空間に存在します。アプリケーションがデータの集合にアクセスするには、まずデータベースに接続しなければなりません。
DBTools.h++でデータベースに接続するには、
RWDBDatabase db =
RWDBManager::database("ODBC", "TRIAL", "ALLADIN", "abracadabra", "", "");
RWDBManager::database()には6つの引数を与えます。それぞれサーバタイプ、サーバ名、ユーザ名、パスワード、データベース名、ロールです。この例では、ODBCを使ってデータベース'TRIAL'に接続しています。
DBTools.h++のライブラリは、接続するデータベースに依存しないコア・ライブラリと、データベース毎に異なるアクセス・ライブラリの2つから構成されています。DBTools.h++を使ったアプリケーションを作るときは、コア・ライブラリと、接続するデータベースに応じたアクセス・ライブラリ、そしてTools.h++ライブラリをリンクしてください。
最初の引数であるサーバタイプは接続するデータベースによって異なります。この引数に文字列として何を与えればよいかは、それぞれのアクセス・ライブラリのマニュアルを参照してください。
ちなみに、DBTools.h++がサポートするデータベースすなわちアクセス・ライブラリの種類は以下のとおりです。
- Oracle
- Sybase DB
- Sybase CT
- MS SQL Server
- ODBC
- DB2
データベースを操作するとき、DBTools.h++はその操作に応じたSQLコマンドを生成し、データベースに発行します。
その様子をモニタしたいときはデータベースへの接続が完了した時点で
RWDBTracer& tracer = db.tracer(); tracer.setOn(RWDBTracer::SQL); tracer.stream(cout);
すれば、発行されたSQLを見ることができます。
また、データベースの操作中にエラーが発生したとき、プロトタイプ
void function(const RWDBStatus&)
である関数を呼び出すことができます。
RWDBStatusのメソッドraise()を呼べば例外を送出してくれますから、
void onError(const RWDBStatus& aStatus) {
aStatus.raise();
}
int main() {
RWDBManager::setErrorHandler(onError);
try {
// DBTools.h++を使ったデータベース操作
} catch ( RWExternalErr& er) {
cout << er.why() << endl;
return 1;
}
return 0;
}
のようなコードを書いておくと良いでしょう。
テーブルの作成と定義
データベースに接続できたところで、データを格納するテーブルを作成しましょう。デーブル名を'PERSON'とします。
RWDBTable person = db.table("PERSON");
データベース上にテーブル"PERSON"が既に存在していたときは、一旦削除してしまいます。
if ( person.exists(true) ) {
person.drop();
}
次に、テーブルに登録するレコードがどんな要素から構成されるのか、つまり構造体でいえばメンバの型と名前を定義します。
struct Person {
char name[50]; // 名前
char phone[50]; // 電話番号
};
のテーブル上での表現を定義するわけです。
RWDBColumn name(person["NAME"]); RWDBColumn phone(person["PHONE"]);
テーブル上のひとつのレコードを構成するカラム(項目)としてnameとphoneを用意し、それぞれに名前"NAME","PHONE"を与えました。そしてname,phoneのテーブル上での型やサイズを決めてあげます。
name.type(RWDBValue::String).storageLength(50).nullAllowed(false); phone.type(RWDBValue::String).storageLength(50).nullAllowed(false);
name,phoneともに文字列、サイズ50、必ず入力しなければならない必須項目とします。
この2つのカラムを持つテーブルを作りましょう。
RWDBSchema schema; schema.appendColumn(name); schema.appendColumn(phone); db.createTable(person.name(), schema);
レコードの追加
それでは試しに、レコードをいくつか追加してみましょう。
struct {
const char* name;
const char* phone;
} entries[] = {
{ "police", "110" },
{ "fire", "119" },
{ "weather", "177" },
{ "time", "117" },
{ "ambulance", "119" },
{ "episteme", "045-XXX-XXXX" },
{ "episteme", "060-XXX-XXXX" },
{ "friend", "060-XXX-XXXX" },
{ "s34", "06-XXXX-XXXX" },
{ 0,0 } // end marker
};
RWDBInserter inserter = person.inserter();
for ( int i = 0; entries[i].name; ++i ) {
inserter << entries[i].name << entries[i].phone;
inserter.execute();
}
テーブルにレコードを追加するには、テーブルからinserterを取得し、inserterに項目を<<した後にexecute()します。
レコードの読み出し
正しく追加されたかどうか、テーブル内の全レコードを読み出してみます。
RWDBReader reader;
RWCString nameVal;
RWCString phoneVal;
reader = person.reader();
while ( reader() ) {
reader >> nameVal >> phoneVal;
cout << setw(20) << nameVal
<< setw(14) << phoneVal
<< endl;
}
レコードの読み出しは、テーブルからreaderを取得し、operator()によって次のレコードに移動して>>で項目を取り出します。
実行結果を示します:
police 110
weather 177
time 117
ambulance 119
episteme 045-XXX-XXXX
episteme 060-XXX-XXXX
friend 060-XXX-XXXX
s34 06-XXXX-XXXX
fire 119
レコードの削除
今度は名前が"friend"であるレコードを削除します。
RWDBDeleter deleter ; deleter = person.deleter(); deleter.where(name == "friend"); deleter.execute();
テーブルからdeleterを取得します。
次にwhere()によって削除するレコードが満たす条件を与えます。ここではname=="friend"を与えています。
そしてdeleter.execute()すれば、where()に与えた条件を満たすすべてのレコードが削除されます。
同様に電話番号が"06"から始まるレコードを削除するには:
deleter = person.deleter();
deleter.where(phone.like("06%"));
deleter.execute();
メソッドlikeに与えた文字列中にある'%'はワイルドカード、すなわち任意の文字列を表します。
police 110
weather 177
time 117
ambulance 119
episteme 045-XXX-XXXX
fire 119
レコードの更新
名前が"weather"であるレコードを"forecast"に変更してみましょう。
RWDBUpdater updater;
updater = person.updater();
updater << name.assign("forecast");
updater.where(name == "weather");
updater.execute();
まずテーブルからupdaterを取得します。
次に << で変更するカラムと値を設定します。
ここで変更を要求しなかったカラムは既存の値のままとなります。そしてwhere()で更新の対象となるレコードの条件を与え、execute()します。
police 110
forecast 177
time 117
ambulance 119
episteme 045-XXX-XXXX
fire 119
レコードの検索
RDBでは特定の条件を満たすレコードを検索する処理が最も頻繁に行なわれることでしょう。
レコードの件策にはselectorが用いられます。たとえば電話番号が"11"から始まるレコードを見つけるには、
RWDBSelector selector;
selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
selector.execute();
reader = selector.reder()
データベースからselectorを取得します。テーブルからではないことに注意してください。なぜなら、複数のテーブルに対して検索条件を指定することがあるからです。
得られたselectorに対し <<
によって検索条件に適合するレコードのカラム群を与えます。ここではテーブルを丸ごと与えることでテーブルないの全カラムを取り出すことにします。
次に、where()によって検索条件を指定し、execute()で検索を実行します。
検索が完了したらserectorからreaderを取得します。あとはreaderから検索結果を読み出します。
police 110
time 117
ambulance 119
fire 119
where()には、各条件を && や || で繋ぐことで、複雑な検索条件で検索できます。
電話番号が"119"、または名前に's'が含まれるレコードを検索するには、
selector = db.selector();
selector << person;
selector.where(phone == "119" || name.like("%s%"));
selector.execute();
forecast 177
ambulance 119
episteme 045-XXX-XXXX
fire 119
selectorには検索条件だけでなく、結果のソート条件も設定できます。
selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
selector.orderBy(phone);
selector.orderBy(name);
selector.execute();
reader = selector.reder()
この例では、検索結果を電話番号、名前の順にソートしています。
police 110
time 117
ambulance 119
fire 119
検索結果の一括取得
RWTPtrMemTableを使えば、selectorによる検索結果をメモリ上のコンテナに一気に読み込むことができます。
まず、レコードの各カラムを格納するクラスRecordを作り、RWDBReaderからの読み込みを行なうoperator>>()を定義しておきます。
struct Record {
RWCString name;
RWCString phone;
};
RWDBReader& operator>>(RWDBReader& reader, Record& record) {
return reader >> record.name >> record.phone;
}
// これはオマケ。
ostream& operator<<(ostream& strm, const Record& record) {
strm << setw(20) << record.name
<< setw(14) << record.phone
<< endl;
return strm;
}
検索自体は前述のとおり、
selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
ここまでは同じなのですが、execute()せずに、
RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > founds(selector);
とやると、検索の結果がfoundsに一括して登録されます。
RWDBTPtrMemTableの第2
template引数にはレコードを格納するコンテナを与えます。Tools.h++の提供するコンテナRWTPtrOrderedVector, RWTPtrSlist, RWTPtrDiistなどが指定できます。
読み出してみましょう。
for ( i = 0; i < founds.entries(); ++i ) {
cout << *founds.at(i);
}
founds.clearAndDestroy();
最後のclearAndDestroy()は、operator new()によって取得されたRecordをシステムに返却するためのものです。
police 110
time 117
ambulance 119
fire 119
また、RWDBTPtrMemTableのコンストラクタにはselectorだけでなく、テーブル(RWDBTable)やreader(RWDBReader)も与えることができますから、テーブル全体の一括読み出しも可能です。
typedef RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > RecVector;
RecVector records(person);
for ( i = 0; i < records.entries(); ++i ) {
cout << *records.at(i);
}
records.clearAndDestroy();
police 110
forecast 177
time 117
ambulance 119
episteme 045-XXX-XXXX
fire 119
いかがでしょうか、SQLの心得のある方なら、DBTools.h++を使ったデータベースの操作がC++のオブジェクトにとても上手にマッピングされているのが理解できるかと思います。
ここまでの各操作をまとめてテストするコードを以下に示します。
#include <windows.h>
#include <iostream>
#include <iomanip>
#include <rw/db/dbmgr.h>
#include <rw/db/db.h>
#include <rw/db/tpmemtab.h>
#include <rw/tpordvec.h>
using namespace std;
/* 発行されているSQLコマンドをモニタしたいときは
* 以下のコメントを外してください
*/
// #define TRACE
struct Record {
RWCString name;
RWCString phone;
};
RWDBReader& operator>>(RWDBReader& reader, Record& record) {
return reader >> record.name >> record.phone;
}
ostream& operator<<(ostream& strm, const Record& record) {
strm << setw(20) << record.name
<< setw(14) << record.phone
<< endl;
return strm;
}
void dump(RWDBReader& reader) {
int count = 0;
Record record;
while ( reader() ) {
reader >> record;
cout << record;
++count;
}
cout << count << " records." << endl << endl;
}
int trial() {
// データベースに接続
cout << "データベース'TRIAL'に接続します。" << endl;
RWDBDatabase db = RWDBManager::database(
"ODBC", // serverType
"TRIAL", // serverName
"ALLADIN", // userName
"abracadabra", // password
"", // databaseName
"" // role
);
if ( !db.isValid() ) return 1;
#ifdef TRACE
RWDBTracer& tracer = db.tracer();
tracer.setOn(RWDBTracer::SQL);
tracer.stream(cout);
#endif
// テーブルを作成
RWDBTable person = db.table("PERSON");
if ( person.exists(true) ) {
cout << "テーブル'PERSON'を削除します。" << endl;
person.drop();
}
cout << "テーブル'PERSON'を作成します" << endl;
RWDBColumn name(person["NAME"]);
RWDBColumn phone(person["PHONE"]);
name .type(RWDBValue::String)
.storageLength(50)
.nullAllowed(false);
phone.type(RWDBValue::String)
.storageLength(50)
.nullAllowed(false);
RWDBSchema schema;
schema.appendColumn(name);
schema.appendColumn(phone);
db.createTable(person.name(), schema);
// レコードを追加
cout << "レコードを追加します。" << endl;
struct {
const char* name;
const char* phone;
} entries[] = {
{ "police", "110" },
{ "fire", "119" },
{ "weather", "177" },
{ "time", "117" },
{ "ambulance", "119" },
{ "episteme", "045-XXX-XXXX" },
{ "episteme", "060-XXX-XXXX" },
{ "friend", "060-XXX-XXXX" },
{ "s34", "06-XXXX-XXXX" },
{ 0,0 } // end marker
};
RWDBInserter inserter = person.inserter();
for ( int i = 0; entries[i].name; ++i ) {
inserter << entries[i].name << entries[i].phone;
inserter.execute();
}
Sleep(1000);
RWDBReader reader;
RWDBSelector selector;
RWDBDeleter deleter ;
RWDBUpdater updater;
// 全レコードの読み出し
cout << "追加されたレコードは:" << endl;
dump(person.reader());
// レコードの削除
cout << "電話番号が'06'で始まるレコードを削除します" << endl;
deleter = person.deleter();
deleter.where(phone.like("06%"));
deleter.execute();
Sleep(1000);
dump(person.reader());
// レコードの更新
cout << "'weather'を'forecast'に変更します" << endl;
updater = person.updater();
updater << name.assign("forecast");
updater.where(name == "weather");
updater.execute();
Sleep(1000);
dump(person.reader());
// 条件によるレコードの読み出し
cout << "電話番号が'11'から始まるレコードは:" << endl;
selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
selector.execute();
dump(selector.reader());
// 条件によるレコードの読み出し-2
cout << "電話番号が'11'から始まるレコードは:" << endl;
selector = db.selector();
selector << person;
selector.where(phone.like("11%"));
typedef RWDBTPtrMemTable<Record, RWTPtrOrderedVector<Record> > RecVector;
RecVector founds(selector);
for ( i = 0; i < founds.entries(); ++i )
cout << *founds.at(i);
cout << endl;
founds.clearAndDestroy();
// 全レコードの読み出し
cout << "現在の全レコードは:" << endl;
RecVector records(person);
for ( i = 0; i < records.entries(); ++i )
cout << *records.at(i);
cout << endl;
records.clearAndDestroy();
return 0;
}
void onError(const RWDBStatus& aStatus) {
aStatus.raise();
}
int main() {
RWDBManager::setErrorHandler(onError);
int ret = 0;
try {
ret = trial();
} catch ( RWExternalErr& er) {
cout << er.why() << endl;
}
return ret;
}
はじめてのDBTools.h++ (part-4)
JOIN と WHERE
SQLによるテーブルの検索の際、多くの場合検索条件をWHERE節に記述します。
たとえば2つのテーブル:
|
|
があり、'大阪府'に住む人を探し出すSQLは
SELECT person.name
FROM person, address
WHERE person.addr_id = address.addr_id
AND
address.pref = '大阪府'
となるでしょう。
ここで、WHERE節に指定したふたつの条件文はそれぞれ別の意味を持っています。
すなわち、
person.addr_id =address.addr_id
は、ふたつのテーブルpersonとaddressをひとつのテーブルに結合する条件であり、
address.pref = '大阪府'
は、結合されたテーブルから列を得るための検索条件です。
結合条件と検索条件をひとつのWHERE節に混在させるより、JOINによる結合をFROM節に記述することで、WHERE節には検索条件だけが残り、すっきりしたSQL文となります。
SELECT person.name
FROM person INNER JOIN address
ON person.addr_id = address.addr_id
WHERE address.pref = '大阪府'
こう書けば、"addr_idで結合されたテーブルから'大阪府'を見つけ出す"という検索条件がはっきりします。DBTools.h++で書けば以下のようになります。
RWDBDatabase db = ...;
RWDBTable person = db.table("person");
RWDBTable address = db.table("address");
RWDBSelector selector = db.selector();
selector << person["name"];
selector.from(rwdbInner(person, address));
selector.on(person["addr_id"] == adress["addr_id"]);
selector.where(address["pref"] == "大阪府");
...
DBTools.h++ v4.1 でのJOIN
上述のように、テーブルの結合をFROM〜JOIN〜ONで行なうことでWHERE節をすっきり記述できるのですが、DBTools.h++ v4.0まではこのJOIN〜ONに大きな制限がありました。
|
|
|
上のようなテーブルから、全従業員の所属する部署の一覧を作ることを考えてみましょう。
従業員と所属部署との対応はt_mapによって対応づけられています(ただし従業員の中には所属部署の定まらない新入社員が存在するものとします)。
各テーブルの内容は、たとえば次のようになります('江口'さんと'大木'さんは新入社員であり、まだ所属部署が決まっていません)。
|
|
|
さてこのとき、この3つのテーブルを t_map.job_id = t.job.id かつ t_map.id = t_map.emp_idという条件で結合するには、JOINの入れ子を作ることになります。すなわち、"t_mapと t_job を条件 t_map.job_id = t_job.id で結合したもの" と t_emp を条件t_emp.id = t_map.emp_idで結合したものが求めるテーブルとなります。SQL文では
SELECT t_emp.name, t_job.name
FROM t_emp LEFT JOIN
( t_map INNER JOIN t_job
ON t_map.job_id = t_job.id)
ON t_emp.id = t_map.emp_id
ここでDBTools.h++の問題が露呈します。つまり、JOINの結合条件であるONはRWDBSelectorのメソッドon()で指定するため、JOINの入れ子が表現できないのです。
DBTools.h++ v4.1ではこの(JOINが入れ子にできない)が解消されました。JOINの結合条件を指定するメソッドon()がRWDBSelectorからRWDBJoinExprに移されました(RWDBSelector::on()も残してありますが、そのうちなくなってしまうと思われます)。
DBTools.h++ v4.1でJOINの入れ子が可能になり、以下のようなコードで記述できるようになりました。
/*
* SELECT t_emp.name, t_job.name
* FROM t_emp LEFT JOIN
* ( t_map INNER JOIN t_job
* ON t_map.job_id = t_job.id)
* ON t_emp.id = t_map.emp_id
*/
RWDBTable emp = db.table("T_EMP");
RWDBTable job = db.table("T_JOB");
RWDBTable map = db.table("T_MAP");
RWDBSelector selector = db.selector();
selector << emp["name"] << job["name"];
RWDBJoinExpr inner_join =
rwdbInner(map, job).on(map["job_id"] == job["id"]);
RWDBJoinExpr left_join =
rwdbLeftOuter(emp, inner_join).on(emp["id"] == map["emp_id"]);
selector.from(left_join);
...
sample code (only for DBTools.h++ v4.1 or later)
#include <iostream>
#include <locale>
#include <rw/db/db.h>
using namespace std;
#ifdef _DEBUG
#define SERVER_TYPE "msq15d.dll"
#else
#define SERVER_TYPE "msq12d.dll"
#endif
int trial() {
// データベースに接続
RWDBDatabase db = RWDBManager::database(
SERVER_TYPE, // serverType
"MSSQL_TRIAL", // serverName
"sa", // userName
"", // password
"TRIAL" // databaseName
);
if ( !db.isValid() ) return 1;
RWDBConnection connection = db.connection();
// 3つのテーブル 'T_EMP', 'T_JOB', 'T_EMP' を作成
RWDBTable emp = db.table("T_EMP");
RWDBTable job = db.table("T_JOB");
RWDBTable map = db.table("T_MAP");
RWDBOSql sql;
if ( !emp.exists(connection, TRUE) ) {
const char* statements[] = {
"create table T_EMP(ID int, NAME char(10))",
"insert into T_EMP values(1, '相川')",
"insert into T_EMP values(2, '井上')",
"insert into T_EMP values(3, '上田')",
"insert into T_EMP values(4, '江口')",
"insert into T_EMP values(5, '大木')",
0 };
for ( int i = 0; statements[i]; ++i ) {
sql.statement(statements[i]);
sql.execute(connection);
}
}
if ( !job.exists(connection, TRUE) ) {
const char* statements[] = {
"create table T_JOB(ID int, NAME char(10))",
"insert into T_JOB values(1, '総務')",
"insert into T_JOB values(2, '営業')",
"insert into T_JOB values(3, '広報')",
"insert into T_JOB values(4, '開発')",
"insert into T_JOB values(5, '保守')",
0 };
for ( int i = 0; statements[i]; ++i ) {
sql.statement(statements[i]);
sql.execute(connection);
}
}
if ( !map.exists(connection, TRUE) ) {
const char* statements[] = {
"create table T_MAP(EMP_ID int, JOB_ID int)",
"insert into T_MAP values(1, 1)",
"insert into T_MAP values(2, 2)",
"insert into T_MAP values(3, 3)",
0 };
for ( int i = 0; statements[i]; ++i ) {
sql.statement(statements[i]);
sql.execute(connection);
}
}
/*
* SELECT t_emp.name, t_job.name
* FROM t_emp LEFT JOIN
* ( t_map INNER JOIN t_job
* ON t_map.job_id = t_job.id)
* ON t_emp.id = t_map.emp_id
*/
RWDBSelector selector = db.selector();
selector << emp["name"] << job["name"];
RWDBJoinExpr inner_join =
rwdbInner(map, job).on(map["job_id"] == job["id"]);
RWDBJoinExpr left_join =
rwdbLeftOuter(emp, inner_join).on(emp["id"] == map["emp_id"]);
selector.from(left_join);
cout << selector.asString() << endl;
RWDBReader reader = selector.reader(connection);
while ( reader() ) {
RWCString emp_name, job_name;
reader >> emp_name >> job_name;
if ( job_name.isNull() ) {
job_name = "(NULL)";
}
cout << emp_name << '\t' << job_name << endl;
}
cout << db.version() << endl;
emp.drop();
job.drop();
map.drop();
return 0;
}
void onError(const RWDBStatus& aStatus) {
if ( aStatus.errorCode() == RWDBStatus::ok )
return;
cerr << aStatus.vendorMessage1() << " / "
<< aStatus.vendorMessage2() << endl;
aStatus.raise();
}
int main() {
locale::global(locale("japanese"));
RWDBManager::setErrorHandler(onError);
int ret = 0;
try {
ret = trial();
} catch ( RWExternalErr& er) {
cerr << er.why() << endl;
} catch ( exception& ex ) {
cerr << ex.what() << endl;
}
return ret;
}