『入門向け』Java開発環境の構築とかんたんなコードの書き方と実行方法 (後編)
はじめに
この記事は、 rubytomato.hateblo.jp rubytomato.hateblo.jp に続く記事の後編です。 前編、中編ではJava開発環境の構築とEclipseの簡単な使い方を中心に説明しました。この記事では引き続きJavaプログラムの書き方と実行方法を中心に説明します。
Javaプログラムの書き方
中編のおさらい
中編のレッスン1では、Fig1のようにstudy01project
プロジェクトにDemo
クラスとSnack
クラスまで作成しました。
レッスン2
レッスン2では、レッスン1で作成した「おかし」を表現するSnack
クラスに加え、「おかしを売る店」を表現するShop
クラスと「おかしを買う客」を表現するPerson
クラスを使って、お店とお客がおかしを売買する機能をコーディングしていきます。
おかしを売買できる条件として以下の3つを仕様とします。
- お店(
Shop
)は扱っているおかししか売れないこと - お客(
Person
)はおかしを買うのに代金を支払うこと - お客(
Person
)は買ったおかしをおかし袋に仕舞えること(最大5個まで仕舞える)
また、そのほかの仕様として
- お店(
Shop
)はおかしをいくつでも販売できる。(おかしの在庫数は管理しない) - おかしの売買は『おかしの名前』で行う。(『おかしの名前』が一意で管理できる(重複しない)こと)
とします。
レッスン2用のパッケージを作成する
まずレッスン2用のパッケージcom.example.lesson02
を作成し、lesson02
パッケージにDemo
クラスを作成します。Fig2はここまで作成した状態です。
Demoクラス
Demo.javaのソースコードは以下の通りです。レッスン1同様にこのクラスのmain
メソッドにレッスン2で実行するコードを実装していきます。
package com.example.lesson02; public class Demo { public static void main(String[] args) { System.out.println("Lession02 Demo start"); // ここに動作確認するコードを追加していく System.out.println("Lession02 Demo end"); } }
Snackクラスを修正する
おかしの売買を『おかしの名前』で行うためには『おかしの名前』が重複なく一意であることが求められます。そのためにSnack
クラスのname
フィールドでオブジェクトを一意に識別できるように実装する必要があります。(※ちなみにオブジェクトを一意に識別するフィールドには社員IDや商品IDといったIDフィールドを使うことが一般的です。)
Snack
クラスの2つのオブジェクトが同一(おなじおかしを指すか)であるか判別するには、Snack
クラスのhashCode
とequals
メソッドを正しくオーバーライドする必要があります。
たとえばSnack
クラスのhashCode
とequals
メソッドをオーバーライドしていない状態で以下のコードを実行すると、同じおかしの名前を持つ2つのオブジェクト(salami_1とsalami_2)なのに”違うおかし”と判断されます。
Snack salami_1 = new Snack("うまいぼう サラミ味", 10); Snack salami_2 = new Snack("うまいぼう サラミ味", 10); if (salami_1.equals(salami_2)) { System.out.println("同じおかし"); } else { System.out.println("違うおかし"); }
この2つのオブジェクトを ”同じおかし”である(おかしの名前が同じなので)と判別するには、hashCode
とequals
メソッドを以下のようにオーバーライドします。
- メニューバーの
ソース(S)
→hashCode() および equals() の生成(H)...
を選択 - キーボードのAlt + Shift + Sを押し、メニューの
hashCode() および equals() の生成(H)...
を選択
"hashCode() および equals() の生成"画面(Fig3)のname
フィールドだけにチェックを入れ、"生成"ボタンをクリックします。
すると、Eclipseによって以下のコードが生成されます。これでname
フィールドが同じであれば”同じおかし”であると判別されるようになります。
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Snack other = (Snack) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; }
先ほどのコードを実行すると2つのオブジェクトは”同じおかし”と判断されます。
ちなみに”おかしの名前”と”価格”の2つのフィールドが同一であれば同じおかしと判別したい場合は、"hashCode() および equals() の生成"画面(Fig3)でname
とprice
フィールドにチェックを入れてコードを生成しなおします。
Shopクラスを作成する
まずcom.example.lesson02.model
パッケージを作成し、model
パッケージにShop
クラスを作成します。
Fig4はmodel
パッケージおよびShop
クラスを作成した直後のプロジェクトの状態です。
以下がShop
クラスを作成した直後のソースコードです。
package com.example.lesson02.model; public class Shop { }
フィールドを定義する
Shop
クラスに、お店が扱うおかしの種類を持つSet
型のsnackVarieties
、おかしの売上金を持つint
型のsales
の2つのフィールドを定義します。同じ名前のおかしを持たないようにsnackVarieties
はSet
型にしています。
アクセサメソッドは必要がないので実装しません。
/** * お店が扱うおかしの種類 */ private Set<Snack> snackVarieties; /** * 売上金 */ private int sales;
ちなみに、下記のようにこのフィールドをList
型にすると同じ名前のおかしを複数追加できてしまいます。
private List<Snack> snackVarieties;
toStringメソッドをオーバーライドする
Eclipseの機能を使ってtoStringメソッドをオーバーライドします。
@Override public String toString() { return "Shop [snackVarieties=" + snackVarieties + ", sales=" + sales + "]"; }
コンストラクターを実装する
デフォルトコンストラクタ―と、引数におかしの種類と売上金を取るコンストラクタ―の2つを実装します。
// デフォルトコンストラクタ― public Shop() { this(new HashSet<>(), 0); } public Shop(Set<Snack> snackVarieties, int sales) { this.snackVarieties = snackVarieties; this.sales = sales; }
おかしの種類を追加するメソッドを実装する
お店が扱うおかしの種類を追加するメソッドをaddSnackVariety
という名前で実装します。上述した通りsnackVarieties
フィールドはSet
型なので、オブジェクトを追加する際の重複チェックは不要です。同じ名前のおかしを登録しても無視されます。
/** * お店が扱うおかしの種類を追加する * @param snack */ public void addSnackVariety(Snack snack) { this.snackVarieties.add(snack); }
さらに、お店が扱うおかしを複数まとめて追加するメソッドをaddSnackVarieties
という名前で実装します。このメソッドの(Snack ... snacks)
の部分を可変長引数といい、複数のパラメータをまとめて渡すことができます。
その可変長引数snacks
を取るSet.of
メソッドは可変長引数をSet型のコレクションにして返します。
/** * お店が扱うおかしの種類を複数まとめて追加する * @param snacks */ public void addSnackVarieties(Snack... snacks) { this.snackVarieties.addAll(Set.of(snacks)); }
お店がおかしを扱っているか確認するメソッドを実装する
引数で指定した名前のおかしをお店が扱っているか確認する処理をisAvailableSnack
メソッドに実装します。と言っても実際の検索処理はgetSnack
メソッドで行い、その結果を真偽値で返すだけの処理になります。
/** * 指定した名前のおかしを扱っているか確認する * @param snackName おかしの名前 * @return true:扱っている */ public boolean isAvailableSnack(String snackName) { return getSnack(snackName).isPresent(); }
Set
型のコレクションからおかしの名前で検索する処理はgetSnack
メソッドに実装します。このメソッドはShop
クラス内からのみ利用するのでメソッドの可視性はprivate
にして他のクラスから利用されないようにします。
また、指定した名前のおかしを必ず扱っているとは限らないのでこのメソッドの戻り値はOptional
でラップします。見つかったおかしのオブジェクトはOptional.of
でラップして返し、もし扱っていない(見つからなかった)場合はnullではなくOptional.empty()
を返すようにします。
/** * 指定した名前のおかしのオブジェクトを返す * @param snackName おかしの名前 * @return おかしのオブジェクト */ private Optional<Snack> getSnack(String snackName) { Iterator<Snack> ite = snackVarieties.iterator(); while (ite.hasNext()) { Snack snack = ite.next(); if (snack.getName().equals(snackName)) { return Optional.of(snack); } } return Optional.empty(); }
お店が扱っているおかしの種類を表示するメソッドを実装する
扱うおかしを出力する処理はprettyPrintSnackVariety
メソッドに実装します。この記事ではデバッグ用に使用していますが、オブジェクトの状態を出力するメソッドがあると便利なときがあります。
/** * お店が扱っているおかしを表示する */ public void prettyPrintSnackVariety() { if (snackVarieties.isEmpty()) { System.out.println("取り扱うおかしはありません"); return; } snackVarieties.forEach(snack -> { System.out.println(snack.getName() + "は" + snack.getPrice() + "円"); }); System.out.println("計" + snackVarieties.size() + "種類のおかしを取り扱い中"); }
Personクラスを作成する
model
パッケージにPerson
クラスを作成します。
Fig5はmodel
パッケージおよびPerson
クラスを作成した直後のプロジェクトの状態です。
以下がPerson
クラスを作成した直後のソースコードです。
package com.example.lesson02.model; public class Person { }
フィールドを定義する
Person
クラスに、人の名前を持つString
型のname
、所持金を持つint
型のmoney
、持っているおかしを持つList
型のsnackBag
の3つのフィールドと、おかしの最大所持数を持つ定数を定義します。おかし袋のsnackBag
フィールドはList
型なのでおなじ名前のおかしを複数持つことができます。
このクラスもアクセサメソッドは必要がないので実装しません。
/** * 名前 */ private String name; /** * 所持金 */ private int money; /** * おかし袋 */ private List<Snack> snackBag; /** * おかし袋に入れられるおかしの最大数 */ private static final int MAX_SNACK_NUM = 5;
toStringメソッドをオーバーライドする
Eclipseの機能を使ってtoStringメソッドをオーバーライドします。
@Override public String toString() { return "Person [name=" + name + ", money=" + money + ", snackBag=" + snackBag + "]"; }
コンストラクターを実装する
名前のname
、所持金のmoney
は必須のフィールドなので、コンストラクターの引数で初期化するようにします。
おかし袋のsnackBag
の要素数はMAX_SNACK_NUM
定数で初期化します。(※あくまでも定数の値で要素数を確保するだけで、要素数の最大値を設定しているわけではありません。)
public Person(String name, int money) { assert name != null; this.name = name; this.money = money; this.snackBag = new ArrayList<>(MAX_SNACK_NUM); }
持っているおかしを出力するメソッドを実装する
持っているおかしを出力する処理をprettyPrintHasSnack
メソッドに実装します。このメソッドもこの記事ではデバッグ用に使用します。
/** * 持っているおかしを表示する */ public void prettyPrintHasSnack() { StringBuilder sb = new StringBuilder(name); sb.append("は 所持金:"); sb.append(money); sb.append("円"); if (!snackBag.isEmpty()) { sb.append("とおかし "); String comma = ""; for (Snack snack : snackBag) { sb.append(String.format("%s(name:%s, price=%d)", comma, snack.getName(), snack.getPrice())); comma = ", "; } } sb.append(" を持っている"); System.out.println(sb.toString()); }
おかしを売買する機能を実装する
Personクラスの実装
Shop
クラスとおかしの売買を行うdeal
メソッド- おかしを買う
buySnack
メソッド
Shopクラスの実装
Person
クラスへおかしを売るsellSnack
メソッド
売買が失敗した場合
売買が出来なかった場合に例外をスローするようにします。想定する売買できない状況というのは
- お店(
Shop
)が取り扱っていないおかしを購入しようとした場合 - お客(
Person
)が購入するおかしの代金を支払えない場合 - お客(
Person
)のおかし袋に空きがない場合
があります。このような場合はDealException
というRuntimeException
を継承した例外クラスをスローするようにします。
package com.example.lesson02; public class DealException extends RuntimeException { public DealException(String message) { super(message); } }
Shopクラスにおかしを売るsellSnackメソッドを実装する
指定した名前のおかしがお店で扱われているかgetSnack
メソッドで確認し、扱われている場合はPerson
クラスのbuySnack
メソッドでおかしを買う処理を実行します。その処理が正常終了したら代金を売上金に加算します。
なおFig7のようにcustomer.buySnack(snack);
の部分にエラーが出るとおもいますが、とりあえずこのままにして次のPerson
クラスの実装に移ります。
/** * おかしを売る * @param snackName 売りおかしの名前 * @param customer おかしを買う人 * @throws DealException おかしの売買が失敗した場合 */ public void sellSnack(String snackName, Person customer) { Optional<Snack> optionalSnack = getSnack(snackName); optionalSnack.ifPresentOrElse(snack -> { // おかしが見つかった場合 // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; }, () -> { // おかしが見つからなかった場合 throw new DealException(snackName + "は取り扱っていません"); }); }
getSnack
メソッドはOptional
型でラップしたSnack
クラスのオブジェクトを返します。Optional
型でラップするということは、指定した『おかしの名前』でおかしが見つからない場合があるということを表しているので、見つかった場合と見つからなかった場合の両方の処理を忘れずに実装します。
Optional<Snack> optionalSnack = getSnack(snackName);
その次のoptionalSnack.ifPresentOrElse( ... )
というコードの部分は、以下のコードの書き方と同じ意味になります。isPresent
メソッドがtrueを返す場合はおかしが見つかった場合ということなので、取引処理を実行します。それ以外は見つからなかった場合ということなので例外をスローします。
// 指定した『おかしの名前』でおかしが見つかった場合 if (optionalSnack.isPresent()) { Snack snack = optionalSnack.get(); // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; // おかしが見つからなかった場合 } else { throw new DealException(snackName + "は取り扱っていません"); }
Personクラスにおかしの売買をするdealメソッドを実装する
Person
クラスとShop
クラス間のおかしを売買する処理はdeal
メソッドに実装します。メソッドの先頭で、引数がnullであれば不正(バグ)ということを示すためにassert
文でチェックを行います。
なお、実際の売買はShop
クラスのsellSnack
メソッドで行います。
/** * お店と取引する * @param shop 取引するお店 * @param snackName 買うおかしの名前 */ public void deal(Shop shop, String snackName) { assert shop != null; assert snackName != null; try { shop.sellSnack(snackName, this); } catch (DealException e) { System.out.println(snackName + "の購入に失敗しました"); System.err.println(e.getMessage()); } }
buySnack
メソッドではおかしを買える所持金を持っているかとおかし袋に空きがあるかのチェックを行い、条件を満たしていない場合はDealException
をスローして処理を中断するようにしています。
/** * おかしを買う * @param snack 買うおかしのオブジェクト * @return 支払った金額 * @throws DealException おかしが買えなかった場合にスロー */ int buySnack(Snack snack) throws DealException { if (money < snack.getPrice()) { throw new DealException("所持金が足りません"); } if (MAX_SNACK_NUM <= snackBag.size()) { throw new DealException("おかし袋に空きがありません"); } // 所持金からおかしの価格を引く money -= snack.getPrice(); // おかし袋におかしを追加する snackBag.add(snack); return snack.getPrice(); }
Demoクラス
上記の実装が終わったら、Demo
クラスのmain
メソッドに動作確認用のコードを書いて実行してみます。
まず、お店が扱うおかしのオブジェクトを準備します。
// サラミ味のおかしを生成 Snack umaibouSalami = new Snack("うまいぼう サラミ味", 10, "コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤"); // チーズ味のおかしを生成 Snack umaibouCheese = new Snack("うまいぼう チーズ味", 10, "コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素"); // めんたい味のおかしを生成 Snack umaibouMentai = new Snack("うまいぼう めんたい味", 10, "コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料");
次にお店のオブジェクトを生成し、扱うおかしを追加します。
// お店の扱うおかしの準備 Shop dagashiya = new Shop(); dagashiya.addSnackVariety(umaibouSalami); dagashiya.addSnackVariety(umaibouCheese); dagashiya.addSnackVariety(umaibouMentai); // 取り扱い中のおかしを出力 dagashiya.prettyPrintSnackVariety();
3つのおかしの登録はaddSnackVarieties
メソッドで1行で書くこともできます。
dagashiya.addSnackVarieties(umaibouSalami, umaibouCheese, umaibouMentai);
おかしを買う人のオブジェクトを生成します。おかしを1つも買っていない場合は以下のようなメッセージを出力します。
Person taro = new Person("たろう", 100); // 所持金と購入したおかしを出力 taro.prettyPrintHasSnack();
たろうは 所持金:100円 を持っている
deal
メソッドでお店からおかしを5つ買い、もう一度所持金と購入したおかしの情報を出力してみます。
taro.deal(dagashiya, "うまいぼう サラミ味"); taro.deal(dagashiya, "うまいぼう サラミ味"); taro.deal(dagashiya, "うまいぼう チーズ味"); taro.deal(dagashiya, "うまいぼう チーズ味"); taro.deal(dagashiya, "うまいぼう めんたい味"); // 所持金と購入したおかしを出力 taro.prettyPrintHasSnack();
たろうは 所持金:50円とおかし (name:うまいぼう サラミ味, price=10), (name:うまいぼう サラミ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう めんたい味, price=10) を持っている
もう1人、おかしを買う人のオブジェクトを生成し、所持金より多いおかしを購入してみます。
Person jiro = new Person("じろう", 30); // 所持金と購入したおかしを出力 jiro.prettyPrintHasSnack(); jiro.deal(dagashiya, "うまいぼう サラミ味"); jiro.deal(dagashiya, "うまいぼう チーズ味"); jiro.deal(dagashiya, "うまいぼう めんたい味"); jiro.deal(dagashiya, "うまいぼう めんたい味"); // 所持金と購入したおかしを出力 jiro.prettyPrintHasSnack();
じろうは 所持金:30円 を持っている うまいぼう めんたい味の購入に失敗しました 所持金が足りません じろうは 所持金:0円とおかし (name:うまいぼう サラミ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう めんたい味, price=10) を持っている
レッスン2のさいごに
これでレッスン2は終了です。これまでに書いたJavaのソースコードは以下のようになります。
Snack.java
package com.example.lesson01.model; public class Snack { private static final String UNDEFINED_INGREDIENTS = "未定"; public Snack(String name, int price) { this(name, price, UNDEFINED_INGREDIENTS); // 3つの引数を取るコンストラクターを呼ぶ } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients == null ? UNDEFINED_INGREDIENTS : ingredients; } /** * 名前 (必須) */ private String name; /** * 値段 (必須) */ private int price; /** * 原材料 (任意) */ private String ingredients; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getIngredients() { return ingredients; } public void setIngredients(String ingredients) { this.ingredients = ingredients; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Snack other = (Snack) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Snack [name=" + name + ", price=" + price + ", ingredients=" + ingredients + "]"; } }
Shop.java
package com.example.lesson02.model; import java.util.HashSet; import java.util.Iterator; import java.util.Optional; import java.util.Set; import com.example.lesson01.model.Snack; import com.example.lesson02.DealException; public class Shop { // デフォルトコンストラクタ― public Shop() { this(new HashSet<>(), 0); } public Shop(Set<Snack> snackVarieties, int sales) { this.snackVarieties = snackVarieties; this.sales = sales; } /** * お店が扱うおかしの種類 */ private Set<Snack> snackVarieties; /** * 売上金 */ private int sales; /** * お店が扱うおかしの種類を追加する * @param snack */ public void addSnackVariety(Snack snack) { this.snackVarieties.add(snack); } /** * お店が扱うおかしの種類を複数まとめて追加する * @param snacks */ public void addSnackVarieties(Snack... snacks) { this.snackVarieties.addAll(Set.of(snacks)); } /** * 指定した名前のおかしを扱っているか確認する * @param snackName おかしの名前 * @return true:扱っている */ public boolean isAvailableSnack(String snackName) { return getSnack(snackName).isPresent(); } /** * 指定した名前のおかしのオブジェクトを返す * @param snackName おかしの名前 * @return おかしのオブジェクト */ private Optional<Snack> getSnack(String snackName) { Iterator<Snack> ite = snackVarieties.iterator(); while (ite.hasNext()) { Snack snack = ite.next(); if (snack.getName().equals(snackName)) { return Optional.of(snack); } } return Optional.empty(); } /** * お店が扱っているおかしを表示する */ public void prettyPrintSnackVariety() { if (snackVarieties.isEmpty()) { System.out.println("取り扱うおかしはありません"); return; } snackVarieties.forEach(snack -> { System.out.println(snack.getName() + "は" + snack.getPrice() + "円"); }); System.out.println("計" + snackVarieties.size() + "種類のおかしを取り扱い中"); } /** * おかしを売る * @param snackName 売りおかしの名前 * @param customer おかしを買う人 * @throws DealException おかしの売買が失敗した場合 */ public void sellSnack(String snackName, Person customer) { Optional<Snack> optionalSnack = getSnack(snackName); optionalSnack.ifPresentOrElse(snack -> { // おかしが見つかった場合 // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; }, () -> { // おかしが見つからなかった場合 throw new DealException(snackName + "は取り扱っていません"); }); } @Override public String toString() { return "Shop [snackVarieties=" + snackVarieties + ", sales=" + sales + "]"; } }
Person.java
package com.example.lesson02.model; import java.util.ArrayList; import java.util.List; import com.example.lesson01.model.Snack; import com.example.lesson02.DealException; public class Person { public Person(String name, int money) { assert name != null; this.name = name; this.money = money; this.snackBag = new ArrayList<>(MAX_SNACK_NUM); } /** * 名前 */ private String name; /** * 所持金 */ private int money; /** * おかし袋 */ private List<Snack> snackBag; /** * おかし袋に入れられるおかしの最大数 */ private static final int MAX_SNACK_NUM = 5; /** * 持っているおかしを表示する */ public void prettyPrintHasSnack() { StringBuilder sb = new StringBuilder(name); sb.append("は 所持金:"); sb.append(money); sb.append("円"); if (!snackBag.isEmpty()) { sb.append("とおかし "); String comma = ""; for (Snack snack : snackBag) { sb.append(String.format("%s(name:%s, price=%d)", comma, snack.getName(), snack.getPrice())); comma = ", "; } } sb.append(" を持っている"); System.out.println(sb.toString()); } /** * お店と取引する * @param shop 取引するお店 * @param snackName 買うおかしの名前 */ public void deal(Shop shop, String snackName) { assert shop != null; assert snackName != null; try { shop.sellSnack(snackName, this); } catch (DealException e) { System.out.println(snackName + "の購入に失敗しました"); System.err.println(e.getMessage()); } } /** * おかしを買う * @param snack 買うおかしのオブジェクト * @return 支払った金額 * @throws DealException おかしが買えなかった場合にスロー */ int buySnack(Snack snack) throws DealException { if (money < snack.getPrice()) { throw new DealException("所持金が足りません"); } if (MAX_SNACK_NUM <= snackBag.size()) { throw new DealException("おかし袋に空きがありません"); } money -= snack.getPrice(); snackBag.add(snack); return snack.getPrice(); } @Override public String toString() { return "Person [name=" + name + ", money=" + money + ", snackBag=" + snackBag + "]"; } }
以上で、前編・中編・後編と続けたこの記事もひとまず終了です。