貯金箱プログラムのリファクタリング
今週は,オブジェクト指向から少し離れて,プログラムの「リファクタリング」をテーマにします.
「リファクタリング」とは,プログラムの機能を変えないで中身を改善していく作業のことです.前回までに作成した貯金箱プログラムをよく見てみると,たとえば,以下のような問題を指摘することができます.
(1) 同じような処理をするコードが散らばっている
SavingsBoxクラスのinsertCoin100とinsertCoin500は,ほとんど同じ中身で,違っているのはその金額だけです.これは通称コピペコードと呼ばれ,プログラマの間で忌み嫌われるものです.また,コインの種類を増やそうとしたとき,いちいちinsertCoin1, insertCoin5 などといった似たようなメソッドをさらに追加しなくてはなりません.これを何とかしましょう.
(2) 鍵がかかっている貯金箱に対して鍵がかけられる
これは,どちらかというとプログラムの仕様(機能)に関することかもしれませんが,前回のプログラムでは鍵がかかっている貯金箱にさらに鍵をかけることができてしまいます(事実上は何もしませんが).これは気持ちが悪いので,これらの操作に対してチェックを行い,鍵が既にかかっている貯金箱には改めて鍵をかけられないようにしましょう.
(3) コマンドの入力方式が拡張性に欠ける
貯金箱と操作,コインの種類の組み合わせに対して,コマンド番号を割り当てているので,これらの数や種類が増えると,コマンド番号が増えて破綻します.もう少し一般的な入力方式を考えることにしましょう.
ところで,リファクタリングというのは,既に動いているソフトウェアを改変するわけですから,それによってもともと動いていた機能が動かなくなる,新たにバグを入れ込んでしまう(デバッグの反対なのでエンバグといったりします),というような危険性があります.これは多くの人に使ってもらうような種類のプログラムでは大問題(おそらくプログラムの作者に対してユーザから文句のメールが殺到することでしょう)になります.そのような事態を避けるためには,テストが重要です.リファクタリング対象となるプログラムのテストコードを書いて,リファクタリングした後でそのテストコードを実行し,機能が損なわれていないことを確認するのが良いと思います.(今回は省略します...)
列挙型を使ってコインの種類を表す
まずは,問題点(1)についてよく考えてみます.コインの種類を増やしたときに,貯金箱が提供すべき操作(メソッド)の種類が増えてしまうのは,おかしなことです.あくまでも「コインを挿入する」という操作はコインの種類と無関係になるようにしましょう.
そこで,insertCoin100, insertCoin500というようにメソッド名でコインの種類を表現するのではなく,insertCoinメソッドの引数でコインの種類を指定するようにします.さて,引数の型はどうするのがよいでしょうか?一番簡単なのは,コインの額面を引数にしてしまうことです.たとえば,100円玉を一枚挿入する場合,insertCoin(100), 500円玉の場合はinsertCoin(500)というようにする方法です.この方法は簡単で良いのですが,123円玉や-500円玉のようなあり得ないコインも貯金できてしまうので,今回の貯金箱のモデルとしては好ましくありません.もちろん,引数をチェックすることによって,へんてこなコインの挿入を排除することは可能ですが,それよりも,コインの種類を適切に表現できるような何かを定義しましょう.
そのような表現にぴったりのJavaの言語機構として,ここでは,列挙型を使った方法を紹介します.列挙型はたとえば,曜日(月,火,水,...)とか色(赤,青,黄,...)など,有限の種類のデータ型を表現するのに適しています.ここでは,コインの種類,1円玉,5円玉,10円玉,...を列挙型Coinとして定義することにしましょう.Javaで列挙型を定義したい場合,enumキーワードを使って以下のように書くことができます.
enum Coin { Y1, Y5, Y10, Y50, Y100, Y500 }
これで,Coin型が定義できました.50円玉を挿入する場合は,insertCoin(Coin.Y50)のようにします.
さてここで,ひとつ問題があります.コインの額面の情報をどうやって持たせたらよいのでしょうか?Coin型の要素(Y1, Y5, ...)と額面を対応させる表を別途定義する,というのも一案ですが,ここではもっと直接的に,Coin型の列挙定義の中にその情報を埋め込んでしまいましょう.また,額面を取得するメソッドvalue()も同時に定義してやります.
enum Coin { // コインの種類を定義(額面で初期化) Y1(1), Y5(5), Y10(10), Y50(50), Y100(100), Y500(500); private final int value; // 額面情報を保持するフィールド Coin(int value) { this.value = value; } public int value() { return value; } }
これにあわせて貯金箱の定義を以下のように修正します.
class SavingsBox { ... private ArrayList<Coin> coins; // 貯金箱にあるコイン達 ... // コインを入れる public void insertCoin(Coin c) { coins.add(c); } ... // 合計金額を取得する public int getAmount() { int total = 0; for (Coin c : coins) { total += c.value(); } return total; } ... }
insertCoin()では,コインをArrayListに追加するだけの処理とし,getAmount()が呼ばれるたびに,コインの合計金額を計算するようにしました.
※ ここでは,insertCoin()メソッドの中では,鍵がかかっているかどうかのチェックはしていません.これは以降の説明で修正します.
貯金箱の施錠状態をチェックする
前回の貯金箱では,コインの挿入時には,鍵の状態をチェックしていましたが,鍵をかけるとき(lock)と,はずすとき(unlock)にもこれらのチェックを行うように修正しましょう.
要件を整理すると,
- insertCoin操作は解放(locked==false)状態のときのみ許される
- lock操作は解放(locked==false)状態のときのみ許される
- unlock操作は施錠(locked==true)状態のときのみ許される
ということになります.
鍵かかかった状態でのlock操作や鍵がはずれた状態でのunlock操作は不正な操作ということで,その都度チェックする必要があります.さて,insertCoinと同様に,このチェック処理を貯金箱クラス(SavingsBox)に押し付けてしまい,lock 操作, unlock操作の中で施錠状態をチェックしてもよいのですが,今回はその責任を呼び出し側にまかせることにしましょう.すなわち,これらの操作を呼び出す側で,もし貯金箱の施錠状態がわからない場合には,
if (savingsBox.isLocked() == false) { savingsBox.insertCoin(Coin.Y100); }
のように,必ず貯金箱の状態を確認してから呼び出すようにします.
このようにすることのメリットは何でしょうか?lockやunlock操作の中でいちいちチェックする必要が省けるのでSavingsBoxクラスの実装はシンプルになりますが,その分,呼び出し側のチェックが必要になってくるので,その点ではあまり変わり嬉しいことはありません.一番のメリットは,エラー処理を呼び出し側に任せることで,貯金箱クラスの再利用性が高まることです.前に作った貯金箱クラスのinsertCoin100()の処理を見てください.このメソッドの中では,以下のように鍵がかかった状態のときにはエラーメッセージを表示して終了するようにしています.
public void insertCoin100(int coins) { if(this.lock) { // コインは挿入できない。メッセージにその旨が表示される。 this.showMessage("この貯金箱には鍵がかかっています。"); } else { ...
このSavingsBoxクラスを別のアプリケーションの中で再利用することを考えたとき,このままではエラー発生時の扱いが固定されてしまいます.アプリケーションによっては,エラー時にはメッセージをログファイルに書き出したい,とか,あるいはネットワークアプリケーションのような場合,エラーが発生したことをメッセージとして要求元に応答したい,といったケースがあるかもしれません.そのような場合,SavingsBoxクラスの中のエラー処理の部分をいちいち書き換えなくてはならなくなり,再利用性を損ねます.ということで,今回は,呼び出し側(アプリケーション側)でエラーのチェックをし,必要に応じてエラー処理を行うことにします.これは,SavingsBoxクラスの利用者に対して,「insertCoin()はunlock状態の時にのみ呼び出し可能である」という契約を結ぶことを意味します.利用者はこの契約を守る義務があります.また,操作を提供する側は,この契約が守られることを前提にinsertCoin()を実装することができるので,以下のようにシンプルに記述することができるようになります.
public void insertCoin(Coin c) { coins.add(c); }
これでも良いのですが,利用者側のコードにバグがあり,契約が破られてinsertCoinを呼び出された場合,何ごともなかったかのように,処理が進んでしまいます.そこで,表明(asseert)を用いてそのようなケースをきちんと発見できるようにしましょう.
Javaのassert文を使うと,その文の実行時に満たされているべき条件を明確に記述することができるようになります.ここでは,insertCoinメソッドを実行する前に満たされているべき条件(事前条件ともいいます)locked==falseを,assert文を使って明示的に指定するよう,insertCoinメソッドを以下のように書き直します.
public void insertCoin(Coin c) { assert this.locked == false; coins.add(c); }
assertキーワードの後には,boolean型の値を返す式を記述します.実行時に,この式が偽(false)と評価されると,エラー(AssertionError)が報告されてプログラムが停止します.真(true)と評価された場合には,assert文は何事もなかったかのように終了し,その次の文が実行されます.
上のassert this.locked == falseの例では,貯金箱に鍵がかかっていなければ,何ごともなかったかのように処理が続くけれど,鍵がかかっている場合(this.locked == falseの結果が偽,すなわちthis.lockedがtrueの場合),AssertionErrorが発生し,プログラムのどの部分でassertが失敗したかを報告し,プログラムが終了します.このようにしておくことで,プログラムにバグがあって契約が満たされなかった場合,プログラマはどこに原因があるのかすぐに見つけることができます.
#ちなみに,このような考え方にもとづくプログラムの設計を「契約による設計#(Design by Contract:DbC)」と呼びます.DbCを直接的にサポートしているオブジェクト指向プログラミング言語としてEiffelが有名です.Eiffelでは事前条件,事後条件をプログラムに記述することが可能です.
以上をまとめると,最終的にSavingsBoxクラスの定義は以下のようになります.
public class SavingsBox { private String name; private ArrayList<Coin> coins; private boolean locked; public SavingsBox(String name) { this.name = name; this.coins = new ArrayList<Coin>(); this.locked = false; } public void insertCoin(Coin c) { assert !locked; // 鍵がかかっていないこと coins.add(c); } public String getName() { return name; } public boolean isLocked() { return locked; } public int getAmount() { int total = 0; for (Coin c : coins) { total += c.value(); } return total; } public void lock() { assert !locked; // 鍵がかかっていないこと locked = true; } public void unlock() { assert locked; // 鍵がかかっていること locked = false; } }
※ 全貯金箱の合計金額を求めるためのgetAllAmount()メソッド, totalAllフィールドはここでは,削除しました.
コマンド入力方式を検討する
さて,最後の問題点(3)に対応するため,コマンドの入力方式について検討してみることにしましょう.先週のプログラムでは,コマンド(操作)とその対象となる貯金箱やコインの組合せに対して,あらかじめ番号を割り当てておき,その番号をユーザーに入力してもらう,というやり方を採用していました.この方法は,ユーザーの入力を解析しやすい,という利点はあるのですが,本稿のはじめの方で指摘した通り,コマンドの種類や対象となる貯金箱の数が増えると,あっという間に破綻します.この問題は,コインを入れる,鍵をかける,といったコマンドの種類と,その操作対象となる貯金箱の名前やコインの種類を特定の書式に従って,ユーザーが個別に指定できるようにすれば解決します.たとえば,「貯金箱Aに100円玉を入れる」という操作を実行するためには
"insert A 100"
というような文字列をユーザーが入力するようにします.その他の操作としては,鍵をかける(lock),鍵をはずす(unlock),貯金箱の一覧を表示する(list),といったものが考えられます.それぞれの操作の書式を以下のように整理します.
- insert
--- 貯金箱にコインを入れる - lock
--- 貯金箱に鍵をかける - unlock
--- 貯金箱の鍵をはずす - list --- 貯金箱の一覧を表示する
- quit --- 終了する
※ コマンドと各操作対象の区切りは空白文字(スペースやタブなど)とする
全体の処理の流れは,以下のようになります.
while (true) { System.out.print("コマンドを入力してください:"); // ユーザーからの入力を解析する // if (ユーザーの入力が終了命令) break; // 入力に応じた処理を実行する }
入力に応じた処理を実行するための,簡単な方法は,コマンドの種類ごとに分岐文を使う以下のようなやり方が思いつきます.
if (ユーザー入力が"insert") { // 貯金箱にコインを入れる処理 } else if (ユーザー入力が"lock") { // 貯金箱に鍵をかける処理 } else if...
このやり方でも良いのですが,ユーザーからの入力を解析する処理の部分で,入力文字列の種類ごとの分岐処理が必要あり,さらに実行の部分でも同じような分岐処理を入れるのは,あまり賢明なやり方ではありません.かといって,ユーザーからの入力を解析しながら,コマンド操作を実行するという方法も,プログラムがごちゃごちゃしてしまいそうです.
コマンドパターンによる各種操作の実装
そこで,今回はそれぞれの操作を対応するコマンドクラスのオブジェクトとしてカプセル化し,そのオブジェクトのメソッドを呼び出すことで処理を実行する,というやり方で実現していくことにします.
# デザインパターンにおけるコマンドパターンとして知られているやり方です
コマンドパターンを使って,全体の処理の流れを書き直すと以下のようになります.
while (true) { System.out.print("コマンドを入力してください:"); // ユーザーからの入力を解析し,対応するコマンドオブジェクトを取得する Command command = parseCommand(...); // 終了操作の場合,nullが返される if (command == null) break; // 入力に応じた処理を実行する command.execute(); }
ここでのポイントは,command変数に格納されるコマンドオブジェクトは,ユーザーが入力したコマンド文字列によって,「コインを挿入する」,「鍵をかける」など,異なる種類のものになる可能性がある,ということです.したがって,command.execute()で実際に実行される処理の内容もそのつど変わります.
# 専門的にはこのような性質を多態性(あるいは多相性,英語ではpolymorphism)といったりします.
オブジェクト指向プログラミングでは「継承」と呼ばれる機構を使うことによって,このような多態性を実現することができます.
# インタフェース(interface)を使ってもできますが,interfaceは制限された形の継承とみなすことができるので,結局は同じことです.
それでは,継承を使ってコマンドパターンを実装していくことにしましょう.まず,継承元のクラスであるCommandクラスを以下のように定義します.
abstract class Command { public abstract void execute(); }
たったこれだけです.この定義の特徴は,execute()メソッドの中身が空っぽで,メソッド名と引数,戻り値の型だけが指定されている点です.このように,中身の実装のないメソッドのことをJavaでは抽象メソッドと呼び,そのようなメソッドはabstract修飾子を使って明示的に示します.抽象メソッドを1つでも含んでいるようなクラスを抽象クラスと呼び,こちらもabstract修飾子を使って明示的に指定します.抽象クラスはインスタンスを作成することはできません.すなわち,以下のようなコードはコンパイラによってはじかれます.
Command c = new Command(); // コンパイルエラーになる
さて,このように中身のない空っぽのメソッドを定義して,何の意味があるのでしょうか?実は,抽象メソッドは,それを継承したクラスの中で具象メソッドとして実装される必要があります.すなわち,抽象クラスであるCommandクラスから継承したクラスを定義し,その中で改めてexecute()メソッドを定義する必要があるのです.このあたりは,章をあらためて詳しく説明することとし,ここではとりあえず先に進みましょう.
継承した貯金箱にコインを挿入するコマンドを表すCommandInsertクラスをCommandクラスから継承して以下のように定義します.
class CommandInsert extends Command { Coin coin; SavingsBox savingsBox; public CommandInsert(SavingsBox sb, Coin c) { this.savingsBox = sb; this.coin = c; } public void execute() { if (savingsBox.isLocked()) { System.out.println("貯金箱 '" + savingsBox.getName() + "'には鍵がかかっています."); return; } savingsBox.insertCoin(coin); System.out.println("貯金箱 '" + savingsBox.getName() + "'に" + coin.value() + "円を入れました."); } }
extendsキーワードの後には継承元となるクラス名を指定します.コンストラクタの引数として,対象となる貯金箱,コインの種類を指定します.引数で渡されたこれらのオブジェクトは,それぞれフィールドsavingsBoxとcoinに格納され,execute()メソッドでは,それらのオブジェクトが参照されます.execute()メソッドでは,まず貯金箱に鍵がかかっているかどうかをチェックし(これらのチェックは貯金箱クラスの利用者の責任としたことを思い出してください),鍵がかかっていないときだけ,コインを挿入して,その結果を標準出力に表示します.
同じように,鍵をかけるCommandLockや鍵をはずすCommandUnlock,貯金箱の一覧を表示するCommandListといったクラスも,それぞれ以下のように定義することができます.
// 鍵をかける class CommandLock extends Command { SavingsBox savingsBox; public CommandLock(SavingsBox sb) { this.savingsBox = sb; } public void execute() { if (savingsBox.isLocked()) { System.out.println("貯金箱 '" + savingsBox.getName() + "'には鍵がかかっています."); return; } savingsBox.lock(); System.out.println("貯金箱 '" + savingsBox.getName() + "'に鍵をかけました."); } } // 鍵をはずす class CommandUnlock extends Command { SavingsBox savingsBox; public CommandUnlock(SavingsBox sb) { this.savingsBox = sb; } public void execute() { if (!savingsBox.isLocked()) { System.out.println("貯金箱 '" + savingsBox.getName() + "'に鍵はかかっていません."); return; } savingsBox.unlock(); System.out.println("貯金箱 '" + savingsBox.getName() + "'の鍵をはずしました."); } } // 貯金箱の一覧を表示する class CommandList extends Command { Collection<SavingsBox> savingsBoxes; public CommandList(Collection<SavingsBox> sbs) { this.savingsBoxes = sbs; } public void execute() { for (SavingsBox sb : savingsBoxes) { System.out.println("貯金箱 '" + sb.getName() + "' " + sb.getAmount() + "円" + (sb.isLocked() ? " (鍵がかかっています)" : "") ); } } }
これらのクラスはすべてCommandクラスから継承していますが,execute()の中身は異なっています.command.execute()で実際に呼び出されるexecute()は,例えば,command変数に格納されたオブジェクトがCommandListクラスのオブジェクトならば,その中のexecute()メソッドが呼ばれ,またCommandInsertクラスのオブジェクトならば,それのexecute()といった具合に,オブジェクトの種類によって,変わってきます.
1つ注意すべき点は,command変数にいろいろな種類のコマンドクラスのオブジェクトを代入できるのは,これらのクラスがすべてCommandクラスという1つのクラスから継承しているおかげだということです.Javaは静的に型付けされた言語なので,(BarがFooから継承したクラスでない限り)Fooというクラス型の変数にBarというクラス型のオブジェクトを代入することはできないのです.
コマンドパーサーの実装
さて,各コマンドクラスの実装は以上です.次に実際にユーザーからの入力文字列を解析して,これらコマンドクラスのオブジェクトを生成する処理を実装していくことにしましょう.
まずは,ユーザーから入力した文字列を空白文字を区切りとして,いくつかの固まり(トークンと呼びます)に切り分ける処理が必要です.たとえば,"insert A 100"という入力は,"insert", "A", "100"という3つのトークンに分解されます.そのためのクラスとして,JavaではStringTokenizerという便利なものがライブラリに用意されているので,今回はそれを使うことにします.
処理の流れは以下のようになります.
while (true) { String line = scanner.nextLine(); StringTokenizer tok = new StringTokenizer(line); Command command = parseCommand(tok); if (command == null) { System.out.println("終了します"); break; } command.execute(); }
parseCommandは,StringTokenizerクラス型の引数を取ります.parseCommandでは,引数で受け取ったStringTokenizerオブジェクトからトークンを1つ取り出し,その中身に応じたコマンドクラスのオブジェクトを生成し,returnします.
だいたい,以下のような感じになります.
static Command parseCommand(StringTokenizer tok) { String commandStr = tok.nextToken(); if (commandStr.equals("insert")) { SavingsBox sb = parseSavingsBox(tok); Coin c = parseCoin(tok); return new CommandInsert(sb, c); } else if (commandStr.equals("lock")) { SavingsBox sb = parseSavingsBox(tok); return new CommandLock(sb); } else if ... }
parseSavingsBox()は,トークンを取り出し,それに対応する貯金箱オブジェクトをテーブルから検索し,返します.また,parseCoin()も同様に対応するコインオブジェクトを返します.
さて,ここで考えなくてはならない重要なことに,エラー処理があります.ユーザーからの入力は間違った書式のコマンドが渡されてくる可能性があるので,そのような場合でも適切な対処をしなくてはなりません.たとえば,入力が空っぽだったり,スペルミスをしたり(inset A 100),存在しないコインが指定されたり(insert A 101)といったことが考えられます.そのような入力がなされたときは,(親切に)ユーザーにその旨をメッセージとして表示するようにし,改めて入力し直してもらうようにしましょう.
エラー処理の方針が決まったところで,次に考えるべきことは,エラーが発生したことを,どうやってメソッドの呼び出し元に知らせるか,ということです.伝統的なやり方では,メソッドの中でエラーが発生するような状況になったら,
if (エラー状態) { return エラーコード; }
というような形でエラーコードを呼び出し元に返してやる,というものです.
呼び出し側では,
ret = obj.someMethod(); if (retがエラーコード) { // エラーに対処する }
というような処理を書きます.
残念ながら今回のケースでは,メソッドの戻り値はコマンドオブジェクトとしているために,エラーコードを返す余地がありません.また,プログラムの大部分がエラーコードの判定に費やされてしまい,複雑になりがちです.そこで,例外を使うことにします.メソッドの中で不正な入力文字列を検出したらParseExceptionという例外を投げることにします.ParseExceptionはExceptionクラスから継承して以下のように定義します.
class ParseException extends Exception { public ParseException(String s) { super(s); } }
最終的に,parseCommandの実装は以下のようになります.
private static Command parseCommand(StringTokenizer tok) throws ParseException { if (!tok.hasMoreTokens()) { throw new ParseException("コマンドが指定されていません"); } String commandStr = tok.nextToken(); if (commandStr.equals("insert")) { SavingsBox sb = parseSavingsBox(tok); Coin c = parseCoin(tok); return new CommandInsert(sb, c); } else if (commandStr.equals("lock")) { SavingsBox sb = parseSavingsBox(tok); return new CommandLock(sb); } else if (commandStr.equals("unlock")) { SavingsBox sb = parseSavingsBox(tok); return new CommandUnlock(sb); } else if (commandStr.equals("list")) { Collection<SavingsBox> sbs = savingsBoxTable.values(); return new CommandList(sbs); } else if (commandStr.equals("quit")) { return null; } throw new ParseException("'" + commandStr + "' というコマンドはありません"); }
上記の中で,throw new ParseException(...);の部分が実際に例外を投げている部分です.Javaで例外を投げるにはthrowキーワードを用います.例外が発生した時のプログラムの実行はだいたい以下のようになります.
# throw文によって明示的に例外を投げる以外にも例外は発生します.たとえば,nullオブジェクトに対するメソッド呼び出しや,配列の範囲を超えたアクセスが生じた状況などです.
- 例外が発生した箇所以降の文の実行は中止される.
- 例外が発生する部分を取り囲む適切なtry〜catchがある場合,例外は捕捉されcatch節に実行が移る.
- 例外が捕捉されない場合,メソッドの実行は中止され,そのメソッドの呼び出し元に同じ例外が発生する.
以上によって,捕捉されない例外はメソッドの呼び出し系列を次々にさかのぼって伝播していくことがわかります.
parseCommand()メソッドの場合,たとえば,読み出すべきトークンが空だとParseException例外を投げていますが,そこの部分はtry〜catchで囲まれていないので,その例外はparseCommandの呼び出し側に伝播します.parseCommandはこのParseException例外を発生させる可能性があるので,メソッド宣言のところで明示的にthrows ParseExceptionと記述する必要があります.この例外をry〜catchで捕捉する箇所は後ほど紹介します.
さて,同様にして,parseSavingsBoxとparseCoinも定義します.ここでは,貯金箱オブジェクトを格納した表savingsBoxTableとコイン一覧を格納した表coinTableがMainクラスの中で定義されていると仮定しています.
private static SavingsBox parseSavingsBox(StringTokenizer tok) throws ParseException { if (!tok.hasMoreTokens()) { throw new ParseException("貯金箱が指定されていません"); } String sbStr = tok.nextToken(); SavingsBox sb = savingsBoxTable.get(sbStr); if (sb == null) { throw new ParseException("'" + sbStr + "' という貯金箱はありません."); } return sb; } private static Coin parseCoin(StringTokenizer tok) throws ParseException { if (!tok.hasMoreTokens()) { throw new ParseException("コインが指定されていません"); } String coinStr = tok.nextToken(); Coin coin = coinTable.get(coinStr); if (coin == null) { throw new ParseException("'" + coinStr + "' というコインはありません."); } return coin; } これらのメソッドはparseCommandの中から呼び出されることに注意してください.parseSavingsBoxの中でParseException例外が投げられた場合,呼び出し元のparseCommandにその例外が伝播します.そしてその例外はさらに上位の呼び出し元まで伝播することになるのです.こうして,メソッド呼び出しの深い部分から一気に最上位の呼び出し元まで処理を戻すことができるようになります.
最後に,mainメソッドを含むMainクラスの定義を以下に示します.
class Main { private static HashMap<String, SavingsBox> savingsBoxTable; private static HashMap<String, Coin> coinTable; public static void main(String[] args) { // 貯金箱テーブルの初期化 savingsBoxTable = new HashMap<String, SavingsBox>(); savingsBoxTable.put("Pig", new SavingsBox("Pig")); savingsBoxTable.put("Cat", new SavingsBox("Cat")); savingsBoxTable.put("Dog", new SavingsBox("Dog")); // コインテーブルの初期化 coinTable = new HashMap<String, Coin>(); coinTable.put("1", Coin.Y1); coinTable.put("5", Coin.Y5); coinTable.put("10", Coin.Y10); coinTable.put("50", Coin.Y50); coinTable.put("100", Coin.Y100); coinTable.put("500", Coin.Y500); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("コマンドを入力してください:"); String line = scanner.nextLine(); StringTokenizer tok = new StringTokenizer(line); try { Command command = parseCommand(tok); if (command == null) { System.out.println("終了します"); break; } command.execute(); } catch (ParseException e) { // ParseException発生時の処理 System.out.println(e.getMessage()); continue; } } } // parseCommandの定義 ... // parseSavingsBoxの定義 ... // parseCoinの定義 ... }
parseCommand呼び出しの部分を,try〜catch(ParseException e)というように囲むことによって,ParseException例外を捕捉するようにしています.parseCommandを呼び出した際にParseException例外が発生したら,catch節にただちに実行が移ります.そして,エラーメッセージを表示して,continue文によってふたたびwhileループの最初から処理をやり直すようにしています.
実行例は以下のようになります.
コマンドを入力してください:list 貯金箱 'A' 0円 貯金箱 'B' 0円 貯金箱 'C' 0円 コマンドを入力してください:insert B 500 貯金箱 'B'に500円を入れました. コマンドを入力してください:insert A 123 '123' というコインはありません. コマンドを入力してください:lock A 貯金箱 'A'に鍵をかけました. コマンドを入力してください:list 貯金箱 'A' 0円 (鍵がかかっています) 貯金箱 'B' 500円 貯金箱 'C' 0円 コマンドを入力してください:unlock B 貯金箱 'B'に鍵はかかっていません. コマンドを入力してください:quit 終了します
Java によるオブジェクト指向の表現
前回は「クラスの作り方」「フィールド」「メソッド」といった Java の「機能」について説明しました。
# プログラミングの「機能」と「考え方」は異なるお話というトピックを覚えているでしょうか。
# http://d.hatena.ne.jp/java-book/20081103/1225720315
連載初期に少し話しましたが、「Java で書いたプログラムがオブジェクト指向なプログラムになる」わけではなく、「Java で書くとオブジェクト指向な表現がしやすい」ということを、前回の復習も兼ねて改めて説明してみたいと思います。
オブジェクト指向とは?
オブジェクト指向という言葉は曖昧に使われることがあるのですが、今回の記事に限っては、
「オブジェクト(対象)にメッセージという形で頼み事をすると動いてくれる」「オブジェクトは状態を持つことができる」という見方でプログラムの振る舞いをとらえる考え方を指すことにします。
class という機能で実現できること
プログラムを Java では class の中に書くことができるというのが前回の説明にありました。
オブジェクト指向を表現するための準備を class を記述する中で行うことができそうです。
# 「class の機能」と呼ぶには多少語弊があるのですが、class の機能に関しては追々、説明していきたいと思います。
class SavingsBox { // 合計金額 private int total; // コンストラクタ public SavingsBox ( ) { total = 0; } // 100円玉を入れる。 public void insertCoin100 ( int coins ) { if (coins > 0) { total += coins * 100; } } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { if (coins > 0) { total += coins * 500; } } // 合計金額を調べる。 public int getAmount ( ) { return total; } }
オブジェクト指向 | Java |
---|---|
オブジェクトを定義したい | class に定義を書く |
オブジェクトを生成したい | new でコンストラクタを呼ぶ |
メッセージで呼びかけたときの振る舞いを定義したい | メソッドを振る舞い定義領域として使用する |
状態を定義したい | フィールドを状態保持領域として使用する |
オブジェクト指向的なシナリオの記述
class Main { public static void main(String[] args) { SavingsBox box = new SavingsBox(); // シナリオ上に貯金箱を登場させる。 box.insertCoin100(3); // 貯金箱に100円玉を3枚入れることをメッセージとして伝える。貯金箱の内部状態が100円玉が3枚挿入された状態になる。 box.insertCoin500(2); // 貯金箱に500円玉を2枚入れることをメッセージとして伝える。貯金箱の内部状態が500円玉が2枚挿入された状態になる。 int total_yen = box.getAmount(); // 貯金箱に総額を尋ねる。 System.out.println(total_yen); // 貯金箱の総額を表示する。 } }
main メソッドの中で、Java の機能である new でコンストラクタを呼び出したり、メソッドを呼び出しています。
しかし、これはコメント文に書かれているようなシナリオの表現の一つとしてとらえることにします。
プログラムを書くときには、シナリオがあることが前提で、そのシナリオを Java で表現するという流れになります。
前回、最初にメソッドの外装だけ作り、中身を後から書いた理由は、シナリオだけなら中身(実装)がなくても準備することができるからです。
シナリオには、オブジェクトにどんな頼み事をする必要が出てくるかを考え、メソッドの外装を考えることになります。
private フィールドの意味
SavingsBox box = new SavingsBox(); box.insertCoin100(3); box.total = 12345; // フィールドの値を直接書き換える。 int total_yen = box.getAmount(); System.out.println(total_yen);
total フィールドを public にしたらメソッドを用意した意味がないので、private にして直接値を書き換えられないように制限をしたというのが前回の説明でした。
だったらメソッドは用意しなくてもいいんじゃないかという声が聞こえてきそうです。
しかしオブジェクト指向を表現するという前提があるならば、「オブジェクト(対象)にメッセージという形で頼み事をすると動いてくれる」ことを、より自然に表現したいものです。
なので、「メッセージはメソッドで表現する」という使い方で統一し、メッセージによって状態を変化させたいために、このような実装となるわけです。
もしも「= (イコール) 演算子」をメッセージと見立ててしまえば、オブジェクト指向の枠からは外れていないため、public フィールドで実現しても構わないことになりそうです。
実際、Ruby のように演算子もメッセージであると見なすプログラミング言語はあります。
しかし Java で実装した場合、= 演算子と public フィールドでは表現力に乏しいのです。
マイナスの金額なども入れることができるようになってしまうのですから。
public void insertCoin100 ( int coins ) { if (coins > 0) { // 金額は必ずプラスのはず total += coins * 100; } }
これをメソッドで表現することを考えれば、正しい金額のみを投入可能にすることでシナリオから矛盾をなくすことができるのです。
# 安全性を高めるという意味もあります。
インスタンスオブジェクトとクラスオブジェクト
new SavingBox();
この一文も、オブジェクトを生成すると一言で済ませてしまえばそれまでですが、あくまで Java の機能としてとらえてみましょう。
「オブジェクトを生成する」と言いましたが、正確にはインスタンスオブジェクトといいます。
オブジェクト指向らしく「すべてをメッセージとして表現する」ことを前提に置いてしまえば、new もメッセージとしてみることができないでしょうか。
メッセージというからには何かしらのオブジェクト(対象)がいるはずですが、「SavingBox というクラスに対して new というメッセージで話しかけると、インスタンスを1つくれる」という見方をするとどうでしょう。
つまり、クラスもオブジェクトであり、「クラスオブジェクトに new というメッセージで呼びかけるとインスタンスオブジェクトが返ってくる」と見てしまうのです。
Java では、クラスはオブジェクトとは見なしません。しかしそのようにとらえることも可能であり、Smalltalk や Ruby 等ではこのように考えられています。
# それならクラスオブジェクトはどこから生成されるのかというと、今度はメタクラスという話が出てきますが、Java の機能としてはありません。
class は設計図であるという説明されることもあり、言語によって考え方が異なります。
# いずれかが間違っているというわけではありませんが、そのことを知っているかどうかで大きく違うため、曖昧に「クラスは設計図のようなもので〜」という説明を受けた場合は注意したほうがいいかもしれません・・・?
クラスとインスタンスの状態遷移
フィールドという機能を使ってオブジェクトの「状態」を表現できることを説明しました。
http://d.hatena.ne.jp/java-book/20090119/1232293562
今回はインスタンスとクラスという言葉をキーワードに、オブジェクトの状態遷移について話したいと思います。
コード
今回は以下のコードを先に紹介してしまいます。
前回の SavingsBox に機能を追加しました。
SavingsBox.java
class SavingsBox { private static int totalAll = 0; // 全貯金箱の合計金額 private String name; // 貯金箱の識別子 private int total; // 貯金箱の合計金額 private boolean lock; // 貯金箱の施錠状態 // コンストラクタ public SavingsBox(String name) { this.name = name; this.total = 0; this.lock = false; } // 100円玉を入れる。 public void insertCoin100(int coins) { if(this.lock) { // コインは挿入できない。メッセージにその旨が表示される。 this.showMessage("この貯金箱には鍵がかかっています。"); } else { if (coins > 0) { this.total += coins * 100; SavingsBox.totalAll += coins * 100; } // 物理的にマイナスのコインが挿入されることはありえない。 } } // 500円玉を入れる。 public void insertCoin500(int coins) { if(this.lock) { // コインは挿入できない。メッセージにその旨が表示される。 this.showMessage("この貯金箱には鍵がかかっています。"); } else { if (coins > 0) { total += coins * 500; SavingsBox.totalAll += coins * 500; } // 物理的にマイナスのコインが挿入されることはありえない。 } } // 全貯金箱の合計金額を調べる。 public static int getAllAmount() { return SavingsBox.totalAll; } public String getName() { return this.name; } // 貯金箱の合計金額を調べる。 public int getAmount() { return this.total; } // 貯金箱に鍵をかける。 public void lock() { this.showMessage("貯金箱に鍵をかけました。"); this.lock = true; } // 貯金箱の鍵を外す。 public void unlock() { this.showMessage("貯金箱の鍵を外しました。"); this.lock = false; } // 貯金箱に付いているディスプレイにメッセージを表示する。 private void showMessage(String message) { System.out.println(message); } }
Main.java
import java.util.ArrayList; import java.util.Scanner; class Main { public static void main(String[] args) { ArrayList<SavingsBox> boxes = new ArrayList<SavingsBox>(); boxes.add(new SavingsBox("A")); boxes.add(new SavingsBox("B")); boxes.add(new SavingsBox("C")); Scanner scanner = new Scanner(System.in); while(true) { System.out.println("================================"); System.out.println("全貯金箱の合計金額 " + SavingsBox.getAllAmount()); for(SavingsBox box : boxes) { System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)"); } System.out.println("================================"); int index = 0; for(SavingsBox box : boxes) { System.out.println(index + ": 貯金箱 " + box.getName() + " に 100円玉を入れる。"); System.out.println(index+1 + ": 貯金箱 " + box.getName() + " に 500円玉を入れる。"); System.out.println(index+2 + ": 貯金箱 " + box.getName() + " に 鍵をかける。"); System.out.println(index+3 + ": 貯金箱 " + box.getName() + " に 鍵を外す。"); index = index + 4; } System.out.println("================================"); System.out.println(""); System.out.print("コマンド No を選んでください(例: 1)(抜ける場合は quit と入力): "); String line = scanner.nextLine(); if("quit".equals(line)) { break; } int commandNumber = Integer.valueOf(line); if(commandNumber < 0 || commandNumber > index) { continue; } int targetIndex = commandNumber / 4; int methodIndex = commandNumber % 4; SavingsBox target = boxes.get(targetIndex); if(methodIndex == 0) { target.insertCoin100(1); } else if(methodIndex == 1) { target.insertCoin500(1); } else if(methodIndex == 2) { target.lock(); } else if(methodIndex == 3) { target.unlock(); } } } }
インスタンスメソッドとインスタンスフィールド
boxes.add(new SavingsBox("A")); boxes.add(new SavingsBox("B")); boxes.add(new SavingsBox("C"));
new はオブジェクトを生成するという話で進めていましたが、クラスから生成されたオブジェクトのことを「インスタンス」とも呼びます。
class から生成された時点で、そのオブジェクトは他のオブジェクトとは別の存在となっています。
どういうことかと言えば、
boxA.insertCoin100(5); // boxA には 500円が入る。具体的には boxA のフィールド total に 500 という数字が入る。 boxB.insertCoin100(2); // boxB には 200円が入る。具体的には boxB のフィールド total に 200 という数字が入る。 boxC.insertCoin100(8); // boxC には 800円が入る。具体的には boxC のフィールド total に 800 という数字が入る。
このように、インスタンスフィールド(total 等)は各インスタンスごとに用意されるため、その領域はインスタンスが自由に使ってよい領域となり、他のインスタンスに使われるということもありません。
この機能を使って、「オブジェクトの状態」を表現することができます。
状態遷移
フィールドという領域を使って「状態」を表現すると何が嬉しいのでしょうか。
それを説明するために、新たな状態として lock という状態を追加しました。
lock() というメソッドで鍵をかけることができるようになり、unlock() で鍵を外すことができます。
この施錠状態を insertCoin100() メソッドで参照するようにしてみましょう。
すると、
「鍵がかかっている場合は鍵を外さないとコインを入れることができない」
「鍵がかかっていなければコインを入れることができる」
このように、オブジェクトにメッセージで頼み事をしたときに、そのオブジェクトの状態によって振る舞いが変わるようになります。
static メソッドと static フィールド
各インスタンスがそれぞれ状態を持つことが可能であり、振る舞いを変えることができる話をしました。
それでは、特定のインスタンスに対してメッセージを投げかけるのではなく、「クラス全体に対して呼び出す」ようなことはできないのでしょうか。
それが static メソッドです。
static メソッドはインスタンスメソッドを呼び出すことはできませんし、インスタンスフィールドにアクセスすることもできません。
同じファイルに書くため紛らわしいのですが、static のついたクラスのための定義と、それ以外のインスタンスのための定義は別物ということに気をつけましょう。
insertCoin100() メソッドを呼ぶときに totalAll に対して加算を行っています。
これはインスタンス自身ではなく、static というクラスのための領域へ加算しています。
この機能を使うことで、各インスタンスに金額が追加されたときに全体の貯金額を更新することができるようになります。
new から生まれたインスタンスに呼びかけることはできず、クラスに属したメソッドとなるため、呼び出すときには以下のようになります。
SavingsBox.getAllAmount();
拡張 for 構文
最後に、Main で以前にも紹介した Scanner と ArrayList を使って複数のインスタンスに対して入力を与えて振る舞いを確認できる CUI を用意しました。
今回はおまけとして、拡張 for 構文というものを紹介します。
for(SavingsBox box : boxes) { System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)"); }
今までの方法であれば以下のように書いていたと思います。
for(int i = 0; i < boxes.size(); i++) { System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)"); }
このようなインデックスを使って 0 から数える記述は、上限が size() などではなくもっと曖昧なものだった場合に上限を超えてエラーになる危険性などが考えられます。
それに対しての拡張 for 構文は、リストの中にある要素を展開するという手法で、どちらがよいという話ではありませんが、全要素を参照したい場合にはこのような書き方ができるということを紹介しておきます(拡張 for 構文は JDK 5.0 以上で使用できます)。
クラスの作り方の基本
これまでは、Javaのコアライブラリにあらかじめ用意されているクラスを使うだけでした。しかし、Javaのプログラミングは本来、自分の手で必要なクラスを作り、それを組み上げて1つのソフトウェアを作り上げるものです。分かりやすく使いやすい、良いクラスを作れるようになれば、あなたも一人前のJavaプログラマーです。
今回は、クラスを作るための基本を学びましょう。
空っぽのクラスを作る
『Javaプログラミングの基本』の回では、貯金箱のプログラムを、Javaのコアライブラリで用意されているArrayListクラスを使って書きました。ここでは、そんな代用品を使わないで済むように、ちゃんとした「貯金箱」のクラスを作りましょう。
クラスを作る初めの一歩は、名前を決めることです。
#Javaでは、クラスやメソッド、変数などに日本語の名前を付けても良いのですが、ここではすべて、英語の名前を付けることにしましょう。もし将来、あなたの作ったプログラムが、世界中の人に使われることになっても大丈夫なように。。。
ここでは「貯金箱」を作るのですから、名前は「SavingsBox」とします。
#オブジェクト指向プログラミングでは、クラスやオブジェクトは、抽象的な意味での「もの」を表しています。ですから、クラスの名前は、たいていは名詞になります。
では、次のようなソースファイルを書いて、「SavingsBox.java」という名前のファイルに保存してください。
class SavingsBox {
}
#Javaでは、ファイル名は何でも良いのですが、クラス名と一致させておくのが良いでしょう。このソースファイルをコンパイルすると、「SavingsBox.class」というクラスファイルが生成されます。クラスファイルの名前は、クラス名と一致したものになりますから、ソースファイルも、同じ名前にしておいた方が無難です。
※ここでは、クラスのアクセス修飾子を付けていないので、クラス名と異なるファイル名を付けられました。ですが、publicなクラスの場合は、クラス名とファイル名は一致しなければなりません。
このクラスはまだ空っぽで、何もできませんが、正真正銘、世界で唯一のオリジナルのクラスです。
メソッドを決める
クラスの名前が決まったら、次は、そのオブジェクトをどうやって使うか、何ができるのか、を考えます。
Javaでは、オブジェクトに対してメソッドを呼び出すことで、オブジェクトを操作できます。このSavingsBoxクラスは空っぽですから、このままでは何もできません。貯金箱として使えるように、メソッドを作る必要があります。
ここでは例として、
- 100円玉を入れる。
- 500円玉を入れる。
- 合計金額を調べる。
という、3つの操作ができるように、3つのメソッドを作ることにしましょう。
1つのメソッドを作るには、下記の3点を決める必要があります。
- メソッドの名前。
- 引数に何を渡すか。
- 戻り値に何を返すか。
では、まずは「100円玉を入れる」メソッドについて、この3点を決めましょう。
メソッドの名前は、「100円玉を入れる」を英訳して、「insertCoin100」としましょう。
#メソッドは、オブジェクトに対して何かを行うものなので、基本的に、動詞から始まる名前を付けます。Javaでは慣例として、先頭の単語はすべて小文字で、2番目以降の単語は先頭のみを大文字にした名前を付けることが多いです。
引数は、無くても良いのですが、それだと、一度に1枚ずつしか入れられません。そこで、一度に何枚も入れられるように、100円玉の枚数を渡すことにしましょう。戻り値は、何も無しとします。
このメソッドを追加したソースファイルは、下記の通りになります。
class SavingsBox { // 100円玉を入れる。 public void insertCoin100 ( int coins ) { } }
括弧の中に、引数を書きます。「int」型、つまり、整数の値を引数に渡す、という意味になります。引数で渡された値は、coinsという変数に格納されます。この変数の名前は、自由に付けられます。
戻り値は、メソッド名「insertCoin100」の前に書きます。ここでは「void」と書いてありますね。「void」とは、何も無い、ということを表す特別なキーワードです。ここでは、メソッドの戻り値が無い、という意味になります。
先頭に「public」というキーワードがあります。これは、アクセス修飾子と呼ばれるもので、このメソッドを誰が使えるか、を表しています。詳しくは、少し後でまた説明します。
同じように、「500円玉を入れる」メソッドと、「合計金額を調べる」メソッドも作りましょう。
class SavingsBox { // 100円玉を入れる。 public void insertCoin100 ( int coins ) { } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { } // 合計金額を調べる。 public int getAmount ( ) { } }
「合計金額を調べる」メソッドは、引数が無い代わりに、戻り値が「int」になっています。つまり、このメソッドを呼び出すと、合計金額が整数の値で戻ってくることになります。
さて、この3つのメソッドがあれば、100円玉と500円玉を入れる貯金箱として使えそうです。例えば、こんな風にプログラムを書いてみたらどうでしょう。
SavingsBox box = new SavingsBox(); box.insertCoin100(3); box.insertCoin500(2); int total_yen = box.getAmount(); System.out.println(total_yen);
100円玉を3枚、500円玉を2枚入れましたので、最後には「1300」と出力されるはずですね。
ですが、ここではまだ、メソッドの中身が空っぽですから、もちろん、このままでは動きません。この通りに動くように、メソッドの中身を作る必要があります。
#この時点では、コンパイルが通らなくなっています。
フィールドを用意する
ここまでは、このSavingsBoxクラスを使う利用者の立場に立って、クラス名やメソッドを決めてきました。ここからは、クラスの内部に目を向けます。
さきほど、100円玉を3枚、500円玉を2枚入れると、最後には「1300」と出力されるはずだ、と言いました。つまり、SavingsBoxクラスのオブジェクトは、貯金箱に100円玉や500円玉を入れると、それを覚えていてくれます。
オブジェクトが覚えているものを、フィールドとして定義します。フィールドとは、オブジェクトの中に入っている値や別のオブジェクトのことです。
SavingsBoxクラスでは、貯金箱に入れたお金の合計だけを覚えておけば良さそうです。そこで、フィールドとしては、合計金額を表す整数の値を1つだけ、用意することにします。
フィールドは、変数と同じように書きます。名前は「total」としましょう。
#C++言語では、フィールドのことを「メンバー変数」と呼びます。なお、メソッドのことは「メンバー関数」と呼びます。
class SavingsBox { // 合計金額 private int total; // 100円玉を入れる。 public void insertCoin100 ( int coins ) { } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { } // 合計金額を調べる。 public int getAmount ( ) { } }
int型のフィールド「total」が定義されました。その前に、「private」というキーワードが付いています。これもアクセス修飾子です。これについても、後ほど説明します。
メソッドの中身を作る
フィールドを用意したら、次はメソッドの中身を作ります。
100円玉を入れたり、500円玉を入れた時は、フィールド「total」に金額を足していけば良いですね。また、「合計金額を調べる」メソッドは、フィールド「total」の値を、そのまま返せば良いです。
この通りにメソッドを書くと、次のようになります。
class SavingsBox { // 合計金額 private int total; // 100円玉を入れる。 public void insertCoin100 ( int coins ) { if (coins > 0) { total += coins * 100; } } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { if (coins > 0) { total += coins * 500; } } // 合計金額を調べる。 public int getAmount ( ) { return total; } }
「coins」には、メソッドが呼び出された時に、引数として渡された値が格納されています。「coins」は硬貨の枚数ですから、100円玉ならそれに100円を、500円玉なら500円を掛けて、合計金額に加えています。
なお、「coins」の値が0より大きいことをチェックしているのは、ちょっとした配慮です。マイナスの値を渡された時に、合計金額が減ってしまうのは、貯金箱の動きとしてはおかしいですからね。このように、クラスを作る時は、どのように利用されても良いように、いろいろなケースを想定して配慮する必要があります。
getAmountメソッドは、フィールドtotalの値をそのまま返しています。メソッドが戻り値を返す時は、「return」というキーワードを使います。
コンストラクタを作る
これで貯金箱のクラスはほぼ完成なのですが、1つ疑問があります。100円玉も500円玉も、どちらも1枚も入れずに、合計金額を調べるメソッドを呼び出すと、いくらになるのでしょうか?
実は、Javaでは、変数やフィールドは、自動的に初期化されます。int型であれば、必ず初期値は0になっています。ですから、硬貨を1枚も入れなかった時は、合計金額は0円になります。
この場合はこれで良いのですが、もし、初期値を0以外にしておきたいのであれば、コンストラクタと呼ばれる特別なメソッドを用意して、初期値をセットする必要があります。
コンストラクタは、キーワード「new」でオブジェクトが作成される時に、自動的に呼び出されるメソッドです。ふつうのメソッドと異なり、戻り値はありませんが、必要であれば、引数を持たせることができます。なお、コンストラクタの名前は、クラスの名前と同じにしなければなりません。
SavingsBoxクラスでもコンストラクタを用意すると、次のようになります。
class SavingsBox { // 合計金額 private int total; // コンストラクタ public SavingsBox ( ) { total = 0; } // 100円玉を入れる。 public void insertCoin100 ( int coins ) { if (coins > 0) { total += coins * 100; } } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { if (coins > 0) { total += coins * 500; } } // 合計金額を調べる。 public int getAmount ( ) { return total; } }
これで、貯金箱のクラスは完成です。次のサンプルプログラムを動かしてみてください。
class MyFirstProgram { public static void main(String[] args) { SavingsBox box = new SavingsBox(); box.insertCoin100(3); box.insertCoin500(2); int total_yen = box.getAmount(); System.out.println(total_yen); } }
ちゃんと「1300」と出力されます。
アクセス修飾子とカプセル化
最後に、アクセス修飾子についてお話ししておきましょう。
ここまで、「public」と「private」という、2種類のアクセス修飾子が出てきました。アクセス修飾子は、メソッドやフィールドを、誰が使えるか、を表したものです。「public」は、このクラスの外からでも使えるものを、「private」は、このクラスの中でしか使えないものを表しています。
#この説明は厳密には正しくない。後の章で詳しく解説する予定。
貯金箱のクラスでは、メソッドには「public」を、フィールドには「private」を付けていました。つまり、SavingsBoxを使う人は、メソッドを呼び出すことはできますが、フィールドを直接使うことはできません。
もしも、totalフィールドも、アクセス修飾子をpublicにしたら、どうでしょう。すると、次のようなことができるようになってしまいます。
SavingsBox box = new SavingsBox(); box.insertCoin100(3); box.total = 12345; // フィールドの値を直接書き換える。 int total_yen = box.getAmount(); System.out.println(total_yen);
ですが、これでは、100円玉を入れたり、500円玉を入れるメソッドを用意した意味がありません。いくらでもやりたい放題になってしまいます。マイナスの値だって入れられてしまいます。
貯金箱の中のお金を好き勝手に書き換えられないようにして、『100円玉と500円玉だけを入れられる貯金箱である』という意味を明確にするために、totalフィールドのアクセス修飾子をprivateにして、直接値を書き換えられないように制限をしているのです。
オブジェクトの操作方法を制限するのには、クラスの意味を明確にすることの他に、クラス内部の作りを隠蔽する、という意味もあります。
例えば、このSavingsBoxクラスは、次のように書いても、まったく同じように動きます。
class SavingsBox { // 100円玉の枚数 private int total100; // 500円玉の枚数 private int total500; // コンストラクタ public SavingsBox ( ) { total100 = 0; total500 = 0; } // 100円玉を入れる。 public void insertCoin100 ( int coins ) { if (coins > 0) { total100 += coins; } } // 500円玉を入れる。 public void insertCoin500 ( int coins ) { if (coins > 0) { total500 += coins; } } // 合計金額を調べる。 public int getAmount ( ) { return total100 * 100 + total500 * 500; } }
さきほどまでのSavingsBoxクラスのソースファイルと比べると、フィールドはまったく別物に変わっていますし、すべてのメソッドの中身も書き換えられてしまっています。ですが、このクラスを利用する人は、その違いを気にする必要はありません。何故なら、メソッドの呼び出し方は変わっていませんし、結果も同じだからです。
もしも、フィールドのアクセス修飾子をpublicにしていたら、どうでしょう。誰かが、totalフィールドを直接書き換えるようなプログラムを書いていたかもしれません。すると、SavingsBoxクラスを書き換えてしまうと、それを使っている他の人のプログラムまで、すべて書き直さないといけない羽目になってしまいます。
このように、アクセス修飾子を使って、クラスを利用する人に見せる部分を制限し、見せる必要がない部分を隠蔽することを、カプセル化と呼びます。
#「カプセル化」は、データと手続きを一体化する、という意味もあるので、ここの記述は適切ではないかもしれない。
すべてをpublicにした方が自由に使えて便利に思えるかもしれませんが、そうではないのです。カプセル化は、オブジェクト指向プログラミングではとても重要な考え方ですので、ぜひ身に付けてください。
※アクセス修飾子について、Javaではクラスレベルでの制限になりますが、他のオブジェクト指向言語(Smalltalk, Eiffel, Ruby)では、インスタンスレベルでの制限になるそうです。例えば、Javaでは、同じクラスのインスタンスであれば、他のインスタンスのprivateメンバーを参照できますが、Smalltalk, Eiffel, Rubyでは、同一クラスのインスタンスでも、他のインスタンスのprivateメンバーにはアクセスできないそうです。
いろいろな計算処理(アルゴリズム)を書いてみる
実は,繰返し処理と条件分岐さえあれば,どんなプログラムでも書くことができるようになります.
# 正確には計算可能なものだけ.たとえば,任意のプログラムを入力として受け付け,それが終了するかしないかを判定するプログラムは(たとえどのようなプログラミング言語を用いても)書くことができません.
さて,せっかく条件分岐と繰返し処理を学んだので,単に入力された値を表示するといった単純なものではなく,もっとコンピュータに計算をさせるようなプログラムをいくつか書いてみることにしましょう.
最大公約数を求める
まずは,小学校のころを思い出して,与えられた2つの数の最大公約数を求めるプログラムを作成してみることにします.最大公約数とは2つの数を割り切る最大の数なので,たとえば,60と24という2つの数が与えられたとき,最大公約数は12になります.
さて,どのようにプログラムを書けばよいでしょうか?
まず,思い浮かぶ簡単な方法は,最大公約数の候補を適当に生成して,それらが与えられた2つの数を割り切るかどうかひとつずつ試していく,というものです.ようするにコンピュータの力を利用して力任せに問題を解くという手法(brute-force)です.最大公約数を求める問題の場合,与えられた2つの数のうち,小さい値からはじめて,1つずつ数を減らしていきながら,割り切れるかどうか調べていく,というものになります.
// // 力任せ法(brute-force)によって最大公約数を求める // public class GCD { public static void main(String[] args) { // 入力された文字列を整数に変換する int p = Integer.parseInt(args[0]); int q = Integer.parseInt(args[1]); int d = Math.min(p, q); // pとqのうち小さい方を返す for (/* 初期化コードは省略 */; d >= 1; d--) { if (p % d == 0 && q % d == 0) break; } System.out.printf("%dと%dの最大公約数は%dです\n", p, q, d); } }
このプログラムは,これまで紹介した標準入力からではなく,コマンドライン引数から入力を受け取ります.コマンドライン引数とは,プログラムの起動時に与えるパラメータのことで,たとえば,上のプログラムの場合,以下のように2つのパラメータを与えてプログラムを起動します.
$ java GCD 60 24 60と24の最大公約数は12です
ここで与えた60と24という数は,mainメソッドの引数であるargsという変数に渡されます.argsは文字列の配列型(String[])の変数で,args[0]に1目のコマンドライン引数である60が,args[1]には2番目の12が文字列として格納されます.つぎに,渡された文字列を整数値に変換してやる必要があります.これは,Integer.parseInt(args[0])の部分です.こうして,あらためて整数値としての値を得て,それらでpとqの変数を初期化しているのです.そして,pとqのうち小さい方を最初の候補として変数dを初期化します.(Math.min(p, q)の部分)
あとは,dの値を1つずつ減らして(d--)いきながら,dがpとqを割り切るかどうかを調べていくわけです.p % dはpをdで割ったときの余りを求めます.したがって,p % d == 0は余りが0かどうかの判定,すなわち,pがdで割り切れるかどうかを調べています.
プログラムをより堅牢にする
さて,上で紹介したプログラムには実はいくつか問題点があります.たとえば,以下のようにプログラムに与えるコマンドライン引数を1つだけにして起動したら,何が起こるでしょうか?
$ java GCD 60
筆者の環境では,以下のようなメッセージが表示されてしまいました.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1 at GCD.main(GCD.java:8)
これは,プログラム(GCD.java)の8行目で,ArrayIndexOutOfBoundsExceptionという例外が発生したことを示しています.この例外は,配列の範囲外の要素にアクセスしたときに発生します.このケースでは,コマンドライン引数が1つしか与えられなかったにもかかわらず,2つ目の要素(args[1])にアクセスしたため,Javaの実行時システムが例外を投げた(例外を発生させることを,例外を投げる,と言います)のです.
さきほどのプログラムは,コマンドライン引数を2つ受け取ることを想定して書いたので,このように実際は1つだけしか与えられなかった,といったケースに対応していませんでした.この問題に対処する方針としてはいくつか考えられます.
- どうせ,1つの数に対する最大公約数なんてあまり意味がないので,とりあえず気にしない.
- 入力をチェックして,2つの引数が与えられなかった場合,適当なエラーメッセージを出して終了する.
- 1つ以上(2つでも3つでも4つでも...)の数に対しても最大公約数を求められるようにアルゴリズムを修正する.
どの対処方法が正しいかどうかは,プログラムの使われ方によってかわってきます.個人的に使うプログラムで例外が発生しても気にしなくて済むならば,1のアプローチが簡単です.でも,自分の作ったプログラムを他人に使ってもらうような場合,突然例外が発生してプログラムが終了してしまったら,その人は驚いてしまうことでしょう.3番目のアプローチは最も汎用的ではありますが,少々難しいし,そこまでやる必要はないかもしれません.ここでは2番目のアプローチに沿って,プログラムを修正してみることにしましょう.
以下のように,argsの長さをチェックして,2以外ならユーザに対してエラーメッセージを出して終了させるのが簡単です.
public class GCD { public static void main(String[] args) { if (args.length != 2) { System.out.println("入力パラメータの数は2つでなければなりません"); return; } // 入力された文字列を整数に変換する ...
return文は,メソッドの実行を途中で終了し,メソッドの呼び出し元に処理を戻す命令です.mainメソッドの中でreturn文が実行されると,プログラムが終了します.
これで,少しだけプログラムが親切になりました.
$ java GCD 60 入力パラメータの数は2つでなければなりません
でも,まだ問題が残っています.以下のように数値ではない入力を与えたらどうなるでしょう?
$ java GCD foo bar
筆者の環境では,以下のような例外が発生しました.
Exception in thread "main" java.lang.NumberFormatException: For input string: "foo" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:66) at java.lang.Integer.parseInt(Integer.java:485) at java.lang.Integer.parseInt(Integer.java:520) at GCD.main(GCD.java:11)
これは,Integer.parseInt(args[0])の処理に失敗して,NumberFormatExceptionという例外が発生したことを示しています.つまり,fooという文字列は整数値に変換できませんでした,ということです.
ここでは,発生した例外をキャッチしてやることで,この問題に対応することにしましょう.そのためにはtry〜catch構文を用います.
public class GCD { public static void main(String[] args) { ... // 入力された文字列を整数に変換する int p, q; try { p = Integer.parseInt(args[0]); q = Integer.parseInt(args[1]); } catch (NumberFormatException e) { System.out.println("入力された文字列を整数に変換できません"); return; } int d = Math.min(p, q); ...
tryの{と}で囲まれた文の中で何らか例外が発生した場合,その例外とマッチするcatch節の部分に実行が移ります.ここでは,NumberFormatException例外が発生した場合に,catch (NumberFormatException e) {以下の処理に実行が移ることになります.これが例外のキャッチです.
実行結果は以下のようになります.
$ java GCD foo bar 入力された文字列を整数に変換できません
このように,いろいろな入力ケースに対して,プログラムがきちんと動作するようにするには,適切に例外をキャッチしてやることが必要です.特に入出力を扱うような場合,外部の環境によってどのような値が渡されるか,プログラムを書いた時点では予想できないことがありますので,このような処理は堅牢なプログラムを作る上で重要になります.
# ライブラリのメソッドがどのような例外を返すかどうかは,ライブラリのリファレンスマニュアルに記載されています.
さて,これでどんな入力でも対応できるGCDプログラムが完成したのでしょうか?
実は,まだまだ想定していないケースがあります.たとえば,0の値や負の値をパラメータとして与えた場合です.
たとえば,5と0の最大公約数や,-12と30の最大公約数はどんな値になるのでしょうか?この問題に対処するには,そもそも,そういった数に対する最大公約数はどういった値にするべきなのか,といった仕様をきっちりと定義してやる必要があります.あるプログラムが正しいかどうかはそれだけでは判断できなくて,あくまでも仕様に対して,そのプログラムが正しいかどうか,といった相対的な判断しかできません.たとえば,さきほどのプログラムの仕様として,「引数は正整数のみとし,それ以外の入力が与えられた場合は未定義の値を出力する」というものだったらば,プログラムは正しいことになります.もし,0や負の値に対しても最大公約数を求めたいのであれば,まずは入力が負や0に対する妥当な仕様を策定し,それにしたがってプログラムを修正する必要があるのです.これについては,読者に対する課題とします.
いずれにしても,きちんとしたプログラムを書く,というのは実は大変なことだということがわかります.本職のプログラマたちも,単に動くだけのプログラムを書くのではなく,いろんな状況に対してもきちんと動作する,ことを常に心がけており,そのために日々格闘しているのです.
# 実はそれだけではありません.どうやったら,より柔軟性の高いプログラムになるか,拡張性が高くなるか,テストがしやすいか,実行効率が良くなるか,セキュリティが高くなるか,可読性が高くなるか,などなど,様々な観点からプログラムの品質を高める努力をしています.
# 本連載では,そのための考え方や手法について,折にふれて紹介していけたらと考えています.
Euclidの互除法による解
さて,前節では,どうやって堅牢なプログラムを作るか,といった観点から最大公約数のプログラムを改良していきましたが,最大公約数を求めるアルゴリズムについては,何も変更しませんでした.ここでは,別の観点として,最大公約数を求めるより賢いアルゴリズムを紹介したいと思います.
実は,最大公約数を求めるには,Euclidの互除法というアルゴリズムが古くからよく知られています.これは,2つの整数pとqがあり,rをpをqで割ったときの余りとしたときに,pとqの最大公約数とqとrの最大公約数は常に等しくなる,すなわち,GCD(p,q)=GCD(q,r)という関係を利用したアルゴリズムです.
この関係を使えば,64と28の最大公約数は以下のようにもとめることができます.
GCD(64,28) = GCD(28,8) // 64を28で割ったあまりは8 = GCD(8, 4) // 28を8で割ったあまりは4 = GCD(4, 0) = 4 // 8を4で割ったあまりは0,割り切れたので答えは4
この考え方を使って,GCDのプログラムを書くと次のようになります.
// // Euclidの互除法によって最大公約数を求める // public class GCD { public static void main(String[] args) { // 入力された文字列を整数に変換する int p = Integer.parseInt(args[0]); int q = Integer.parseInt(args[1]); for (;;) { int r = p % q; if (r == 0) break; p = q; q = r; } System.out.printf("%sと%sの最大公約数は%dです\n", args[0], args[1], q); } }
このアルゴリズムは,先に紹介した力任せによる手法よりもずっと早く解を見つけ出すことができます.
すなわち,力任せ法では,解の候補となる値を1つずつ減算して調べていったのにたいして,Euclidの互除法では,余りを求めながら(繰り返しの処理ごとにrの値はどんどん小さくなります)調べていってるので,より早く,解にたどりつくわけです.したがって,このアルゴリズムはさきほどのアルゴリズムよりも(処理速度の面から)効率が良い,といえます.
より本格的なプログラムを作成するために,プログラマは様々なアルゴリズムについて知っている必要があります.
# 大学の情報系学部で最初の学期に受ける講義はたいてい「アルゴリズムとデータ構造」というタイトルの授業で,プログラミングの基本となる様々なアルゴリズムや重要なデータ構造について学びます.
本連載では,折にふれて重要なアルゴリズムやデータ構造,なども紹介していく予定です.(ただし本格的には専門の教科書や参考書を使って勉強してください)
もちろん,典型的なアルゴリズムはライブラリとして実装されていることが多く,実際の開発においてプログラマが直接,これらのアルゴリズムを実装することはほとんどありませんが,アルゴリズムを多く学び,そのアイディアや概念を学ぶことによって,プログラミングの腕が上がるのは間違いありません.いろんな場面で応用がきくようになります.
# わたしが学生時代のアルゴリズムの先生は「よりアルゴリズムを見つけるには,問題の背後にある良い(数学的)構造を見つけることがポイントである」と常日頃からおっしゃってました.たとえば,GCDの例でいうと,GCD(p,q)=GCD(q,r)という良い構造に着目したおかげで,Euclidの互除法という効率良いアルゴリズムが得られたのです.
# すべての問題に対して,このように効率の良いアルゴリズムが存在するかというとそうではありません.たとえば,NP完全問題と呼ばれる範疇に含まれる問題は,本質的に力任せ法による解法しかないので,解くのに非常に時間がかかります.
繰返しと例外処理
前回は,if〜else構文を用いた条件分岐の処理について学びました.条件分岐によって,単に一つ一つの文を順番に実行するだけでなく,ある条件によって処理の流れを変えることができるようになりました.今回は,さらに一歩進んで繰返しの処理を学ぶことにします.たとえば同じ処理を何度も行なう場合などは,繰返しを使うことによって,まとめて書くことができるようになります.さらには,より複雑な処理を実行したり,本格的な計算を行なったりすることもできるようになります.
for文による繰返しの処理
まずは,具体的な例から見ていくことにしましょう.前回,プログラムの入力についても学びました.そこで紹介したHelloCUIプログラムでは,文字列を配列の要素に繰返し入力しながら,最後にその中身を出力するというものでした.ここではそのプログラムを,繰返しを使って書き直してみることにします.
import java.util.ArrayList; // java が提供しているライブラリ、リスト。 import java.util.Scanner; // java が提供しているライブラリ、スキャナ。 public class HelloCUI2 { public static void main(String[] args) { System.out.println("ここに5つの要素(テキスト)が入るリストがあります。"); System.out.println("何か文字列を入力していってみてください。"); Scanner scanner = new Scanner(System.in); ArrayList<String> list = new ArrayList<String>(); // String が入るリストです。 for (int i = 1; i <= 5; i++) { System.out.printf("%dつ目>", i); list.add(scanner.nextLine()); } System.out.print("リストには以下の5つの要素が挿入されました。"); System.out.println(list.toString()); } }
前と違っている部分は,文字列の入力処理の部分をfor文を使ってまとめている部分です.
for文の基本構文は以下のようになっています.
for (初期化コード ; 条件判定式 ; 更新コード) 文
for文の実行が開始されると,まず初期化コードが実行されます.先ほどの例では,初期化コードの部分はint i = 1であり,整数型の変数iを宣言し,それを1で初期化しています.つぎに,条件判定式が評価されます.この判定式が真ならば,for文の中の文が実行され,偽ならば繰返しを終了(for文を抜けるといいます)します.上の例でいうと,初回はi=1なので,条件式i<=5が真なので,for文の中身である{と}で囲まれたブロック文が実行されることになります.for文の中の文の実行が完了すると,つぎに更新コードが実行されます.上の例ではi++の部分になります.i++の意味は「iの値を1つだけ増やしなさい」ということです.したがって,この文を実行するとiの値は2になります.そして,再び条件判定式が評価されます.相変わらずi<=5が成立します(ある条件式を評価した結果が真になることを,成立する,と言うこともあります)ので,再びforの中のブロック文が実行されます.そして,更新コードが実行sあれ,条件判定式が評価され...,といったことをiの値が1,2,3,4,...と増えながら繰り返されることになります.さて,iの値が6になると何が起こるのでしょうか?i <= 5という条件式が成立しなくなるので,for文の実行を終了します.そして,その後の処理である,System.out.print("リストには...に実行が移ります.以上がfor文の処理の流れになります.
さて,さきほどのHelloCUI2では,実はもう1つ変更した部分がありました.以下の部分です.
System.out.printf("%dつ目>", i);
printfはフォーマット付きの出力を行なうメソッドです.%dは整数の値を十進数(dはdecimalのdです)の書式で出力するように指定する記述子です.ここでは,printfメソッドの2つ目の引数であるiの値を十進数で出力します.
# ここで出てきた「メソッド」や「引数」といった用語はそのうち回を改めてきちっと説明する予定です.
繰返しの回数を変えてみる
さて,先ほどのプログラムは繰返しの回数が5回と決まっていました.これでは面白くないので,"END"という文字列が入力されるまで,ひたすら繰り返すようにプログラムを改造してみましょう.
import java.util.ArrayList; // java が提供しているライブラリ、リスト。 import java.util.Scanner; // java が提供しているライブラリ、スキャナ。 // // ENDという入力があるまで,リストに対して文字列の入力を繰り返す // public class HelloCUI3 { public static void main(String[] args) { System.out.println("ここに要素(テキスト)が入るリストがあります。"); System.out.println("何か文字列を入力していってみてください。"); System.out.println("ENDと入力するとプログラムは終了します。"); Scanner scanner = new Scanner(System.in); ArrayList<String> list = new ArrayList<String>(); for (int i = 1; /* 条件式が空になっている */ ; i++) { System.out.printf("%dつ目>", i); String line = scanner.nextLine(); if (line.equals("END")) { break; } list.add(line); } System.out.printf("リストには以下の%dつの要素が挿入されました。", list.size()); System.out.println(list.toString()); } }
重要な部分は,まずfor文の条件式の中が空っぽ(コメントのみ)になっているところです.for文の条件式は省略することができ,その場合,常に条件式が成立するとみなされます.すなわち,for文の中がいつまでも繰り返されることになります.つぎに,for文を抜けるための条件文が文字列の入力の後に追加しました.以下の部分です.
String line = scanner.nextLine(); if (line.equals("END")) { break; }
まず,入力した文字列をいったんlineという変数に代入しておきます.lineに対してequalsという文字列が等しいかどうかを判定するメソッドを呼び出しています.もし,lineに代入された文字列がENDだったら,line.equals("END")は真になり,if文が成立します.そしてif文の中のbreak文が実行されることになります.break文が実行されると,現在の繰返しを抜け出します.
ここで,if (line == "END") {としていないことに注意して下さい.こうしてしまうと,たとえENDという文字列が入力されてもif文は成立しません.したがって永久にループから抜けられなくなります.(無限ループといいます)
# プログラムが万が一無限ループに陥ってしまって終了させることができなくなった場合,慌てず騒がずCtrl+Cを押しましょう.強制的にプログラムを終了させることができます.
line == "END"が真にならない理由は,オブジェクトの等価性について学ぶと理解できるようになります.簡単に説明すると,line == "END"の場合,lineという文字列オブジェクトと"END"という文字列オブジェクトが同一のオブジェクトである場合に真になります.一方,line.equals("END")の場合,文字列オブジェクトの中身(すなわち文字列)が"END"と等しい場合に真になります.この違いは,同一のオブジェクトとは何かという話にもつながってきます.100円玉を例に取ると分かり易いかもしれません.2つの100円玉があったとします.これらは,異なるオブジェクト(この100円玉とあの100円玉は違いますよね)です.でも100円という価値(中身)は同じです.同じオブジェクト(100円玉)かどうかを判定するのが==であり,同じ中身かどうかを判定するのがequalsであるといえます.
入力と条件分岐
先週記事が投稿されていない件について。
3時間ほどかけて記事を書いて投稿したら「メンテ中」ですという WEB ブラウザからの返事とともに、すべてが消滅するという初歩的ミスをしてしまいました;
さて本題。前回まではプログラムに書かれているものが一方的に出力されるだけでしたが、今回はユーザ側からプログラムに対して何か入力するということをできるようにしていきましょう。
今回学ぶ考え方は「入力」と「条件分岐」です。
いろいろな Java の機能を合わせて紹介しますが、あくまで上記の概念を表現するための機能であるということを念頭に置いてください。
ユーザインターフェース
ユーザインターフェースとは、ユーザが対象を操作する手段(入力)とユーザが操作した結果対象が生成したものを提示する手段を提供するものを指します。
今まではコマンドラインベースで、文字列が出力されてきましたが、キーボードからの文字列を受け付けるようにしたプログラムの作成が今回のお題です。
キーボードで文字列を入力し、ディスプレイに文字列を出力するインターフェースを、キャラクタユーザインターフェース (CUI) と呼びます。
対して、マウスでボタンをクリックしたりしながら進めるユーザインターフェースは グラフィカルユーザインターフェース (GUI) と呼びます。
# 余談ですが、分かりやすいインターフェースデザインというのを考えるのは難しいもので、たとえば「誰のためのデザイン」という書籍を読むとマウスとキーボードでの操作の差などを知ることができます。
入力入門 (オウム返し)
import java.util.Scanner; // java の機能として、Scanner というライブラリが用意されているので使うことにします。 public class HelloCUI { public static void main(String[] args) { System.out.print("なにかメッセージを入力してください>"); Scanner scanner = new Scanner(System.in); // System.in(標準入力) をスキャンしたスキャナを用意します。 String message = scanner.nextLine(); // scanner に1行メッセージスキャンしてもらいます。それを message 変数に入れておきます。 System.out.println("あなたが入力したメッセージは以下ですね?"); System.out.println(message); // out に message を println してもらいます。 System.out.println("以上、オウム返しプログラムでした。"); } }
コメントの使い方
今回はソースコード中にコメントを使いました。
コメントとは、コンパイルするときには無視される部分で、難しい処理をしていてコードが読みにくいときなどに補足メッセージを入れておくことができます。
# とはいえ、「// scanner に1行メッセージスキャンしてもらいます。それを message 変数に入れておきます。」などは Java プログラマにとっては「読めば分かる」話なので、このようなコメントを書くことは冗長です。コメントを書きすぎて可読性を下げては本末転倒なので気をつけましょう。
// コメント文 (// より後ろが1行コメントとして無視されます)
Scanner による入力について
このプログラムを動かすと、入力待ち状態になり、キーボードからの入力を受け付けるようになったと思います。
そして、キーボードから入力されたメッセージに従って、出力するメッセージが動的に変わります。
# プログラミングの世界では静的・動的という言葉がよく使われますが、何度も聞くうちに慣れると思います。
Scanner というのは Java が提供している入力用ライブラリです。
System.in (標準入力) をスキャンしてくれるスキャナを用意することで、以後、スキャナに対して nextLine() というメッセージで「次の行を読んでくれ」と頼み入力を与えることができるようになっています。
安易に「入力するときには Scanner って書けばいいんだ」と思わないように気をつけてください。
大事なのは「プログラミングの世界には入力という概念がある」ということであり、Java では「Scanner というオブジェクトにメッセージをスキャンしてもらう」という形で入力を表現することが可能であるということです。
# C言語では scanf(), ruby では gets といった形で入力機能が提供されています。
リストへの要素の入力
キーワード: ArrayList<型>
import java.util.ArrayList; // java が提供しているライブラリ、リスト。 import java.util.Scanner; // java が提供しているライブラリ、スキャナ。 public class HelloCUI { public static void main(String[] args) { System.out.println("ここに5つの要素(テキスト)が入るリストがあります。"); System.out.println("何か文字列を入力していってみてください。"); Scanner scanner = new Scanner(System.in); ArrayList<String> list = new ArrayList<String>(); // String が入るリストです。 System.out.print("1つ目>"); list.add(scanner.nextLine()); System.out.print("2つ目>"); list.add(scanner.nextLine()); System.out.print("3つ目>"); list.add(scanner.nextLine()); System.out.print("4つ目>"); list.add(scanner.nextLine()); System.out.print("5つ目>"); list.add(scanner.nextLine()); System.out.print("リストには以下の5つの要素が挿入されました。"); System.out.println(list.toString()); } }
JavaDoc について
Java から提供されるオブジェクトとして、ArrayList や Scanner を使ってきました。
「オブジェクトにはメッセージで話しかけることができる」と何度も言ってきましたが、これらのオブジェクトに対してどんな風に話しかければ答えてくれるのでしょうか。
これからは、JavaDoc と呼ばれる API 仕様書で確認しながら使うようにしてみましょう。
# Java のプログラミングは JavaDoc でメソッドを確認して話しかけるのが基本です。
メソッドの概要というところを見ていきます。
ArrayList に対して要素を挿入するには add(element) と話しかければよいことが分かります。
ArrayList (Java Platform SE 6)
Scanner で現在行を読むには nextLine() を呼べばよさそうということが分かります。
[http://java.sun.com/javase/ja/6/docs/ja/api/java/util/Scanner.html:title=
Scanner (Java Platform SE 6)]
ArrayList の型指定について
前回、ArrayList というリストにコインを追加していく例を挙げましたが、今回はこのリストに好きな文字列を入力していく例です。
ArrayList というライブラリは、どんな要素が入ってくるのかを明示しておくことができます。
この例では、scanner.nextLine で得られるものは文字列(String)なので、ArrayList
これは書かなくても動くのですが、数字など String 以外の要素が入ってこないように Java コンパイラが未然に防いでくれる機能です。
安全であることが保障されるプログラムを書くことは基本になりますので、ArrayList を使うときには要素に何が入るかを明示するようにしていきましょう。
条件分岐 if/else
Scanner だけでは、一方的にプログラムに対して入力しているだけじゃないかという話になってきます。
プログラムと対話するために、もし○○なら△△する。そうでなければ××するといったことを表現したくなるものです。
このような「もしなら」という表現する機能 Java では if/else という構文で記述することが可能であり、他の様々なプログラミング言語でも、それぞれ記述方法は異なることもありますが、その機能は提供されていることが多いです。
以下はパスワード認証の例です。
import java.util.Scanner; public class HelloCUI { public static void main(String[] args) { String password = new String("HelloHello"); // パスワードは HelloHello ということにします。 System.out.print("パスワードを入力してください>"); Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); // 入力文字列とパスワードの比較を行います。 if(password.equals(input)) { System.out.println("認証に成功しました。"); } else { System.out.println("認証に失敗しました。"); } } }
もしパスワード(password)と入力文字列(input)が同じ文字列であれば認証に成功するというシナリオです。
文字列(String)はオブジェクトなのでメッセージを投げて比較確認を行うことができます。
password に対して、あなたは input ですか?と聞くと、一致していれば if の中に書いた処理を行い、異なれば else の中に書いた処理を行います。
import java.util.Scanner; public class HelloCUI { public static void main(String[] args) { String password = new String("HelloHello"); // パスワードは HelloHello ということにします。 System.out.print("パスワードを入力してください>"); Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); // 入力文字列とパスワードの比較を行います。 if(password.equals(input)) { System.out.println("認証に成功しました。"); } else if(input.length() > 10) { // input の長さは?それは10より大きい? System.out.println("ヒント:パスワードは10文字以内です。"); } else { System.out.println("認証に失敗しました。"); } } }
複数条件を書きたいときには、間に else if と書くことができます。
input.length() というのは、String オブジェクトに対して、あなたの長さはいくつですか?と聞くメッセージです。
そしてその結果を > という比較演算子で比較しています。数字はオブジェクトではなくリテラルだったので、メッセージで尋ねることができません。なので比較演算子という Java の機能で比較しています。
入力と制御の応用例
Scanner による入力と、変数、if/else 構文を覚えただけでもいろいろなことができるようになっています。
状態遷移という言葉がキーワードになります。
自動販売機
ある一定金額まではお金を投入し続けるように入力を何度も求めるようにします(入力状態とします)。もしある金額を超えたら飲み物を選ぶように促します。もしある飲み物が選択されたら、飲み物を出し、入れた金額から飲み物代を引いた金額をお釣りとして返します。
# オートマトンという言葉を調べてみると面白いかもしれません。
ゲームのフラグ
例えばシミュレーションゲームや RPG。
もしあるアイテムを手に入れたら次のマップに進めるようになります。
ボードゲームの CPU 戦
例えばオセロなどのボードゲーム。
「もしここに打たれたらここに打てば勝てる」という情報が全て書かれていれば次に打つべき手が自動的に決まることになります。
# if/else でルールベースを構築してしまうということですね。
ファンの制御
もし暑かったら空気を読んで涼しくしてくれるような知的なエアコンがあったらいいですね。
# ファジィ理論と呼ばれるものがあります。
if(部屋の温度が "非常に寒い") { fan.stop(); } else if(部屋の温度が "寒い") { fan.speedDown(); } else if(部屋の温度が "普通") { fan.speedKeep(); } else if(部屋の温度が "暑い") { fan.speedUp(); }