2016年度 発展プログラミング演習 第6講 図書館システム 4日目

目次

  1. 11. 利用者に本を貸し出す.
    1. 11-1. 貸し出し履歴を保存する型.
    2. 11-2. 貸し出し履歴を保存・参照する.
    3. 11-3. 貸し出し処理を実行する.
  2. 12. 利用者の貸し出し履歴を検索する
    1. 12-1. Mapの列挙子の取得状況.
    2. 12-2. 検索する.
  1. 本日のまとめ
  2. 図書館システムのまとめ
  3. 提出課題について

11. 利用者に本を貸し出す.

11-1. 貸し出し履歴を保存する型.

どのような型に保存すれば良いか

まず,履歴を保存しておくための変数を用意しなければいけません. そのため,履歴をどのような形式で保存すれば良いかを考えましょう. 履歴は,History型の変数が多数存在します. List型に入れるので良いでしょうか.もっと他に良い方法はないでしょうか.

どのような形式にすれば良いかは,どのように使われるかを考えましょう. 本が貸し出し中であれば,重複して貸し出しはできません. そのため,その本が貸し出し中であるか否かを簡単に調べられることが重要です. では,貸し出し履歴Historyが 本ごとに異なるListに格納されていると, 貸し出し中かの判断処理が楽に行えるようになるでしょう. このような用途のため,キーと値(バリュー)のペアでデータを保存する型が存在します. Mapです.

キーとバリューのペア

キーと値のペア

キーとバリューのペアとは,例えば,iPhoneやAndroidの 連絡先アプリを想像すると良いでしょう. 連絡先アプリは,人の名前をキーとして,その人に関する情報をバリューとして扱います. その人に関する情報とは,住所やメールアドレス,電話番号などのひとまとまりの情報です. このように,なんらかの情報をキーとして,それに別の情報を対応付けます. そして,キーとバリューのペアを一つのデータとして集合を扱うのがMapです. キーとバリューの型に制限はなく,どのような型であっても指定できます.

Mapの宣言方法

Map map1 = new HashMap();
Map map2 = new TreeMap();

さて,List型の変数に代入する値は, new List()ではなく,new ArrayList()で作成した値でした. Mapも,同じように,new Map()はコンパイルエラーとなります. そこで,HashMapを利用します. ハッシュコードを利用した Mapという意味です.他にも,木構造 を利用したMapであるTreeMapも存在します. 左図のようにどちらを利用しても構いません.また,これらを利用する時には,List の時と同じく,クラス宣言よりも前にimport java.util.*;もしくは, import java.util.Map;import java.util.HashMap;の記述が必要です.

Listは格納する型を指定しました.例えば,String型を格納するのであれば, List<String>のように指定しました. Mapも同じように指定しなければいけません. しかし,Mapはキーとバリューの2つの型を指定しなければいけません. ここでは,キーは本型(Book)とし,バリューは複数の History型の値です.複数のHistoryをどのように扱えば良いでしょうか.

複数の値を格納する変数といえば,List型です. ここでは,Historyが格納されます.すなわち,List<History>です. この型がバリューの型となるわけです.つまり,以下のような宣言を行えば良いわけですね. 一見ややこしいように見えますが,分解して考えましょう.

Mapの宣言の解説

Map<Book, List<History>> historyMap = new HashMap<Book, List<History>>();

まず,型がMapであり,変数名がhistoryMapです. また,Mapには,キーの型,バリューの型を指定しなければいけません. それが,Mapの直後の<>の間に書かれています. つまり,キーの型が,Bookであり,バリューの型がList<History>です. バリューの型が,この宣言をややこしくしていますが,バリューの型も分解してみれば, List型であり,History型を格納すると分解しましょう. また,右辺は,Mapの値を作成するために,new HashMap を実行しています.また,HashMapの後ろにも,HashMap に格納する型を指定する必要があります.しかし,これはMap の型と合わせる必要があるため,Map でのキーとバリューの型の指定と全く同じになります.

11-2. 貸し出し履歴を保存・参照する.

履歴を作成するメソッドを作成する.

MapListと同じくCRUD が行えます.作成(追加),読み取り,更新,削除です. それぞれ,次のメソッド呼び出しで可能です.

これらの操作を行う前に,履歴情報を保持する変数を宣言しましょう. 変数名は,historyMap,型はMap, 格納する型は,キーがBook,バリューがList<History>です. この変数をLibraryクラスのフィールドに,宣言しましょう.

