コンピュータ基礎および演習II

講義資料

誤った継承の使用と弊害

継承を用いるとスーパクラスの属性やメソッドを取り込むことができます。 すなわち、サブクラスからはスーパクラスの属性を参照することや、 変更することが可能になるということです。 これは、 「本来オブジェクトは内部の状態を見せることなくデータと手続きを一体化し、 外部からはメソッドを用いてのみオブジェクトにアクセスする、」 というカプセル化の利点を減らしてしまいます。

このため、継承は使う必然性が認められる場面でのみ用いるべきである、 と考えておくべきです。 継承を使う必然性が認められる場面とは、 サブクラスはスーパクラスの一種である、 という関係が明確に成り立つ場合です。 英語では、 Sub class is a kind of super class. と書くことから、 あるクラスが別のクラスのサブクラスであることを、 「is-a の関係」と言います。

継承を利用すべきではない場面

前々回の演習問題の 2 番で、 クラス CD と CDPlayer を定義しました。

クラス CDPlayer を定義するために、 クラス CD を継承し、 CD に演奏機能を付け加えたクラスを定義すれば便利である、 と以下のようなプログラムを考えることができるかも知れません。

class CD {
    String artist;
    String title;
    String[] tracks;

    void setArtist(String name) { ... }
    String getArtist() { ... }
    void setTitle(String t) { ... }
    String getTitle() { ... }
    void setTracks(String[] t) { ... }
    String[] getTracks() { ... }
}

class CDPlayer extends CD {
    void load() { ... }
    void play() { ... }
    void play(int trackNo) { ... }
    void eject() { ... }
}

クラス CD を継承した、クラス CDPlayer では、 CD のタイトルやアーティスト、曲名といった属性をそのまま使えるので、 CD を演奏する、ということだけに注目すれば便利なように見えます。

継承を用いるべきかどうかを判断する一つの方法は、 サブクラスがスーパクラスの性質を持っているか、 すなわち、サブクラスがスーパークラスの属性やメソッドを 受け継いで持つのが適切かどうかを考えることです。

この例では、 CD プレイヤの中に、 アーティスト名やタイトルという属性が存在してしまうことになります。 これらは、本来 CD プレイヤの属性としては正しくないものです。 このような関係は、is-a の関係ではありません。 したがって、継承を用いるのは適切ではありません。

また、仮に、別の場面でここで定義した CD を用い、 CD をたくさん収納できる棚 (コンテナ) クラスを作成した場合、 CD を入れるはずの棚に CDPlayer まで収納できることになってしまいます。 このように、論理的に間違ったオブジェクトの取り扱いができてしまうと、 設計に矛盾が生じたり、プログラムの間違いにつながります。

以上のように、 必然性があり継承を行った結果としてプログラム再利用ができる という利益が得られるのであって、 単に機能を取り込みたいという理由から 継承を用いるのは避けるべきである、 ということを理解する必要があります。

問題

「パソコン」というクラスを考えてみます。 パソコンにはキーボード、ディスプレイ、ディスクドライブなどがあります。 キーボードとディスプレイ、ディスクドライブ等のクラスをまとめて継承し、 これらの機能を統合したパソコンというクラスを作成する方法が考えらます。 これは、継承の正しい利用方法でしょうか。

クラスのメンバに他のクラスのインスタンスを取り込む方法

他のクラスの機能を利用するための方法の一つが、 必要なクラスのインスタンスをメンバ (属性) として内部に持つという方法です。

これを利用し、クラス CD と CDPlayer は次のように設計するのが適当です。

class CD {
    String artist;
    String title;
    String[] tracks;

    void setArtist(String name) { ... }
    String getArtist() { ... }
    void setTitle(String t) { ... }
    String getTitle() { ... }
    void setTracks(String[] t) { ... }
    String[] getTracks() { ... }
}

class CDPlayer {
    CD insertedDisc;

    void load(CD cd) { ... }
    void play() { ... }
    void play(int trackNo) { ... }
    void eject() { ... }
}

クラス CDPlayer のメンバに、 クラス CD のインスタンスを示す変数が宣言されています。
(CD insertedDisc;)

このように、利用したいクラスのインスタンスをメンバとして宣言するのです。 クラスの属性である insertedDisc を通じて CD クラスの機能を利用することができます。

このように、クラスに別のクラスのインスタンスを内部に取り込むことを、 コンポジション (composition) あるいは包含と言います。 コンポジションによってクラス A がクラス B を取り込んだ状態を、 英語で Class A has a class B と言うことから、 has-a の関係と呼びます。

問題

「パソコン」というクラスを考えてみます。 パソコンにはキーボード、ディスプレイ、ディスクドライブなどがあります。 これらの機能を統合するために、 キーボードとディスプレイ、ディスクドライブ等のクラスをメンバに持つ、 パソコンのクラスを作成する方法が考えられます。 これは、コンポジションの正しい利用方法でしょうか。

修飾子

今回取り上げる 2 つめの話題は修飾子です。 修飾子は、クラスの属性やメソッドを、 他のクラスからアクセスできるようにするかどうかや、 変数の書き換えを禁止させるかどうか、継承を許すかどうか、 といった様々な設定を行うためのものです。

エッセンシャル Java pp.176-182 を参照してください。

クラスの利点の1つに、 他のオブジェクトによるアクセスから クラスの属性を守ることができるということがあります。 属性は外部に対して公開されたメソッドを通じてのみ、 変更や参照を行うことができます。 これにより、意図しない属性の操作を防ぐことができます。

