カプセル化

これまでのプログラムでは、インスタンスの外からフィールドを勝手に読み書きすることができました。 Javaでは、フィールドを直接読み書きすることを防ぎ、しかるべき手段によってのみ読み書きできるように制限することができます。 そうすることで、データの不適切な書き換えや読み出しを防ぐことができます。 それを実現する手法をカプセル化(encapsulation)と呼びます。 ここでは、カプセル化によってより安全で品質の高いクラスを構築する方法を解説していきます。

privateアクセス修飾子とアクセッサメソッド

カプセル化では次の2つのことを行います。

  • フィールドにprivateアクセス修飾子を付けて、クラスの外からは見えないようにする。
  • フィールドの値を取得・設定するアクセッサメソッドを宣言する。

Javaには、フィールドへのアクセスを制御するためのアクセス修飾子がいくつか用意されています。 ここでは、そのうちのprivateを使います。 private を付けて宣言されたフィールドは、宣言したクラスの外からは見えなくなります。 つまり、外部から直接読み書きをすることができなくなるのです。 そうなると、そのフィールドの値を外部から利用できなくなってしまうように思えるかもしれません。 しかし、実際にはメソッドを通して間接的に読み書きすることができるので問題有りません。

フィールドの値を取得するメソッドをゲッターメソッド(getter method)、フィールドの値を設定するメソッドをセッターメソッド(setter method)と呼びます。 また、ゲッターメソッドとセッターメソッドをまとめてアクセッサメソッド(accessor method)と呼びます。 アクセッサを通じて、間接的にフィールドへの読み書きをすることができます。

RobotTest06は、カプセル化を施したRobotクラスを初期化して結果を表示するプログラムです。

RobotTest06.java
//ロボットクラス【第6版】
class Robot {
    private String name;    //名前
    private int battery;    //バッテリー残量
    private int x;    //x座標
    
    /*---- コンストラクタ ----*/
    Robot(){ }
    
    Robot(String n){
        name = n;  battery = 0;  x = 0;
    }
    
    Robot(String n, int btry){
        name = n;  setBattery(btry);  x = 0;
    }
    
    /*---- バッテリー残量を取得する ----*/
    int getBattery(){
        return battery;
    }
    
    /*---- 名前を設定する ----*/
    void setName(String n){
        name = n;
    }
    
    /*---- バッテリー残量を設定する ----*/
    void setBattery(int btry){
        if(btry >= 0){
            battery = btry;
        }else{
            battery = 0;
        }
    }
    
    /*---- x軸方向に歩く ----*/
    boolean walkX(int dx){
        int d = dx >= 0 ?  dx : -dx;    //dxの絶対値をdに代入しておく。
        if(d <= battery){
            x += dx;
            battery -= d;
            return true;
        }else{
            return false;
        }
    }
    
    /*---- 状態を表示する ----*/
    void showStatus(){
        System.out.println("現在のx座標: " + x);
        System.out.println("バッテリー残量: " + battery);
    }
    
    /*---- 名前と状態を表示する ----*/
    void show(){
        System.out.println("名前: " + name);
        showStatus();
    }
}

//ロボットクラス【第6版】のテスト用クラス
class RobotTest06 {
    public static void main(String[] args) {
        Robot robot1 = new Robot();
        //robot1.name = "ロボ太郎";    //private宣言されたフィールドへの代入
        //robot1.battery = 100;    //private宣言されたフィールドへの代入
        robot1.setName("ロボ太郎");
        robot1.setBattery(100);
        
        Robot robot2 = new Robot("ロボ子");
        robot2.setBattery(-90);
        if(robot2.getBattery() == 0)
            robot2.setBattery(90);
        
        robot1.show();    System.out.println();
        robot2.show();
    }
}
RobotTest06の実行結果
名前: ロボ太郎
現在のx座標: 0
バッテリー残量: 100

名前: ロボ子
現在のx座標: 0
バッテリー残量: 90

フィールドの宣言にprivateを付けたことにより、これまでのように直接値を代入することはできなくなっています。 試しに次の2行の先頭の // を外してみてください。 そうすると、コンパイルのときにエラーとなります。

        //robot1.name = "ロボ太郎";    //private宣言されたフィールドへの代入
        //robot1.battery = 100;    //private宣言されたフィールドへの代入