次に,Historyを作成するメソッドであるcreateHistory 次に示す指示に従ってLibraryに追加しましょう.

createHistoryは引数に受け取った値を元にHistory 型の実体を作成して返すためのメソッドです.

貸し出し履歴を登録する.

さて,いよいよcreateHistoryメソッドで作成できたHistoryの値を Mapに登録します. Listの場合,値を格納するには,addメソッドを利用しました. Mapの場合は,putを利用します. putの引数は2つです.第1引数はキーとなる値,第2引数はバリューとなる値です. では,ユーザの誰かが,何らかの本を借りたとして,履歴を作成し,登録しましょう.

Boolean registerHistory(History history){
    List<History> histories = 
        historyMap.get(history.book);
    if(histories == null){
        histories = new ArrayList<History>();
        historyMap.put(history.book, histories);
    }
    if(this.canLend(history, histories)){
        histories.add(history);
        return true;
    }
    return false;
}
Boolean canLend(History history, 
                List<History> histories){
    if(!shelf.contains(history.book)){
        return false;
    }
    if(histories.size() > 0){
        History lastHistory = 
            histories.get(histories.size() - 1);
        if(lastHistory.isLent()){
            return false;
        }
    }
    return true;
}

また,左に示すcanLendメソッドとregisterHistoryメソッドを Libraryに定義しましょう. この2つのメソッドの内容をよく読んでください.非常に重要な内容が含まれています.

まず,registerHistoryメソッドを見てみましょう. メソッドの名前から,履歴(history)を登録する(register)メソッドであるとわかります. 登録の成否が返り値として返されることが名前,引数,返り値をみると予想できます. この予想がJavaプログラムを読み解く上で非常に重要です.

なお,実際にこの予想の通りです. registerHistoryは名前だけから判断すると,無条件で履歴を登録するように見えますが, 貸し出し中であったり,実際に履歴を登録できるかは調査しなければ判断できません. そのため,登録できたか否かをregisterHistoryの呼び出し元に返さなければいけません. そのために,返り値がBooleanになっています.

JavaはCよりもより大規模なプログラムが多くなります. そのため,メソッドの名前,返り値から処理の内容を予測することが, プログラムの全体像を掴む上で,重要なポイントになってきます. もちろん,詳細まで知るためには,メソッドの中身を読み解く必要があるのですが, 処理内容が予測できている場合は,そうでない場合に比べて,理解度が上がります. そのため,皆さんも,メソッドの名前から処理内容を予測しながら読み進めてみましょう.

具体的にメソッドの内容を見ていきましょう. 2, 3行目は,historyMapからキーを指定して, バリューを取得しています.キーは引数で与えられた履歴が含むBook 型の変数です.バリューは,その本の今までの履歴のリストです. 次に,5-6 行目は,今まで一度も貸し出しがなかった場合(履歴が一つも登録されていなかった場合) に行われる処理です. 履歴を格納するリストが作成されていないため,作成し(5行目), historyMapに新たな値として格納しています(6行目). そして,8行目で,貸し出し可能かを判定し,貸し出し可能であれば,9行目で 履歴のリストに引数のリストを追加しています.

次に,canLendメソッドを見てみましょう.このメソッドも 名前から,貸し出しができるか判定するのであろう,と予測できます. そして,返り値の型がBooleanですから, 貸し出し可能であれば,trueが返され, 貸し出しできなければ,falseが返されるのだろうと予想できます.

さて,メソッドの内容を見てみましょう.引数として,これから貸し出そうとしている History型の実体と,これまでの貸し出し履歴であるHistory のリストが渡されています. このリストに,とある本の今までの貸し出し履歴が格納されています. まず,canLendメソッドでは,16行目で, 貸し出そうとしている本が,この図書館システムが管理している本であるかを判定しています. リストに対して,containsメソッドを呼び出すことで, リストに含まれているかどうかを判定できます.

次に,19行目で履歴が存在しているかを判定しています. 履歴が存在しない場合は,今まで借りられたことがないため,貸し出し可能です. 履歴が存在する場合,20-24行目の処理が実行されます. 20, 21行目でリストの最後の要素を取得しています. 履歴は順に格納されるため,最後の要素が貸し出し中でなければ貸し出し可能です. 貸し出し可能かの判断は 22 行目で行っています.このメソッドは3日目練習問題2で実装しているはずです.

