目次へ

7.継承

2006.04.04 株式会社四次元データ 西谷太郎

この章では、クラスの機能であり、オブジェクト指向の重要な概念のひとつ、「継承」について説明します。継承とはあるクラスを基にして、別のクラスを作成するための機能です。

7.1. 継承とは?

さて、ここでは少し寄り道をして、「生徒」を定義するクラスが必要になったとして、Student.javaというファイルを2通り作ってみます。

  • パターンA
  • public class Student extends Human {
    
        private String school = null;
    
        public String getSchool() {
            return school;
        }
    
        public void setSchool(String school) {
            this.school = school;
        }
    
        public String toString() {
            return "名前は" + name + "さんです。年は" + age + "歳です。学校は" + school + "です。";
        }
    
    }
        
  • パターンB
  • public class Student {
    
        private String name = null;
        private int age = -1;
        private String school = null;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getSchool() {
            return school;
        }
    
        public void setSchool(String school) {
            this.school = school;
        }
    
        public String toString() {
            return "名前は" + name + "さんです。年は" + age + "歳です。学校は" + school + "です。";
        }
    
    }
        

実は、パターン A とパターン B は見た目は違いますが、同じ内容のクラスを表しています。え?と、思われた人もいるでしょう。ではまずパターン B を見てみてください。
実はこれは6章までの Human クラスを下敷きにし、フィールドとして school を加え、それに関するメソッドを加えただけのものです。

次にパターン A を見てみましょう。大体パターン B の1/3の量のプログラムになっています。中身を見ると、ここに書かれているのはほとんど全てが、Human からパターン B を作るときに付け加えたり変更したものです。

なぜこんなことが出来るのでしょうか?その秘密は、パターン A のクラス名のすぐ後ろにある

extends Human
    

にあります。これによって Human クラスのメンバを引き継いでいるわけです。この機能を「継承」といいます。

ここで扱っている「生徒」は、生徒であると同時に「人間」でもあります。ですから当然、生徒は人間の性質(メンバ)も併せ持っています。このように、Studentクラスのインスタンスは、Humanクラスのインスタンスとしての特徴を余すことなく持っているので、プログラム的には、Studentクラスのインスタンスは、Humanクラスのインスタンスとして取り扱うことが可能となります。実はこの継承という概念は、単純にプログラムの量を減らしてくれるだけでなく、思いのほか大きなメリットをプログラマに与えてくれます。このあたりは、デザインパターンなどを勉強すると、身にしみて感じられると思います。

継承を行うためには、宣言したクラス名の後ろに、

extends 継承元のクラス名
    

を付け加えるだけです。

これで継承先のクラス(サブクラスといいます)は継承元のクラス(スーパークラスといいます)の持つメンバと、新たに書き加えたフィールド、メソッドを持つクラスになります。
しかし、これをこのままコンパイルしてもコンパイルエラーになってしまいます。何故でしょうか?

7.2. アクセス修飾子

それは、ここまで特に説明をせずに使ってきたアクセス修飾子のせいです。
アクセス修飾子を利用すると、どのような場合にメンバを利用できるかを限定することで、意図しない操作を防いだり、内部の処理を隠したりすることができます。これをカプセル化と言います。

フィールドとメソッドのアクセス制御は3種類のアクセス修飾子を使って、次のように4段階に分かれています。

修飾子意味
publicどのクラスからもアクセス可能
protectedサブクラスおよび同一パッケージのクラスからのみアクセス可能
修飾子なし(デフォルトアクセス)同一パッケージのクラスからのみアクセス可能
private同一クラスからのみアクセス可能

下に行くほど厳しい制限がかかっています。
パッケージに関しては、9章で説明しています。

継承はあくまで、スーパークラスの記述を流用しているだけで、実際に記述してあるわけではありません。
ここでの、name、age などのフィールド、getName()、setAge() などのメソッドは Human クラスが持っているもので、Student クラスが利用するときは Human クラスにアクセスしなければなりません。
ここでは、サブクラス Student のスーパークラス Human のフィールドはすべて private になっています(同一クラスからのみアクセス可能=サブクラスからのアクセスが許可されていない)。toString() メソッド内で、このprivateなフィールドに直接参照しようとしているので、コンパイルエラーになってしまっているのです。
では、どのようにすれば利用できるのでしょうか?

一つ目の案として、制限を緩めれば当然アクセスは可能になります。ここでは以下のように protected もしくは、public であればコンパイルエラーにはなりません。

