2016年度 発展プログラミング演習 第6講 図書館システム 4日目
目次
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. 貸し出し履歴を保存・参照する.
履歴を作成するメソッドを作成する.
Map
もList
と同じくCRUD
が行えます.作成(追加),読み取り,更新,削除です.
それぞれ,次のメソッド呼び出しで可能です.
- 作成(追加)は,
put
メソッドで行います.このメソッドで,キーとバリューを結びつけます. - 読み取りは,
get
メソッドです.キーを指定し,対応するバリューを取得します. - 更新は,作成(追加)と同じく,
put
メソッドで行います. 従来のキーに結びついていたバリューを削除し,キーとバリューを新たに結びつけます. - 最後の削除は,
remove
メソッドで行います.キーを指定し, キーとバリューの対応付けを削除します.
これらの操作を行う前に,履歴情報を保持する変数を宣言しましょう.
変数名は,historyMap
,型はMap
,
格納する型は,キーがBook
,バリューがList<History>
です.
この変数をLibrary
クラスのフィールドに,宣言しましょう.
次に,History
を作成するメソッドであるcreateHistory
次に示す指示に従ってLibrary
に追加しましょう.
- 引数は2つ,
User
型とBook
型の変数です. - 返り値の型は
History
型とします. - メソッドでは,最初に
History
型の値を作成しましょう. -
次に,作成した
History
型のフィールドに引数のUser
,Book
型の値を代入しましょう. -
History
型のフィールドのlendDate
に現在時刻を作成し,代入しましょう.現在時刻は,new Date()
で得られます. - 以上の情報が代入された
History
型の変数をreturn
しましょう.
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
の引数はUser
とBook
,
返り値はHistory
とします.そして,以下の処理をlend
メソッドで書いていきましょう.
-
createHistory
メソッドを用いて,引数に与えられたユーザが本を借りた 履歴を作成しましょう. -
作成した履歴を
registerHistory
メソッドを利用して登録しましょう. -
登録に成功すれば,登録した
History
の実体を,失敗すればnull
を返します.
上で作成した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. 検索する
貸し出し履歴を検索するメソッド,findHistory
を
Library
クラスに定義してください.
返り値はList<History>
,引数は,Book
,
User
の2つとします.
引数で受け取った変数のうち,null
ではない項目と一致するすべての
History
をメソッド内で作成したList
に追加して返します.
さて,どのように検索すれば良いかを考えましょう.
まず,Book
がnull
ではない場合を考えましょう.
この場合,Map
からキーであるBook
を検索した結果の
List
を対象に検索を行えば良いでしょう.
キーとなるBook
を指定して得られたList
に対して,
User
で絞り込めば良いです.User
がnull
であれば,
キーとなるBook
を指定して得られたバリューが返り値そのものになります.
一方で,Book
がnull
の場合は,Map
に格納されているすべてのバリューに対して検索を行わなければいけません.
そして,各バリューに対して,User
で絞り込む必要があります.
いずれの場合においても,返り値となるList
の実体をfindHistory
内で作成し,最後に返す必要があります.
特に,引数のBook
,User
の両方が
null
であった場合は,Map
のバリューの内容をすべて返り値のList
に追加し直す必要があります.
では,このfindHistory
メソッドを作成しましょう.
作成ができれば,runFindHistory
メソッドを作成してください.
そして,runFindHistory
をrun
メソッドの最後
(runLend
の呼び出し後)に呼び出しましょう.
そして,返り値のList
の各要素であるHistory
の実体に対して,
print
メソッドを呼び出し,履歴を画面に表示してください.
本日のまとめ
本日,学んだ内容は次の通りです.
- キーとバリュー(値)のペアを扱うには
Map
を使う.- 代入する値は,
HashMap
もしくは,TreeMap
をnew
する. - 利用するには,
import java.util.*;
という文がクラス宣言の前で必要. - キーとバリューの型に制限はない.
-
キーとバリューの型を宣言時,値の作成時に指定しなければならない.
Map<キーの型, バリューの型> map = new HashMap<キーの型, バリューの型>();
- 代入する値は,
-
Map
の利用方法(CRUDの方法.Map<String, Integer> map = new HashMap<String, Integer>();
の場合)-
Map
にペアを追加する.
map.put("first", new Integer(0));
-
Map
のキーに対応するバリューを更新する.
map.put("first", new Integer(1));
-
Map
からキーを指定して,バリューを取得する.
Integer value = map.get("first");
-
Map
のキーに対応するバリューが登録されているか確認する.
Boolean flag = map.contains("first");
登録されていれば,flag
がtrue
となる. -
Map
からキーと対応するバリューを削除する.
map.remove("first");
-
Map
からの列挙子の取得方法.
図書館システムのまとめ
この図書館システムでは,次に挙げる基本的なJavaプログラムの作り方を学びました.
- 独自の型の作成方法.
- 作成した型の利用方法.
List
型の使い方.Map
型の使い方.
以上4点はJavaで高度なプログラムを作成する時には,非常に基本的な知識になってきます. これらは,今後も頻繁に出てきますので,しっかりと身につけてください.
提出課題について
図書館システムで作成したプログラムを提出してください. 提出期限は次回授業の前日24:00までとします. 提出場所は,Moodle上の指示された場所とします. 以下の手順を行って,zipファイルを提出してください.
- まず,ディレクトリを作成し,そのディレクトリの名前を6桁の学生証番号にしてください.
- LibraryUtil.java,books.csv 以外のすべてのプログラムに自身の学生証番号, 名前をコメントに入れてください.
- 次に,この回までに作成したプログラム,データをそのディレクトリに入れてください.
- そのディレクトリをzip圧縮してください.zip圧縮後のファイル名を「学生証番号.zip」にしてください.
提出するファイルは次の通りです.もし,パッケージ宣言している場合は, その部分をコメントアウトし,パッケージ宣言はしないようにしてください.
Library.java
LibraryUtil.java
Book.java
User.java
UserManager.java
History.java
books.csv