ちょっと難しいプログラムだったかもしれませんが, メソッドの名前から処理内容を予測し,一つのステップずつ見て行けば理解できると思います. パッと見てわからないと諦めるのではなく,ちょっとずつでも読み進めていきましょう.

なお,Mapに登録された情報を削除するには,removeを利用します. removeの引数には削除したいキーを指定します. すると,キーとそのペアはMapの登録から削除されます. 図書館システムでは履歴の削除は行いませんので,特に必要ありませんが, 情報として知っておくと良いでしょう.

11-3. 貸し出し処理を実行する.

さて,ようやく貸し出し処理が行えるようになりました.

貸し出し処理を行うlendメソッドをLibraryに作成しましょう. lendの引数はUserBook, 返り値はHistoryとします.そして,以下の処理をlend メソッドで書いていきましょう.

上で作成したlendメソッドを利用するrunLendメソッドを Libraryに定義し,runメソッドの最後に呼び出しましょう. runLendの引数はなし,返り値はvoidとします. runLend内で,5つの貸し出し履歴を作成しましょう. 借りる人,借りる本は何でも構いませんが,少なくとも1回は貸し出しに失敗し, 最終的に4つの本が借りられている状態にしてください.

貸し出し結果を確認するため,System.out.println メソッドでhistoryMapを出力してみましょう. どのような結果になるでしょうか. 履歴をいくつか登録してみるとどのような結果になるでしょうか.

なお,lendメソッドは,Hisotryを返していますが, これは,何のためでしょうか. runLend内では,lendメソッドの返り値については特に指示していませんから, 返り値は無視しても構いません. ここで使わないから返り値はvoidでも構わない? C言語であれば,それが正しい方法かもしれません.しかし,Javaはどんなプログラムでも, 他のプログラムから使われる部品となる可能性があります. 使われる部品として考えたときに,利用者がどんな情報を要求するかを見越して引数, 返り値を設定する必要があります.だから,ここでは,lendを呼び出した結果, 貸し出し履歴が返されれば貸し出し成功,null が返れば貸し出し失敗と判断できるよう返り値を設定しています.

12. 利用者の貸し出し履歴を検索する.

12-1. Mapの列挙子の取得方法.

ここでは,貸し出し履歴を検索します.貸し出し履歴は本ごとに管理されていますが, 検索条件では,一つの本に限った検索とは限りません.全ての履歴から検索しましょう. そのためには,Mapに入っている全ての履歴から検索を行わなければいけません. つまり,Mapからの列挙子の取得方法をここで学びます.

実は,Mapの列挙子は3種類存在します.キーの集合,バリューの集合, キーとバリューのペアの集合です.それぞれ取得する方法や返される列挙子が含む型も異なります.

キーの集合の列挙子

Map<Book, List<History>> historyMap = ....
for(Iterator<Book> i = historyMap.keySet().iterator(); i.hasNext(); ){
    Book book = i.next();
     // bookに対する処理.
}

Map<Book, List<History>>のキー集合に対する Iteratorを使った繰り返しを行いたい場合は左の通りです. このように,Map型の変数に対して,keySetメソッドを呼び出します. その結果,キー集合のSetが返されますので,それに対して, iteratorメソッドを呼び出すと,キー集合に対する列挙子が取得できます. なお,Setとは,重複を許さない集合を表します. キーは重複を許しませんので,Setとして返されることになります. ただし,Setは順番を保持しませんので, どのような順序で返されるかはユーザ側では制御できません.

Map<Book, List<History>> historyMap = ....
for(Book book: historyMap.keySet()){
     // bookに対する処理.
}

Map<Book, List<History>>のキー集合に対して 拡張for文で繰り返しを行いたい場合は左の通りです. こちらの書き方の方が,最近のJavaらしい書き方です.

バリューの集合の列挙子

Map<Book, List<History>> historyMap = ....
for(Iterator<Book> i = historyMap.values().iterator(); i.hasNext(); ){
    List<History> list = i.next();
     // listに対する処理.
}

Map<Book, List<History>>のバリューに対する Iteratorを使った繰り返しを行いたい場合は左の通りです. このように,Map型の変数に対して,valuesメソッドを呼び出します. その結果,バリューの集合であるCollectionが返されますので,それに対して, iteratorメソッドを呼び出すと,バリュー集合に対する列挙子が取得できます. こちらは,なぜSetではないのでしょうか. キーは重複を許しませんが,バリューは重複が含まれる可能性があるためです.

