rubytomato's “Getting Started”

Webアプリケーション開発の入門的な記事を投稿していきます。

『入門向け』Java開発環境の構築とかんたんなコードの書き方と実行方法 (後編)

はじめに

この記事は、 rubytomato.hateblo.jp rubytomato.hateblo.jp に続く記事の後編です。 前編、中編ではJava開発環境の構築とEclipseの簡単な使い方を中心に説明しました。この記事では引き続きJavaプログラムの書き方と実行方法を中心に説明します。

Javaプログラムの書き方

中編のおさらい

中編のレッスン1では、Fig1のようにstudy01projectプロジェクトにDemoクラスとSnackクラスまで作成しました。

f:id:rubytomato:20200404113228p:plain
Fig1.

レッスン2

レッスン2では、レッスン1で作成した「おかし」を表現するSnackクラスに加え、「おかしを売る店」を表現するShopクラスと「おかしを買う客」を表現するPersonクラスを使って、お店とお客がおかしを売買する機能をコーディングしていきます。

おかしを売買できる条件として以下の3つを仕様とします。

  • お店(Shop)は扱っているおかししか売れないこと
  • お客(Person)はおかしを買うのに代金を支払うこと
  • お客(Person)は買ったおかしをおかし袋に仕舞えること(最大5個まで仕舞える)

また、そのほかの仕様として

  • お店(Shop)はおかしをいくつでも販売できる。(おかしの在庫数は管理しない)
  • おかしの売買は『おかしの名前』で行う。(『おかしの名前』が一意で管理できる(重複しない)こと)

とします。

レッスン2用のパッケージを作成する

まずレッスン2用のパッケージcom.example.lesson02を作成し、lesson02パッケージにDemoクラスを作成します。Fig2はここまで作成した状態です。

f:id:rubytomato:20200404113719p:plain
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クラスのhashCodeequalsメソッドを正しくオーバーライドする必要があります。 たとえばSnackクラスのhashCodeequalsメソッドをオーバーライドしていない状態で以下のコードを実行すると、同じおかしの名前を持つ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つのオブジェクトを ”同じおかし”である(おかしの名前が同じなので)と判別するには、hashCodeequalsメソッドを以下のようにオーバーライドします。

  • メニューバーのソース(S)hashCode() および equals() の生成(H)...を選択
  • キーボードのAlt + Shift + Sを押し、メニューのhashCode() および equals() の生成(H)...を選択

"hashCode() および equals() の生成"画面(Fig3)のnameフィールドだけにチェックを入れ、"生成"ボタンをクリックします。

f:id:rubytomato:20200404113257p:plain
Fig3.

すると、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)でnamepriceフィールドにチェックを入れてコードを生成しなおします。

Shopクラスを作成する

まずcom.example.lesson02.modelパッケージを作成し、modelパッケージにShopクラスを作成します。 Fig4はmodelパッケージおよびShopクラスを作成した直後のプロジェクトの状態です。

f:id:rubytomato:20200404113314p:plain
Fig4.

以下がShopクラスを作成した直後のソースコードです。

package com.example.lesson02.model;

public class Shop {

}
フィールドを定義する

Shopクラスに、お店が扱うおかしの種類を持つSet型のsnackVarieties、おかしの売上金を持つint型のsalesの2つのフィールドを定義します。同じ名前のおかしを持たないようにsnackVarietiesSet型にしています。

アクセサメソッドは必要がないので実装しません。

/**
 * お店が扱うおかしの種類
 */
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クラスを作成した直後のプロジェクトの状態です。

f:id:rubytomato:20200404113340p:plain
Fig5.

以下が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を継承した例外クラスをスローするようにします。

f:id:rubytomato:20200404113424p:plain
Fig6.

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クラスの実装に移ります。

f:id:rubytomato:20200404113447p:plain
Fig7.

/**
 * おかしを売る
 * @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 + "]";
    }

}

以上で、前編・中編・後編と続けたこの記事もひとまず終了です。