Javaで図書館の貸出を管理してみよう(総合演習)
ここまでの Java レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。クラス・enum・例外・List・for文・三項演算子——すでに練習したものだけで組み立てます。
作るのは図書館の貸出管理です。誰が何を借りているかを持ち、期限を過ぎたものを見つけ、延滞料金を計算して、一覧にまとめます。
Javaらしさがいちばん出るのは「包む」ところです。データをclassに閉じ込め、状態をenumで限定し、失敗を例外で伝える——この3つが揃うと、おかしな状態になりようがないプログラムになります。
このページではコードを実行していません。Javaはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のJavaでコンパイルして走らせた出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
貸出できません: 田中さんは既に3冊借りています(上限3冊) ===== 貸出状況(20日時点) ===== 統計の入門 田中 期限12日 延滞中 8日超過 小さな家の設計 佐藤 期限25日 貸出中 土を育てる暮らし 鈴木 期限10日 返却済 夜明けの航海記 高橋 期限18日 延滞中 2日超過 デザインの教科書 伊藤 期限30日 貸出中 海の地図 田中 期限28日 貸出中 森の観察記 田中 期限28日 貸出中 ---------------------------------------- 登録冊数 : 7冊 延滞 : 2冊 延滞料金計: 300円 延滞している方に連絡してください。
STEP 1本1冊をクラスで表す
🎯 このステップのゴール: フィールドをprivateにして、コンストラクタとメソッドで触らせる
まずデータの形をクラスで書きます。本1冊が持つのは「タイトル・借りた人・返却期限・返却済みか」の4つです。
フィールドを全部privateにしているのに注目してください。外からbook.title = "..."と直接書き換えられないようにしています。
なぜ閉じるのか。タイトルや借りた人が、どこからでも書き換えられるとしたら——おかしな値になったとき、犯人を探す場所が全部になります。入口を絞れば、探す場所も絞れます。
finalを付けたフィールドはコンストラクタで一度決めたら二度と変わりません。タイトルと借りた人と期限は変わらないのでfinal、返却済みかどうかだけは変わるので付けていません。「変わるもの」と「変わらないもの」を型で区別しておくと、読む人に意図が伝わります。
値を取り出すのがtitle()のようなメソッドです。JavaではgetTitle()と書く流儀が長く使われてきましたが、最近はtitle()と短く書くことも増えています。どちらでも構いません——チームで揃っていることのほうが大事です。
new Book("統計の入門", "田中", 12, false)で1冊作ります。引数の順番はコンストラクタの通りです。
タイトル: 統計の入門 借りた人: 田中 返却期限: 12日 返却済み: false
STEP 2状態をenumで表して判定する
🎯 このステップのゴール: 取りうる状態を3つに限定し、三項演算子で振り分ける
貸出の状態は「貸出中・返却済・延滞中」の3つしかありません。これをStringで持つと何でも入ってしまいます。
enum Status { ON_LOAN, RETURNED, OVERDUE }——この3つ以外は存在できません。打ち間違えればコンパイルが通らないので、実行する前に間違いが分かります。
Javaのenumは、ただの定数の並びではありません。フィールドもコンストラクタもメソッドも持てる、ちゃんとしたクラスです。ここではlabelという日本語名を持たせて、status.label()で取り出せるようにしています。
この設計の効き目は、表示を変えたくなったときに出ます。「貸出中」を「貸出中(返却待ち)」にしたい——直す場所はenumの1行だけです。ifで分岐して文字列を組み立てていたら、書いた場所を全部探すことになります。
判定はstatus(int today)メソッドです。返却済みなら即RETURNED、そうでなければ期限と今日を比べます。
today > dueDay ? Status.OVERDUE : Status.ON_LOAN——三項演算子です。「条件 ? 真のとき : 偽のとき」と読みます。ifで4行書いても同じですが、2択を返すだけならこちらが素直です。
ただし入れ子にしないでください。三項演算子を三項演算子の中に書くと、途端に読めなくなります。3択以上ならifかswitchです。
統計の入門: 延滞中 小さな家の設計: 貸出中 土を育てる暮らし: 返却済 夜明けの航海記: 延滞中 デザインの教科書: 貸出中
STEP 3Listにまとめて延滞を数える
🎯 このステップのゴール: 拡張for文で回し、延滞日数と料金を積み上げる
本をまとめて扱います。使うのはListです。
List<Book> books = new ArrayList<>();——左が型、右が実物という書き方をしています。
なぜ左をArrayListにしないのか。Listは「順番に並ぶもの」という約束だけを表すインターフェースで、ArrayListはその実現方法の1つです。左をListにしておけば、あとでLinkedListに変えたくなっても、この1行を直すだけで済みます。
<Book>がジェネリクスです。「このListにはBookしか入らない」と宣言しています。付けないと何でも入ってしまい、取り出すたびに型を変換する羽目になります。
回すのはfor (Book book : books)——拡張for文です。添字を書かないので、範囲を1つ間違えて落ちる、という事故が起きません。全件を順に見るだけなら、こちらを選んでください。
数え上げの型は決まっています。先に入れ物を0で用意し、条件に合ったときだけ足す。overdueCountとtotalFeeを両方このやり方で積み上げています。
料金はdays * FEE_PER_DAYです。30を直接書かず定数にしています——料金は必ず変わるものだからです。
統計の入門 は 8日超過 夜明けの航海記 は 2日超過 延滞: 2冊 / 料金計: 300円
STEP 4独自例外で「借りられない」を伝える
🎯 このステップのゴール: Exceptionを継承した型を作り、投げて受け止める
「1人3冊まで」という決まりを入れます。4冊目を借りようとしたら、断らなければなりません。
ここでfalseを返すのではなく、例外を投げます。理由があります——falseには理由が書けないからです。「上限だから」なのか「そんな本は無いから」なのか、呼び出した側は区別できません。
class LoanException extends Exceptionでこの図書館専用の例外を作ります。Exceptionを継承するだけで、あとはメッセージを親に渡すコンストラクタを1つ書けば完成です。
なぜ既存のExceptionをそのまま使わないのか。専用の型を作ると、catch (LoanException e)で「貸出の失敗だけ」を捕まえられます。他の失敗まで一緒に握りつぶしてしまうことがありません。
投げるのがthrow new LoanException("...")。メソッドの宣言にthrows LoanExceptionと書く必要があります——Javaは「このメソッドは失敗しうる」を型に書かせる言語です。
これを検査例外と呼びます。呼ぶ側はtry-catchで受けるか、自分もthrowsを付けて上に投げるか、どちらかを必ず選ばされます。書き忘れができません。
受けるのがcatch (LoanException e)で、e.getMessage()に投げるときに渡した文章が入っています。だから「なぜ断られたか」がそのまま利用者に見せられます。
3冊まで借りられました: 3冊 貸出できません: 田中さんは既に3冊借りています(上限3冊)
STEP 5一覧を表の形に整える
🎯 このステップのゴール: printfで桁を揃え、カンマ区切りの金額を出す
数字が並んでいるだけでは読めません。表の形に整えます。
使うのはSystem.out.printfです。%-10sは「左揃えで10文字ぶんの文字列」、%2dは「右揃えで2桁の整数」。文字列は左、数字は右に揃えると読みやすくなります。
金額には%,dを使います。カンマ区切りで1,200のように出ます。手で3桁ごとに区切る処理を書く必要はありません——標準の書式指定でできます。
改行に%nを使っているのに注目してください。\nでも改行できますが、%nはそのパソコンに合った改行文字を出します。Windowsでも同じように見えるので、printfの中では%nを使うのが習慣です。
延滞日数の欄は三項演算子で出し分けます。days > 0 ? String.format(" %d日超過", days) : ""——遅れていなければ空文字にして、何も出しません。
ここでString.formatを使っているのが要点です。printfは画面に出すだけですが、String.formatは同じ書式で文字列を作って返します。あとで組み立てたいときは、こちらです。
日本語は文字の幅が英数字の2倍で表示されるので、%-10sでも見た目は完全には揃いません。大きくは整うので実用になりますが、厳密に揃えたいならHTMLの表を使うのが本筋です。
統計の入門 田中 期限12日 延滞中 8日超過 小さな家の設計 佐藤 期限25日 貸出中 土を育てる暮らし 鈴木 期限10日 返却済 夜明けの航海記 高橋 期限18日 延滞中 2日超過 デザインの教科書 伊藤 期限30日 貸出中 延滞料金計: 300円
STEP 6ひとつのメソッドにまとめて仕上げる
🎯 このステップのゴール: report にまとめ、貸出の失敗もあわせて動かす
最後の仕上げです。ここまでの部品をreportという1つのメソッドにまとめます。
並べる順番に意味を持たせます。まず1冊ずつの状況、次に区切り線、最後に全体のまとめ——細かいものから大きいものへという流れです。
mainでは貸出の失敗も一緒に動かします。3冊借りている田中さんが4冊目を借りようとして断られる——ステップ4で作った例外が、実際の流れの中で働くところが見られます。
tryの中に3回のcheckOutをまとめて書いているのが要点です。1回ごとにtry-catchを書くこともできますが、「どれかが失敗したら、そこで打ち切る」という意図なら、まとめて囲むほうが自然に表せます。
static finalの定数を3つ並べました。TODAY・FEE_PER_DAY・MAX_LOANS——どれも後から変わる値です。staticは「インスタンスごとではなくクラスに1つ」、finalは「書き換えない」という意味です。
完成です。TODAYを10に変えてみてください。延滞が0冊になり、締めの言葉が「延滞はありません。」に変わります。1つの数字を変えるだけで、判定も料金も表示も全部連動する——定数に切り出しておいた効果が確かめられます。
次に進むなら、本のリストをファイルから読むところです。reportはList<Book>を受け取るだけなので、作り方が変わっても、そのまま使えます。
貸出できません: 田中さんは既に3冊借りています(上限3冊) ===== 貸出状況(20日時点) ===== 統計の入門 田中 期限12日 延滞中 8日超過 小さな家の設計 佐藤 期限25日 貸出中 土を育てる暮らし 鈴木 期限10日 返却済 夜明けの航海記 高橋 期限18日 延滞中 2日超過 デザインの教科書 伊藤 期限30日 貸出中 海の地図 田中 期限28日 貸出中 森の観察記 田中 期限28日 貸出中 ---------------------------------------- 登録冊数 : 7冊 延滞 : 2冊 延滞料金計: 300円 延滞している方に連絡してください。
よくある質問
このページではコードを実行しないのですか?
していません。Javaはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のJavaでコンパイルして走らせた出力を、そのまま載せています。このコードをMain.javaという名前で保存してjava Main.javaと実行すれば、まったく同じ結果が出ます。
public class が1つしか書けないのはなぜですか
ファイル名とクラス名を一致させる決まりがあるからです。public class MainはMain.javaに書かなければなりません。1ファイルにpublicなクラスが2つあると、どちらの名前をファイル名にすればよいか決まりません。
この演習ではBookやStatusにpublicを付けていないので、同じファイルに置けます。
実際の開発では1クラス1ファイルに分けるのが普通です。ここでは手元でそのまま試せることを優先して、1つにまとめてあります。
例外を投げるのと false を返すのは、どう使い分けますか
「よくあること」なら戻り値、「起きてはいけないこと」なら例外というのが目安です。
たとえば「検索して見つからなかった」は普通に起こるので、nullや空のリストを返すほうが素直です。
一方この演習の「上限を超えた」は断る理由を伝える必要があるので、例外が向いています。falseでは「なぜ断られたか」が呼び出し側に届きません。
迷ったら「呼び出した側は、この失敗をどう扱いたいか」を考えてみてください。
%n と \\n は何が違いますか
改行文字が環境によって変わるかどうかです。
\\nは必ず「LF」という1文字を出します。%nはそのパソコンの標準的な改行を出すので、Windowsでは「CRLF」の2文字になります。
画面に出すだけならどちらでも同じに見えます。ただしファイルに書き出すときは差が出ます——Windowsのメモ帳で開いたときに改行されない、という古典的な問題です。printfの中では%nを使う習慣にしておくと無難です。
🚩 次はこのカテゴリへ
KotlinはJavaと同じ土台の上で動く言語です。この演習で書いたBookクラスは、Kotlinならdata classの1行でほぼ同じものが書けます。Javaを知っているほど、Kotlinの短さの意味が分かります。Kotlinカテゴリの最終演習で、その違いを確かめてみてください。