そこで、フィールドに値を設定するには、次のようにセッターメソッドのsetNameとsetBatteryを使うことになります。

        robot1.setName("ロボ太郎");
        robot1.setBattery(100);

次の部分では、バッテリー残量としては不適切な負の値を設定しようとしています。

        robot2.setBattery(-90);

setBatteryメソッドは、0以上の値を受け取った場合はbatteryフィールドにその値をそのまま代入します。 一方、負の値を受け取った場合はbatteryフィールドに0を代入するようになっています。

    /*---- バッテリー残量を設定する ----*/
    void setBattery(int btry){
        if(btry >= 0){
            battery = btry;
        }else{
            battery = 0;
        }
    }

従って、-90という不適切な値がバッテリー残量に設定されてしまう心配はありません。 不適切な値を受け取った場合の処理はいろいろ考えられますが、ここでは簡単に0を代入するだけとしておきます。

この段階でバッテリー残量が0ならば、もう一度設定し直すようになっています。 ここでは、ゲッターメソッドのgetBatteryの戻り値が0であるかどうかをチェックしています。

        if(robot2.getBattery() == 0)
            robot2.setBattery(90);

直前に不適切な値を設定しようとして結果的に0が設定されているため、ここでは robot2.getBattery() == 0 の値はtrueとなり、 robot2.setBattery(90) が実行されることになります。

if文の条件を次のように書き換えてbatteryフィールドを直接利用しようとすると、やはりコンパイルのときにエラーとなります。

        if(robot2.battery == 0)
            robot2.setBattery(90);

もう一つ、次のコンストラクタはRobotTest06では使われていないのですが、注意すべきことがあります。

    Robot(String n, int btry){
        name = n;  setBattery(btry);  x = 0;
    }

このコンストラクタでも仮引数btryに負の値が渡される可能性がありますから、それをそのままbatteryフィールドに代入するわけにはいきません。 if文を用いてsetBatteryメソッドの本体と同様の処理をしても良さそうですが、いっそのこと既成のsetBatteryメソッドを呼び出すのが簡潔かつ安全です。

尚、Robotクラス【第6版】ではnameフィールドのゲッターメソッドは宣言されていません。 従って、クラスの外部から名前を取得することは絶対にできません。 showメソッドによって表示することができるだけです。

図 10-12 : privateアクセス修飾子とアクセッサメソッド

カプセル化の利点

クラスをカプセル化すると、極端な場合フィールドの読み書きを完全に禁止することができます。 また、読み書きのいずれか一方だけを可能にすることもできますし、適切な値が設定されるように制御することもできます。

それだけではありません。 カプセル化によって、クラスを利用する側には影響を与えること無く、クラスの内部構造を変更するということも容易になります。 このことは、既に運用が開始されている大きなシステムの中では決定的に重要な利点です。

このように、カプセル化によってデータの隠蔽・保護と保守性の向上を実現することができます。 原則としてすべてのフィールドにprivateを付け、必要に応じてアクセッサメソッドを用意しましょう。

ただし、これはあくまでも実践的なプログラミングで行えば良いことです。 学習用のプログラムや単なるテスト用のプログラムでは、カプセル化にこだわる必要はないでしょう。

原則としてすべてのフィールドをprivateとして、必要に応じてアクセッサを用意すること。

アクセッサメソッドの命名法

既にRobotTest06.javaでゲッターメソッドとセッターメソッドを利用していますが、ここでその命名法について説明しておきましょう。

セッターメソッドはget <フィールド名> 、セッターメソッドはset <フィールド名> という形にすることが推奨されています。 ただし、boolean型のフィールドの場合、ゲッターメソッドはis <フィールド名> とするのが一般的です。

RobotTest06.javaを見る限り、ゲッターメソッドもセッターメソッドも普通のメソッドに過ぎず、この命名法に従わなくてもよいと思われます。 しかしクラスをBeanとして扱う場合や、XMLEncoderでインスタンスを保存する場合等にはこの命名法が決定的に重要になります。 必要になったときにメソッド名を変更しなくても済むように、最初からこの命名法に従っておく方が良いでしょう。

Java言語で作られた再利用可能な汎用部品クラスのことをBeanと呼びます。 また、Beanを組み合わせてアプリケーションソフトを構築する手法、もしくはBeanを作成するための技術仕様をJavaBeansと呼びます。