これまでは、メソッドを用いることで、 内部の属性に対して直接操作を行わないような プログラムの書き方をするように心掛けてきました。 しかし、属性を直接書き換えるような、 好ましくないプログラムも作ろうと思えば作れてしまいます。 Java には、 属性やメソッドへのアクセスを明示的に許可、禁止する方法があります。

Java では、クラスの属性とメソッドを宣言するときに、 それらを保護するために、 以下の 4 つのアクセスレベルを指定することができます。 private, protected, public そして指定なしのアクセスレベルです。

以下のクラス TDUStudent の定義を見てみましょう。 (メソッドの内容は省略しています。)

class TDUStudent {
    int id;
    String name;
    int math;
    int english;
    int physics;

    TDUStudent(int i, String n) { ... }
    void setScore(int m, int e, int p) { ... }
    String getName() { ... }
    int average() { ... }
}

id, name, math, english, physics の各属性が外部に公開されたままであると、 次のように、外部から値を代入したり、参照することが可能です。

class Stranger {
    public static void main(String[] args) {
        TDUStudent john = new TDUStudent(1, "John Lennon");

        john.name = "Ringo Starr";
        john.math = 20;
        System.out.println(john.math);
    }
}

このままでは、勝手に氏名が書き換えられてしまったり、 本来平均点を求めることしかできないはずであるのに、 勝手に数学の点数を盗み見ることができてしまったりと、 クラスの設計に意図しない使われ方ができてしまいます。

秘密にしたい情報を属性に持つオブジェクトを、 別のオブジェクトに渡して処理を依頼したいという場合に、 内部の秘密情報に勝手にアクセスされては困ります。 オブジェクトは公開されたメソッドを通してアクセスされるべきです。

以下に、それぞれ 4 つのアクセスレベルについて説明します。

private

もっとも厳しいアクセス制限レベルは private です。 private のメンバは、 そのクラスの中でのみアクセス可能となります。 外部からアクセスされたくない属性や、 外部から実行されたくないメソッドを private にします。

private のメンバを宣言するには、 宣言時にキーワード private を使用します。

次の例は、クラス TDUStudent の各属性を private にした場合の プログラムです。

class TDUStudent {
    private int id;
    private String name;
    private int math;
    private int english;
    private int physics;

    ......
    ......
}

属性だけでなく、メソッドも private にすることができます。 private メソッドは、 そのクラスの中からのみ実行することができるようになります。

public

最もオープンなアクセスレベルは public です。 public メンバは外部に対して公開され、 任意のクラスからアクセスすることが可能です。 外部から参照される属性や、 外部から実行されるメソッドは public にします。

public のメンバを宣言するには、 宣言時にキーワード public を使用します。

次の例は、クラス TDUStudent の各属性を private にし、 各メソッドを public にした場合のプログラムです。

class TDUStudent {
    private int id;
    private String name;
    private int math;
    private int english;
    private int physics;

    TDUStudent(int i, String n) { ... }
    public void setScore(int m, int e, int p) { ... }
    public String getName() { ... }
    public int average() { ... }
}

protected

protected のメンバは、 そのクラスの中と、そのクラスを継承したサブクラスからのみ アクセス可能となります。

クラスとそのサブクラスに対してのみ公開し、 外部のクラスに対しては非公開としたいメンバは、 protected にします。

ただし、 そのクラスのファイルが存在する同じディレクトリ内にあるクラスに対しては、 サブクラスではなくてもメンバにアクセス可能です。 変則的な規則に思えるかも知れませんが、 これは、「同じパッケージ内では公開される」という決まりなのです。

パッケージについては、この講義では取り扱えません。 皆さんの後学にまかせることにします。

指定なし

これまでのプログラムでは、 main メソッドを除いて、 修飾子を特に指定ませんでした。

この場合、 そのクラスのファイルが存在する 同じディレクトリ内にあるすべてのクラスに対しては、 メンバにアクセス可能となります。 すなわち、「同じパッケージ内では公開される」という決まりなのです。

private, public, protected のキーワードは、 いずれか一つのみを指定することしかできません。

これらをまとめると、 エッセンシャル Java p.178 あるいは p.182 の表のようになります。

その他の修飾子

変数のその他の修飾子

クラスの属性や局所変数 (ローカル変数) の宣言時に指定することができる 修飾子に以下のものがあります。

final で指定された変数は、 宣言時に初期化し、それ以降値の変更ができないことを示します。

プログラムの中で一度決めたら途中で変更することがない定数として用いる場合、 キーワード final を用いて変数宣言を行います。 以下に、簡単な例を示します。

public AClass {
    // 円周率 PI を 3.14 に初期化
    private final double PI = 3.14;
    ...

    public void aMethod() {
        // 自然対数の底 E を 2.72 に初期化
        final double E = 2.72;
        ...
    }
}

なお、上の変数 PI の宣言のように、 キーワード final は他の修飾子と組み合わせて使うことができます。

static については、後の回で扱います。

メソッドのその他の修飾子

private, public, protected に加えて、 メソッドに指定することができる修飾子に以下のものがあります。

static については、後の回で扱います。

また、その他の修飾子については、この講義では扱えません。 皆さんの自習にまかせることにします。

メソッド main

プログラムの実行が開始されるメソッドとして main メソッドがあります。 これは、 public 、すなわち外部から実行可能なメソッドとして、 以下のように宣言されています。

public static void main(String[] args) { ... }

実際の実行時には、 java コマンドで指定されたクラス内にあるメソッド main が、 まず最初に実行されるという決まりになっているのです。

では、なぜメソッド main はインスタンスを作らなくても 実行できるのでしょうか。 この疑問は次回解決されることになります。