public class Human {

    protected String name = null;
    public int age = -1;

      ・
      ・
      ・
}
    

ですが、これでは当初のアクセス制御の目的を達成できなくなってしまいます。
そこで、ここではメソッドを利用します。

Human.java を作成したときに、フィールドの値を取得する getter メソッド( getName() など)を作りました。これらのメソッドのアクセス修飾子には public を指定しています。つまり、どのクラスからでもアクセスできるメソッドということです。したがって、これらのメソッドを介してフィールドを参照すれば、コンパイルエラーにはなりません。
これらのメソッドの実装により、クラスの外から行えるフィールドの操作は、「その値を呼び出す」に限定できます。ここでは他に setter メソッドも実装しているので、フィールドに値を設定することも出来ます。また、メソッドを隠す場合もあります。
以上を踏まえて、以下のように修正しましょう。

public String toString() {
    return "名前は" + getName() + "さんです。年は" + getAge() + "歳です。学校は" + school + "です。";
}
    

また、継承に関わる修飾子に final 修飾子があります。クラス自体に final 修飾子がついていると継承が不可能になります。メソッドに final 修飾子をつけると、オーバーライド(後述)が不可能になります。
ではフィールドにつけると?その値が定数になり、変更ができなくなります。

7.3. オーバーライド

継承を用いたときにだけ発生するオーバーライドについて説明します。
オーバーライドとは「上書き」のことです。5章で解説したオーバーローディングとは、名前は似ていますがまったく別ものなので、混同しないように注意してください。
何を上書きするかというと、下位クラスが上位クラスから引っ張ってきたメソッドです。つまり、下位のクラス内でメソッドを定義しなおすのです。

オーバーライドをする際にはいくつかの規定があります。

  • オーバーライドする側とオーバーライドされる側の戻り値の型、メソッド名、引数の型、引数の数、引数の並び順が同じであること。
  • アクセスレベルが元のメソッドよりも緩くなっていないこと。
  • オーバーライド元のメソッドが投げる例外以外の例外を、オーバーライド先のメソッドが投げていないこと。(ただし、元のメソッドの投げる例外を限定したものであればよい。)
  • オーバーライド元のメソッドに修飾子 final がついていないこと。

  • スーパークラスで abstract として定義されているメソッドはオーバーライドするか、サブクラス自身を abstract にしなければならない。
  • と、わかりにくい説明ははここまでにして、実は既にオーバーライドしている箇所があります。
    Student クラスと Human クラスのそれぞれの最後を見てください。どちらにも toString() というメソッドがあると思います。
    このメソッドはインスタンスの持つ情報を一度に開示する文字列を得るためのメソッドです。何もしなければ、Student クラスに Human クラスの toString() が継承されるのですが、Student.java にはわざわざもう一度 toString() メソッドが書いてあります。
    Human クラスの toString() メソッドはフィールドの name、age の値を開示する文字列を返しています。それに対して、Student クラスの toString() メソッドには、さらに school の値をも返しています。Student クラスの持つ全ての情報を返すには、Human クラスの toString() では不十分です。よって、もう一度定義し直すために、toString() をオーバーライドしたのです。

    お互いに public で、String 型の値を返し、toString と言う名前で、引数をとらない。final は付いていないし、例外も投げていない、ということで、上記の条件を満たしていて、ここではオーバーライドが行われていることがわかります。
    Student クラスでオーバーライドを行わずに、Student クラス内で toString() を呼び出すと、行われる処理は

    return "名前は" + name + "さんです。年は" + age + "歳です。";
        

    となります。この toString() は Human クラスから継承されたもので、Human クラスで記述された処理が呼び出されています。ここでオーバーライドを行うと、

    return "名前は" + getName() + "さんです。年は" + getAge() + "歳です。学校は" + school + "です。";
        

    というオーバーライド後の処理が呼び出されます。これで、学校の名前も表示されるようになりました。

    ちなみに、Human クラス内でも toString() はオーバーライドされています。toString() メソッドは Object クラスに定義されているメソッドで、オブジェクトの情報を文字列で返します。extends 句を持たないクラスでも、全てのクラスは自動的に Object クラスを継承します。よって、Human クラスも本来は Object クラスの toString() メソッドを継承していたのですが、オーバーライドすることで新たに toString() メソッドを定義し直しているわけです。

    ↑このページの先頭へ

    こちらもチェック!

    PR
    • XMLDB.jp