Map<Book, List<History>> historyMap = ....
for(List<History> list: historyMap.values()){
     // listに対する処理.
}

Map<Book, List<History>>のバリュー集合に対して, 拡張for文で繰り返しを行いたい場合は左の通りです. こちらの書き方の方が,最近のJavaらしい書き方です.

キーとバリューのペア集合の列挙子

Map<Book, List<History>> historyMap = ....
for(Iterator<Map.Entry<Book, List<History>>> i = historyMap.entrySet().iterator(); i.hasNext(); ){
    Map.Entry<Book, List<History>> entry = i.next();
    Book key = entry.getKey();
    List<History> value = entry.getValue();
    // key, value に対する処理.
}

Map<Book, List<History>>の各ペアに対して, Iteratorを使った繰り返しを行いたい場合は左の通りです. このように,Map型の変数に対して,entrySetメソッドを呼び出します. その結果,キーとバリューのペアの集合であるSetが返されますので,それに対して, iteratorメソッドを呼び出すと,ペア集合に対する列挙子が取得できます. Iteratorの宣言がややこしいですが,キーとバリューのペアは Map.Entry型で表されると分かれば読み解けるのではないでしょうか.

Map<Book, List<History>> historyMap = ....
for(Map.Entry<Book, List<History>> entry: historyMap.entrySet()){
    Book key = entry.getKey();
    List<History> value = entry.getValue();
    // key, value に対する処理.
}

この書き方も拡張for文で左図のように書けます.

また,キー集合の列挙子を取得し,Mapに対してget メソッドを実行すると,ペアの列挙子で得られる情報と同じものが取得できます. どちらでも理解しやすい方で構いませんが,ペアの列挙子を取得する方法(ここで紹介した方法) の方がパフォーマンスは高いです.

12-2. 検索する

貸し出し履歴を検索するメソッド,findHistoryLibraryクラスに定義してください. 返り値はList<History>,引数は,BookUserの2つとします. 引数で受け取った変数のうち,nullではない項目と一致するすべての Historyをメソッド内で作成したListに追加して返します. さて,どのように検索すれば良いかを考えましょう.

まず,Booknullではない場合を考えましょう. この場合,MapからキーであるBookを検索した結果の Listを対象に検索を行えば良いでしょう. キーとなるBookを指定して得られたListに対して, Userで絞り込めば良いです.Usernullであれば, キーとなるBookを指定して得られたバリューが返り値そのものになります.

一方で,Booknullの場合は,Map に格納されているすべてのバリューに対して検索を行わなければいけません. そして,各バリューに対して,Userで絞り込む必要があります.

いずれの場合においても,返り値となるListの実体をfindHistory 内で作成し,最後に返す必要があります. 特に,引数のBookUserの両方が nullであった場合は,Map のバリューの内容をすべて返り値のList に追加し直す必要があります.

では,このfindHistoryメソッドを作成しましょう. 作成ができれば,runFindHistoryメソッドを作成してください. そして,runFindHistoryrunメソッドの最後 (runLendの呼び出し後)に呼び出しましょう. そして,返り値のListの各要素であるHistoryの実体に対して, printメソッドを呼び出し,履歴を画面に表示してください.

本日のまとめ

本日,学んだ内容は次の通りです.

図書館システムのまとめ

この図書館システムでは,次に挙げる基本的なJavaプログラムの作り方を学びました.

以上4点はJavaで高度なプログラムを作成する時には,非常に基本的な知識になってきます. これらは,今後も頻繁に出てきますので,しっかりと身につけてください.

提出課題について

図書館システムで作成したプログラムを提出してください. 提出期限は次回授業の前日24:00までとします. 提出場所は,Moodle上の指示された場所とします. 以下の手順を行って,zipファイルを提出してください.

  1. まず,ディレクトリを作成し,そのディレクトリの名前を6桁の学生証番号にしてください.
  2. LibraryUtil.java,books.csv 以外のすべてのプログラムに自身の学生証番号, 名前をコメントに入れてください.
  3. 次に,この回までに作成したプログラム,データをそのディレクトリに入れてください.
  4. そのディレクトリをzip圧縮してください.zip圧縮後のファイル名を「学生証番号.zip」にしてください.

提出するファイルは次の通りです.もし,パッケージ宣言している場合は, その部分をコメントアウトし,パッケージ宣言はしないようにしてください.