クラスとインスタンスの状態遷移


フィールドという機能を使ってオブジェクトの「状態」を表現できることを説明しました。
http://d.hatena.ne.jp/java-book/20090119/1232293562

今回はインスタンスとクラスという言葉をキーワードに、オブジェクトの状態遷移について話したいと思います。

コード

今回は以下のコードを先に紹介してしまいます。
前回の SavingsBox に機能を追加しました。

SavingsBox.java

class SavingsBox {

    private static int totalAll = 0; // 全貯金箱の合計金額

    private String name; // 貯金箱の識別子
    private int total; // 貯金箱の合計金額
    private boolean lock; // 貯金箱の施錠状態

    // コンストラクタ
    public SavingsBox(String name) {
        this.name = name;
        this.total = 0;
        this.lock = false;
    }

    // 100円玉を入れる。
    public void insertCoin100(int coins) {
        if(this.lock) {
            // コインは挿入できない。メッセージにその旨が表示される。
            this.showMessage("この貯金箱には鍵がかかっています。");
        } else {
            if (coins > 0) {
                this.total += coins * 100;
                SavingsBox.totalAll += coins * 100;
            }
            // 物理的にマイナスのコインが挿入されることはありえない。
        }
    }

    // 500円玉を入れる。
    public void insertCoin500(int coins) {
        if(this.lock) {
            // コインは挿入できない。メッセージにその旨が表示される。
            this.showMessage("この貯金箱には鍵がかかっています。");
        } else {
            if (coins > 0) {
                total += coins * 500;
                SavingsBox.totalAll += coins * 500;
            }
            // 物理的にマイナスのコインが挿入されることはありえない。
        }
    }

    // 全貯金箱の合計金額を調べる。
    public static int getAllAmount() {
        return SavingsBox.totalAll;
    }

    public String getName() {
        return this.name;
    }

    // 貯金箱の合計金額を調べる。
    public int getAmount() {
        return this.total;
    }

    // 貯金箱に鍵をかける。
    public void lock() {
        this.showMessage("貯金箱に鍵をかけました。");
        this.lock = true;
    }

    // 貯金箱の鍵を外す。
    public void unlock() {
        this.showMessage("貯金箱の鍵を外しました。");
        this.lock = false;
    }

    // 貯金箱に付いているディスプレイにメッセージを表示する。
    private void showMessage(String message) {
        System.out.println(message);
    }

}


Main.java

import java.util.ArrayList;
import java.util.Scanner;

class Main {

    public static void main(String[] args) {
        ArrayList<SavingsBox> boxes = new ArrayList<SavingsBox>();
        boxes.add(new SavingsBox("A"));
        boxes.add(new SavingsBox("B"));
        boxes.add(new SavingsBox("C"));
        Scanner scanner = new Scanner(System.in);
        while(true) {
            System.out.println("================================");
            System.out.println("全貯金箱の合計金額 " + SavingsBox.getAllAmount());
            for(SavingsBox box : boxes) {
                System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)");
            }
            System.out.println("================================");
            int index = 0;
            for(SavingsBox box : boxes) {
                System.out.println(index + ": 貯金箱 " + box.getName() + " に 100円玉を入れる。");
                System.out.println(index+1 + ": 貯金箱 " + box.getName() + " に 500円玉を入れる。");
                System.out.println(index+2 + ": 貯金箱 " + box.getName() + " に 鍵をかける。");
                System.out.println(index+3 + ": 貯金箱 " + box.getName() + " に 鍵を外す。");
                index = index + 4;
            }
            System.out.println("================================");
            System.out.println("");
            System.out.print("コマンド No を選んでください(例: 1)(抜ける場合は quit と入力): ");
            String line = scanner.nextLine();
            if("quit".equals(line)) {
                break;
            }
            int commandNumber = Integer.valueOf(line);
            if(commandNumber < 0 || commandNumber > index) {
                continue;
            }
            int targetIndex = commandNumber / 4;
            int methodIndex = commandNumber % 4;
            SavingsBox target = boxes.get(targetIndex);
            if(methodIndex == 0) {
                target.insertCoin100(1);
            } else if(methodIndex == 1) {
                target.insertCoin500(1);
            } else if(methodIndex == 2) {
                target.lock();
            } else if(methodIndex == 3) {
                target.unlock();
            }
        }
    }
}

インスタンスメソッドとインスタンスフィールド

boxes.add(new SavingsBox("A"));
boxes.add(new SavingsBox("B"));
boxes.add(new SavingsBox("C"));

new はオブジェクトを生成するという話で進めていましたが、クラスから生成されたオブジェクトのことを「インスタンス」とも呼びます。

class から生成された時点で、そのオブジェクトは他のオブジェクトとは別の存在となっています。
どういうことかと言えば、

boxA.insertCoin100(5); // boxA には 500円が入る。具体的には boxA のフィールド total に 500 という数字が入る。
boxB.insertCoin100(2); // boxB には 200円が入る。具体的には boxB のフィールド total に 200 という数字が入る。
boxC.insertCoin100(8); // boxC には 800円が入る。具体的には boxC のフィールド total に 800 という数字が入る。

このように、インスタンスフィールド(total 等)は各インスタンスごとに用意されるため、その領域はインスタンスが自由に使ってよい領域となり、他のインスタンスに使われるということもありません。
この機能を使って、「オブジェクトの状態」を表現することができます。

状態遷移

フィールドという領域を使って「状態」を表現すると何が嬉しいのでしょうか。
それを説明するために、新たな状態として lock という状態を追加しました。

lock() というメソッドで鍵をかけることができるようになり、unlock() で鍵を外すことができます。
この施錠状態を insertCoin100() メソッドで参照するようにしてみましょう。
すると、
「鍵がかかっている場合は鍵を外さないとコインを入れることができない」
「鍵がかかっていなければコインを入れることができる」
このように、オブジェクトにメッセージで頼み事をしたときに、そのオブジェクトの状態によって振る舞いが変わるようになります。

static メソッドと static フィールド

インスタンスがそれぞれ状態を持つことが可能であり、振る舞いを変えることができる話をしました。
それでは、特定のインスタンスに対してメッセージを投げかけるのではなく、「クラス全体に対して呼び出す」ようなことはできないのでしょうか。
それが static メソッドです。

static メソッドはインスタンスメソッドを呼び出すことはできませんし、インスタンスフィールドにアクセスすることもできません。
同じファイルに書くため紛らわしいのですが、static のついたクラスのための定義と、それ以外のインスタンスのための定義は別物ということに気をつけましょう。

insertCoin100() メソッドを呼ぶときに totalAll に対して加算を行っています。
これはインスタンス自身ではなく、static というクラスのための領域へ加算しています。
この機能を使うことで、各インスタンスに金額が追加されたときに全体の貯金額を更新することができるようになります。

new から生まれたインスタンスに呼びかけることはできず、クラスに属したメソッドとなるため、呼び出すときには以下のようになります。

SavingsBox.getAllAmount();

拡張 for 構文

最後に、Main で以前にも紹介した Scanner と ArrayList を使って複数のインスタンスに対して入力を与えて振る舞いを確認できる CUI を用意しました。
今回はおまけとして、拡張 for 構文というものを紹介します。

for(SavingsBox box : boxes) {
    System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)");
}

今までの方法であれば以下のように書いていたと思います。

for(int i = 0; i < boxes.size(); i++) {
    System.out.println("貯金箱 " + box.getName() + " (" + box.getAmount() + " 円)");
}

このようなインデックスを使って 0 から数える記述は、上限が size() などではなくもっと曖昧なものだった場合に上限を超えてエラーになる危険性などが考えられます。

それに対しての拡張 for 構文は、リストの中にある要素を展開するという手法で、どちらがよいという話ではありませんが、全要素を参照したい場合にはこのような書き方ができるということを紹介しておきます(拡張 for 構文は JDK 5.0 以上で使用できます)。